package web import ( "sync" "testing" "rond-with-you/internal/fog" ) // S1 回归:清空迷雾时换索引指针,而瓦片/导出/统计正在同时读它。 // // ⚠️ 这条用例**必须带 -race 跑才有意义**: // // go test -race -count=1 -run TestFogIndexConcurrentResetAndRead ./internal/web/ // // 不带 -race 时它只能证明「并发下不 panic、拿到的不是 nil」。 func TestFogIndexConcurrentResetAndRead(t *testing.T) { f := newLoginFixture(t) // 收尾:把索引恢复成「从库里加载」的状态,别把清空后的空索引留给后面的用例 t.Cleanup(func() { if ix := fog.NewIndex(); ix.LoadAll(f.srv.db) == nil { fogIndexV.Store(ix) } }) // 先触发一次初始加载,让后面的并发都落在「读已发布指针」这条路径上 if _, err := f.srv.fogIndex(); err != nil { t.Fatalf("首次加载迷雾索引失败: %v", err) } var wg sync.WaitGroup for i := 0; i < 8; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 200; j++ { ix, err := f.srv.fogIndex() if err != nil || ix == nil { t.Errorf("并发读取拿到了空索引: ix=%v err=%v", ix, err) return } ix.Stats() // 真实读者会做的事(内部有自己的锁) } }() } wg.Add(1) go func() { defer wg.Done() for j := 0; j < 200; j++ { fogResetIndex() // 与 adminFogClear 同一个调用 } }() wg.Wait() }