package engine import ( "encoding/json" "os" "path/filepath" "testing" "time" ) // ────────────────────────────────────────────── // 辅助 // ────────────────────────────────────────────── // newTestFileScratchpad 在临时目录创建 FileScratchpad,测试结束后自动清理. func newTestFileScratchpad(t *testing.T) *FileScratchpad { t.Helper() sp, err := NewFileScratchpad(t.TempDir()) if err != nil { t.Fatalf("NewFileScratchpad: %v", err) } return sp } // ────────────────────────────────────────────── // NewFileScratchpad // ────────────────────────────────────────────── // TestFileScratchpad_NewCreatesDir 验证目录不存在时自动创建. func TestFileScratchpad_NewCreatesDir(t *testing.T) { dir := filepath.Join(t.TempDir(), "sub", "deep") sp, err := NewFileScratchpad(dir) if err != nil { t.Fatalf("unexpected error: %v", err) } if sp == nil { t.Fatal("expected non-nil FileScratchpad") } if _, err := os.Stat(dir); os.IsNotExist(err) { t.Fatalf("directory %q was not created", dir) } } // TestFileScratchpad_NewExistingDir 验证目录已存在时不报错. func TestFileScratchpad_NewExistingDir(t *testing.T) { _, err := NewFileScratchpad(t.TempDir()) if err != nil { t.Fatalf("unexpected error for existing dir: %v", err) } } // ────────────────────────────────────────────── // Set / Get 基本语义 // ────────────────────────────────────────────── // TestFileScratchpad_SetGet 验证写入后可以读回. func TestFileScratchpad_SetGet(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("hello", "world", 0) got, ok := sp.Get("hello") if !ok { t.Fatal("expected key to exist") } if got != "world" { t.Fatalf("expected 'world', got %q", got) } } // TestFileScratchpad_GetMissing 验证不存在的 key 返回 ("", false). func TestFileScratchpad_GetMissing(t *testing.T) { sp := newTestFileScratchpad(t) _, ok := sp.Get("nonexistent") if ok { t.Fatal("expected false for missing key") } } // TestFileScratchpad_SetOverwrite 验证同 key 多次 Set 以最后一次为准. func TestFileScratchpad_SetOverwrite(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("k", "v1", 0) sp.Set("k", "v2", 0) got, ok := sp.Get("k") if !ok || got != "v2" { t.Fatalf("expected 'v2', got %q ok=%v", got, ok) } } // TestFileScratchpad_SetEmptyValue 验证空字符串值可以正常存取. func TestFileScratchpad_SetEmptyValue(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("k", "", 0) got, ok := sp.Get("k") if !ok { t.Fatal("expected key to exist with empty value") } if got != "" { t.Fatalf("expected empty string, got %q", got) } } // TestFileScratchpad_SetSpecialCharsKey 验证含路径穿越字符的 key 安全处理. // SHA-256 文件名确保 "../../etc/passwd" 类 key 不能逃出目录. func TestFileScratchpad_SetSpecialCharsKey(t *testing.T) { sp := newTestFileScratchpad(t) key := "../../etc/passwd" sp.Set(key, "safe", 0) got, ok := sp.Get(key) if !ok || got != "safe" { t.Fatalf("expected 'safe', got %q ok=%v", got, ok) } } // TestFileScratchpad_SetUnicodeKey 验证 Unicode key 能正常工作. func TestFileScratchpad_SetUnicodeKey(t *testing.T) { sp := newTestFileScratchpad(t) key := "飞驼🐪网络" sp.Set(key, "仓储", 0) got, ok := sp.Get(key) if !ok || got != "仓储" { t.Fatalf("expected '仓储', got %q ok=%v", got, ok) } } // ────────────────────────────────────────────── // TTL 过期 // ────────────────────────────────────────────── // TestFileScratchpad_TTLExpiry 验证 TTL 过期后 Get 返回 false. func TestFileScratchpad_TTLExpiry(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("exp", "value", 10*time.Millisecond) // 过期前应该能读到 got, ok := sp.Get("exp") if !ok || got != "value" { t.Fatalf("expected value before expiry, got %q ok=%v", got, ok) } time.Sleep(20 * time.Millisecond) // 过期后应该读不到 _, ok = sp.Get("exp") if ok { t.Fatal("expected key to be expired after TTL") } } // TestFileScratchpad_TTLZeroNeverExpires 验证 ttl=0 永不过期. func TestFileScratchpad_TTLZeroNeverExpires(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("k", "v", 0) time.Sleep(5 * time.Millisecond) _, ok := sp.Get("k") if !ok { t.Fatal("ttl=0 key should never expire") } } // TestFileScratchpad_TTLRefreshOnOverwrite 验证覆盖写入刷新 TTL. func TestFileScratchpad_TTLRefreshOnOverwrite(t *testing.T) { sp := newTestFileScratchpad(t) // 写入短 TTL sp.Set("k", "v1", 20*time.Millisecond) // 在过期前覆盖写入更长 TTL time.Sleep(5 * time.Millisecond) sp.Set("k", "v2", 200*time.Millisecond) // 等原 TTL 应该已过(但覆盖后不会过) time.Sleep(20 * time.Millisecond) got, ok := sp.Get("k") if !ok || got != "v2" { t.Fatalf("expected refreshed value 'v2', got %q ok=%v", got, ok) } } // ────────────────────────────────────────────── // Delete // ────────────────────────────────────────────── // TestFileScratchpad_Delete 验证删除后 Get 返回 false. func TestFileScratchpad_Delete(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("k", "v", 0) sp.Delete("k") _, ok := sp.Get("k") if ok { t.Fatal("expected key to be deleted") } } // TestFileScratchpad_DeleteMissing 验证删除不存在的 key 不 panic. func TestFileScratchpad_DeleteMissing(t *testing.T) { sp := newTestFileScratchpad(t) sp.Delete("nonexistent") // should not panic } // TestFileScratchpad_DeleteFileRemoved 验证删除后底层文件已被移除. func TestFileScratchpad_DeleteFileRemoved(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("k", "v", 0) path := filepath.Join(sp.dir, keyToFilename("k")) if _, err := os.Stat(path); os.IsNotExist(err) { t.Fatal("expected file to exist after Set") } sp.Delete("k") if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatal("expected file to be removed after Delete") } } // ────────────────────────────────────────────── // Keys // ────────────────────────────────────────────── // TestFileScratchpad_KeysEmpty 验证空 scratchpad 返回空切片. func TestFileScratchpad_KeysEmpty(t *testing.T) { sp := newTestFileScratchpad(t) keys := sp.Keys() if len(keys) != 0 { t.Fatalf("expected empty keys, got %v", keys) } } // TestFileScratchpad_KeysSorted 验证 Keys 返回字典序有序列表. func TestFileScratchpad_KeysSorted(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("z", "1", 0) sp.Set("a", "2", 0) sp.Set("m", "3", 0) keys := sp.Keys() expected := []string{"a", "m", "z"} if len(keys) != len(expected) { t.Fatalf("expected %v, got %v", expected, keys) } for i, k := range expected { if keys[i] != k { t.Fatalf("keys[%d]: expected %q, got %q", i, k, keys[i]) } } } // TestFileScratchpad_KeysExcludesExpired 验证过期 key 不出现在 Keys 中. func TestFileScratchpad_KeysExcludesExpired(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("alive", "v", 0) sp.Set("dead", "v", 10*time.Millisecond) time.Sleep(20 * time.Millisecond) keys := sp.Keys() for _, k := range keys { if k == "dead" { t.Fatal("expired key 'dead' should not appear in Keys()") } } found := false for _, k := range keys { if k == "alive" { found = true } } if !found { t.Fatal("non-expired key 'alive' should appear in Keys()") } } // TestFileScratchpad_KeysAfterDelete 验证删除后不出现在 Keys 中. func TestFileScratchpad_KeysAfterDelete(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("a", "1", 0) sp.Set("b", "2", 0) sp.Delete("a") keys := sp.Keys() for _, k := range keys { if k == "a" { t.Fatal("deleted key 'a' should not appear in Keys()") } } } // ────────────────────────────────────────────── // 文件格式 & 原子写入 // ────────────────────────────────────────────── // TestFileScratchpad_FileContentValid 验证底层文件是有效 JSON 且内容正确. func TestFileScratchpad_FileContentValid(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("mykey", "myvalue", 0) path := filepath.Join(sp.dir, keyToFilename("mykey")) data, err := os.ReadFile(path) if err != nil { t.Fatalf("ReadFile: %v", err) } var entry fileScratchEntry if err := json.Unmarshal(data, &entry); err != nil { t.Fatalf("file content is invalid JSON: %v (content: %s)", err, data) } if entry.K != "mykey" { t.Fatalf("expected K=%q, got %q", "mykey", entry.K) } if entry.V != "myvalue" { t.Fatalf("expected V=%q, got %q", "myvalue", entry.V) } if entry.E != "" { t.Fatalf("expected no expiry for ttl=0, got %q", entry.E) } } // TestFileScratchpad_FileContentWithTTL 验证含 TTL 的条目文件包含过期时间. func TestFileScratchpad_FileContentWithTTL(t *testing.T) { sp := newTestFileScratchpad(t) sp.Set("k", "v", time.Hour) path := filepath.Join(sp.dir, keyToFilename("k")) data, _ := os.ReadFile(path) var entry fileScratchEntry if err := json.Unmarshal(data, &entry); err != nil { t.Fatalf("invalid JSON: %v", err) } if entry.E == "" { t.Fatal("expected non-empty expiry for ttl=1h") } } // TestFileScratchpad_KeyToFilenameStable 验证相同 key 总是映射到相同文件名. func TestFileScratchpad_KeyToFilenameStable(t *testing.T) { f1 := keyToFilename("some_key") f2 := keyToFilename("some_key") if f1 != f2 { t.Fatalf("keyToFilename not stable: %q vs %q", f1, f2) } } // TestFileScratchpad_KeyToFilenameUnique 验证不同 key 映射到不同文件名(SHA-256 碰撞实际为 0). func TestFileScratchpad_KeyToFilenameUnique(t *testing.T) { keys := []string{"a", "b", "c", "abc", "ABC", "1", "../../etc/passwd"} seen := make(map[string]string) for _, k := range keys { fn := keyToFilename(k) if prev, ok := seen[fn]; ok { t.Fatalf("collision: key %q and %q map to same filename %q", k, prev, fn) } seen[fn] = k } } // ────────────────────────────────────────────── // 跨实例持久化(模拟跨进程共享) // ────────────────────────────────────────────── // TestFileScratchpad_PersistsAcrossInstances 验证数据在不同 FileScratchpad 实例间共享. // 模拟多个 Worker 进程挂载同一目录场景. func TestFileScratchpad_PersistsAcrossInstances(t *testing.T) { dir := t.TempDir() // Worker 1 写入 sp1, err := NewFileScratchpad(dir) if err != nil { t.Fatalf("sp1: %v", err) } sp1.Set("shared_key", "shared_value", 0) // Worker 2(新实例,模拟不同进程)读取 sp2, err := NewFileScratchpad(dir) if err != nil { t.Fatalf("sp2: %v", err) } got, ok := sp2.Get("shared_key") if !ok || got != "shared_value" { t.Fatalf("expected 'shared_value', got %q ok=%v", got, ok) } } // TestFileScratchpad_KeysAcrossInstances 验证 Keys 跨实例可见. func TestFileScratchpad_KeysAcrossInstances(t *testing.T) { dir := t.TempDir() sp1, _ := NewFileScratchpad(dir) sp1.Set("k1", "v1", 0) sp1.Set("k2", "v2", 0) sp2, _ := NewFileScratchpad(dir) keys := sp2.Keys() if len(keys) != 2 { t.Fatalf("expected 2 keys across instances, got %v", keys) } } // TestFileScratchpad_MultipleKeys 验证多 key 操作一致性. func TestFileScratchpad_MultipleKeys(t *testing.T) { sp := newTestFileScratchpad(t) data := map[string]string{ "alpha": "1", "beta": "2", "gamma": "3", } for k, v := range data { sp.Set(k, v, 0) } for k, expected := range data { got, ok := sp.Get(k) if !ok || got != expected { t.Fatalf("key %q: expected %q, got %q ok=%v", k, expected, got, ok) } } keys := sp.Keys() if len(keys) != 3 { t.Fatalf("expected 3 keys, got %v", keys) } }