test(memory): memory-tidy.json 写路径(补丁保留其它键 / 值没变不落盘 / 坏文件拒写 / 原子写)
塔批准的 scope 里点名了 internal/config/runtimeconfig/memorytidy_save_test.go, 补上这四个口径的包内单测(网关侧另有端到端用例覆盖三态与审计)。
This commit is contained in:
1 parent
27ee0a8149
commit
e9e7b7dcb2
1 file changed
+151
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user