塔批准的 scope 里点名了 internal/config/runtimeconfig/memorytidy_save_test.go, 补上这四个口径的包内单测(网关侧另有端到端用例覆盖三态与审计)。
152 lines
5.4 KiB
Go
152 lines
5.4 KiB
Go
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)
|
||
}
|
||
}
|