package repository import ( "encoding/json" "os" "sync" "testing" "opsxloki/models" ) // TestConcurrentSaveNoDataLoss 是并发写安全(B1/B2)的回归测试: // N 个 goroutine 并发修改 db 并各自 RequestSave(经防抖 channel 序列化), // 随后同步刷盘,直接读取磁盘文件校验写入数量与 ID 唯一性—— // 证明 channel 序列化 + 原子重命名写入不会发生"后写覆盖先写"的丢更新。 func TestConcurrentSaveNoDataLoss(t *testing.T) { const N = 50 var wg sync.WaitGroup for i := 0; i < N; i++ { wg.Add(1) go func() { defer wg.Done() db := GetDB() // NextID 内部自行加锁;切勿在持有 db.Lock() 时调用(mu 不可重入,会自死锁) id := db.NextID("hosts") db.Lock() db.Hosts = append(db.Hosts, models.Host{ID: id, Name: "concurrent-host"}) db.Unlock() db.RequestSave() }() } wg.Wait() // 同步刷盘一次(绕过 500ms 防抖),确保最新内存状态落盘 if err := GetDB().RequestSaveSync(); err != nil { t.Fatalf("RequestSaveSync: %v", err) } data, err := os.ReadFile(dbPath()) if err != nil { t.Fatalf("read db file: %v", err) } var disk JsonDB if err := json.Unmarshal(data, &disk); err != nil { t.Fatalf("unmarshal db file: %v", err) } if len(disk.Hosts) != N { t.Fatalf("DATA LOSS: disk has %d hosts, want %d (concurrent saves lost writes)", len(disk.Hosts), N) } // ID 唯一性:NextID 在 mutex 保护下递增,不应出现重复 seen := make(map[uint]bool, N) for _, h := range disk.Hosts { if seen[h.ID] { t.Fatalf("duplicate host ID %d (NextID race)", h.ID) } seen[h.ID] = true } } // TestRequestSaveDebounce 校验防抖行为:短时间内多次 RequestSave 不会撑爆 channel。 func TestRequestSaveDebounce(t *testing.T) { db := GetDB() startLen := len(db.Hosts) // 连续触发 100 次防抖保存(channel 容量 10,防抖应合并而非阻塞) for i := 0; i < 100; i++ { id := db.NextID("hosts") // 自行加锁,勿在 db.Lock() 内调用 db.Lock() db.Hosts = append(db.Hosts, models.Host{ID: id, Name: "debounce"}) db.Unlock() db.RequestSave() } if err := db.RequestSaveSync(); err != nil { t.Fatalf("RequestSaveSync after debounce: %v", err) } data, _ := os.ReadFile(dbPath()) var disk JsonDB if err := json.Unmarshal(data, &disk); err != nil { t.Fatalf("unmarshal: %v", err) } // 100 次追加 + 之前测试的 N 个 host,必须全部落盘 if len(disk.Hosts) != startLen+100 { t.Fatalf("debounce lost writes: disk has %d hosts, want %d", len(disk.Hosts), startLen+100) } }