package runtimeconfig import ( "encoding/json" "errors" "os" "path/filepath" "runtime" "testing" ) // 本文件锁定 `memory-tidy.json` **写路径**(PatchMemoryTidyTuning)的四条口径: // ① 补丁语义:只改提交的键,盘上其它键(t1Judge 等)**原样保留**; // ② 值没变 ⇒ 一个字节都不写(连 mtime 都不动); // ③ 盘上那份**读不走**时拒绝覆盖(ErrInvalidMemoryTidy),且**盘上不动**; // ④ 原子写:temp + rename,成功之后不留 `.tmp`,文件权限 0600(Windows 不校验)。 // patchTuning 是测试用的补丁构造:键 → 原样 JSON 值。 func patchTuning(t *testing.T, pairs map[string]any) map[string]json.RawMessage { t.Helper() out := make(map[string]json.RawMessage, len(pairs)) for k, v := range pairs { raw, err := json.Marshal(v) if err != nil { t.Fatalf("marshal %s: %v", k, err) } out[k] = raw } return out } // TestPatchMemoryTidyTuning_MergesAndKeepsOtherKeys 补丁只动提交的键,其余键原样保留; // 返回值是**重新读盘**的结果(真值),不是「我以为写成了什么」。 func TestPatchMemoryTidyTuning_MergesAndKeepsOtherKeys(t *testing.T) { t.Parallel() path := writeMemoryTidyFile(t, `{"t1Judge": false, "t1PerWindow": 120, "intervalSec": 3600}`) tuning, wrote, err := PatchMemoryTidyTuning(path, patchTuning(t, map[string]any{"intervalSec": 7200}), nil) if err != nil { t.Fatalf("patch: %v", err) } if !wrote { t.Fatal("值变了却没写盘") } if tuning.IntervalSec == nil || *tuning.IntervalSec != 7200 { t.Fatalf("返回值不是读回的真值:%+v", tuning.IntervalSec) } if tuning.T1Judge == nil || *tuning.T1Judge { t.Fatalf("没提交的 t1Judge 被动了:%+v", tuning.T1Judge) } doc := map[string]json.RawMessage{} raw, err := os.ReadFile(path) if err != nil { t.Fatalf("read: %v", err) } if err := json.Unmarshal(raw, &doc); err != nil { t.Fatalf("decode: %v", err) } if string(doc["intervalSec"]) != "7200" || string(doc["t1PerWindow"]) != "120" { t.Fatalf("盘上内容 = %s", raw) } if string(doc["t1Judge"]) != "false" { t.Fatalf("不受管的 t1Judge 被抹掉了:%s", raw) } // 清除哨兵:clear 里的键从盘上消失(= 回到下一档兜底)。 if _, _, err := PatchMemoryTidyTuning(path, nil, []string{"intervalSec"}); err != nil { t.Fatalf("clear: %v", err) } doc = map[string]json.RawMessage{} raw, _ = os.ReadFile(path) if err := json.Unmarshal(raw, &doc); err != nil { t.Fatalf("decode: %v", err) } if _, ok := doc["intervalSec"]; ok { t.Fatalf("clear 之后盘上仍有 intervalSec:%s", raw) } if _, ok := doc["t1Judge"]; !ok { t.Fatalf("clear 抹掉了别的键:%s", raw) } } // TestPatchMemoryTidyTuning_NoChangeWritesNothing 值没变 ⇒ 不写盘、不重排格式。 func TestPatchMemoryTidyTuning_NoChangeWritesNothing(t *testing.T) { t.Parallel() path := writeMemoryTidyFile(t, `{"intervalSec": 3600}`) before, err := os.ReadFile(path) if err != nil { t.Fatalf("read: %v", err) } tuning, wrote, err := PatchMemoryTidyTuning(path, patchTuning(t, map[string]any{"intervalSec": 3600}), nil) if err != nil { t.Fatalf("patch: %v", err) } if wrote { t.Fatal("值没变却写了盘") } after, _ := os.ReadFile(path) if string(after) != string(before) { t.Fatalf("值没变却改了盘上内容:\nbefore=%s\nafter=%s", before, after) } if tuning.IntervalSec == nil || *tuning.IntervalSec != 3600 { t.Fatalf("没写盘也要回读真值:%+v", tuning.IntervalSec) } } // TestPatchMemoryTidyTuning_RefusesUnreadableFile 盘上那份运行期读不走时拒绝覆盖: // 返回 ErrInvalidMemoryTidy(调用方据此回 409),且**盘上一个字节都不动**。 func TestPatchMemoryTidyTuning_RefusesUnreadableFile(t *testing.T) { t.Parallel() for _, body := range []string{`{"intervalSec": "3600"}`, `not json at all`, `[1, 2]`} { body := body t.Run(body, func(t *testing.T) { path := writeMemoryTidyFile(t, body) _, wrote, err := PatchMemoryTidyTuning(path, patchTuning(t, map[string]any{"intervalSec": 3600}), nil) if !errors.Is(err, ErrInvalidMemoryTidy) { t.Fatalf("err = %v, want ErrInvalidMemoryTidy", err) } if wrote { t.Fatal("读不走的文件被写入了") } raw, _ := os.ReadFile(path) if string(raw) != body { t.Fatalf("坏文件被改动了:%s", raw) } }) } } // TestPatchMemoryTidyTuning_CreatesMissingFileAtomically 文件缺失 ⇒ 建目录并原子写出 // (temp + rename):成功之后不留 `.tmp`,权限 0600(Windows 不校验)。 func TestPatchMemoryTidyTuning_CreatesMissingFileAtomically(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "config", memoryTidyFileName) tuning, wrote, err := PatchMemoryTidyTuning(path, patchTuning(t, map[string]any{"tempTtlSec": 604800}), nil) if err != nil { t.Fatalf("patch: %v", err) } if !wrote { t.Fatal("缺失文件时应当写出") } if tuning.TempTtlSec == nil || *tuning.TempTtlSec != 604800 { t.Fatalf("回读真值 = %+v", tuning.TempTtlSec) } if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) { t.Fatalf("原子写留下了临时文件:err=%v", err) } if info, err := os.Stat(path); err != nil { t.Fatalf("stat: %v", err) } else if info.Mode().Perm() != memoryTidySaveFileMode && runtime.GOOS != "windows" { t.Fatalf("文件权限 = %v, want %v", info.Mode().Perm(), memoryTidySaveFileMode) } }