From e9e7b7dcb264cf4a3bc7782c1dbc7310f3e3ae58 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 13 Oct 2026 21:35:51 +0800 Subject: [PATCH] =?UTF-8?q?test(memory):=20memory-tidy.json=20=E5=86=99?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=EF=BC=88=E8=A1=A5=E4=B8=81=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E5=85=B6=E5=AE=83=E9=94=AE=20/=20=E5=80=BC=E6=B2=A1=E5=8F=98?= =?UTF-8?q?=E4=B8=8D=E8=90=BD=E7=9B=98=20/=20=E5=9D=8F=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=8B=92=E5=86=99=20/=20=E5=8E=9F=E5=AD=90=E5=86=99=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 塔批准的 scope 里点名了 internal/config/runtimeconfig/memorytidy_save_test.go, 补上这四个口径的包内单测(网关侧另有端到端用例覆盖三态与审计)。 --- .../runtimeconfig/memorytidy_save_test.go | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 internal/config/runtimeconfig/memorytidy_save_test.go diff --git a/internal/config/runtimeconfig/memorytidy_save_test.go b/internal/config/runtimeconfig/memorytidy_save_test.go new file mode 100644 index 0000000..adc0cd4 --- /dev/null +++ b/internal/config/runtimeconfig/memorytidy_save_test.go @@ -0,0 +1,151 @@ +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) + } +}