package cache import ( "errors" "sync" "sync/atomic" "testing" "time" ) // TestTTLCache_SetAndGet 测试基本的 Set/Get func TestTTLCache_SetAndGet(t *testing.T) { c := NewTTLCache[string, int](time.Minute, nil) c.Set("key1", 42) v, ok := c.Get("key1") if !ok || v != 42 { t.Errorf("Get(key1) = %d, %v, want 42, true", v, ok) } // 不存在的 key _, ok = c.Get("nonexistent") if ok { t.Error("Get(nonexistent) should return false") } } // TestTTLCache_Expiry 测试过期后仍返回旧值(stale-while-revalidate) func TestTTLCache_Expiry(t *testing.T) { // 精妙之处(CLEVER): loadCount 用 atomic.Int32-- // loader 在后台 goroutine 中运行,与测试 goroutine 并发访问 loadCount. // 不用 atomic 会触发 -race 检测报告数据竞争. var loadCount atomic.Int32 c := NewTTLCache[string, int](time.Millisecond*50, func(key string) (int, error) { loadCount.Add(1) return 100, nil }) // 设置初始值 c.Set("key1", 42) // 等待过期 time.Sleep(time.Millisecond * 60) // 过期后仍应返回旧值 v, ok := c.Get("key1") if !ok || v != 42 { t.Errorf("expired Get(key1) = %d, %v, want 42, true (stale)", v, ok) } // 等待后台刷新完成 time.Sleep(time.Millisecond * 50) // 刷新后应返回新值 v, ok = c.Get("key1") if !ok || v != 100 { t.Errorf("refreshed Get(key1) = %d, %v, want 100, true", v, ok) } if n := loadCount.Load(); n != 1 { t.Errorf("loader should have been called once, called %d times", n) } } // TestTTLCache_GetOrLoad_Miss 测试缓存 miss 时同步加载 func TestTTLCache_GetOrLoad_Miss(t *testing.T) { c := NewTTLCache[string, string](time.Minute, func(key string) (string, error) { return "loaded_" + key, nil }) v, err := c.GetOrLoad("mykey") if err != nil { t.Fatalf("GetOrLoad error: %v", err) } if v != "loaded_mykey" { t.Errorf("GetOrLoad = %q, want %q", v, "loaded_mykey") } // 再次 Get 应该命中缓存 v2, ok := c.Get("mykey") if !ok || v2 != "loaded_mykey" { t.Errorf("Get after GetOrLoad = %q, %v, want %q, true", v2, ok, "loaded_mykey") } } // TestTTLCache_GetOrLoad_NoLoader 测试没有 loader 时 GetOrLoad 返回错误 func TestTTLCache_GetOrLoad_NoLoader(t *testing.T) { c := NewTTLCache[string, int](time.Minute, nil) _, err := c.GetOrLoad("key") if err == nil { t.Error("GetOrLoad without loader should return error") } if !errors.Is(err, ErrNoLoader) { t.Errorf("expected ErrNoLoader, got %v", err) } } // TestTTLCache_GetOrLoad_LoaderError 测试 loader 返回错误 func TestTTLCache_GetOrLoad_LoaderError(t *testing.T) { loadErr := errors.New("load failed") c := NewTTLCache[string, int](time.Minute, func(key string) (int, error) { return 0, loadErr }) _, err := c.GetOrLoad("key") if err == nil { t.Error("GetOrLoad should propagate loader error") } if !errors.Is(err, loadErr) { t.Errorf("expected load error, got %v", err) } } // TestTTLCache_Invalidate 测试失效 func TestTTLCache_Invalidate(t *testing.T) { c := NewTTLCache[string, int](time.Minute, nil) c.Set("key1", 42) c.Invalidate("key1") _, ok := c.Get("key1") if ok { t.Error("Get after Invalidate should return false") } } // TestTTLCache_Peek 测试不触发刷新的读取 func TestTTLCache_Peek(t *testing.T) { loadCount := int32(0) c := NewTTLCache[string, int](time.Millisecond*10, func(key string) (int, error) { atomic.AddInt32(&loadCount, 1) return 100, nil }) c.Set("key1", 42) // 等待过期 time.Sleep(time.Millisecond * 20) // Peek 不应触发后台刷新 v, ok := c.Peek("key1") if !ok || v != 42 { t.Errorf("Peek = %d, %v, want 42, true", v, ok) } time.Sleep(time.Millisecond * 50) if atomic.LoadInt32(&loadCount) != 0 { t.Errorf("Peek should not trigger refresh, loader called %d times", loadCount) } } // TestTTLCache_Len 测试长度 func TestTTLCache_Len(t *testing.T) { c := NewTTLCache[string, int](time.Minute, nil) if c.Len() != 0 { t.Errorf("empty cache Len = %d", c.Len()) } c.Set("a", 1) c.Set("b", 2) if c.Len() != 2 { t.Errorf("Len = %d, want 2", c.Len()) } } // TestTTLCache_Clear 测试清空 func TestTTLCache_Clear(t *testing.T) { c := NewTTLCache[string, int](time.Minute, nil) c.Set("a", 1) c.Set("b", 2) c.Clear() if c.Len() != 0 { t.Errorf("Len after Clear = %d, want 0", c.Len()) } } // TestTTLCache_ThunderingHerd 测试 thundering herd 防护 func TestTTLCache_ThunderingHerd(t *testing.T) { var loadCount int32 c := NewTTLCache[string, int](time.Millisecond*10, func(key string) (int, error) { atomic.AddInt32(&loadCount, 1) // 模拟慢加载 time.Sleep(time.Millisecond * 100) return 100, nil }) c.Set("key1", 42) // 等待过期 time.Sleep(time.Millisecond * 20) // 多个 goroutine 同时 Get 过期的 key var wg sync.WaitGroup for i := 0; i < 20; i++ { wg.Add(1) go func() { defer wg.Done() v, ok := c.Get("key1") if !ok { t.Error("Get should return true for expired entry") } // 应该返回旧值(stale) if v != 42 && v != 100 { t.Errorf("Get = %d, want 42 or 100", v) } }() } wg.Wait() // 等待后台刷新完成 time.Sleep(time.Millisecond * 150) // loader 应该只被调用一次(thundering herd 防护) count := atomic.LoadInt32(&loadCount) if count != 1 { t.Errorf("loader should be called exactly once, called %d times", count) } } // TestTTLCache_BackgroundRefreshFailure 测试后台刷新失败后可重试 func TestTTLCache_BackgroundRefreshFailure(t *testing.T) { callCount := int32(0) c := NewTTLCache[string, int](time.Millisecond*10, func(key string) (int, error) { n := atomic.AddInt32(&callCount, 1) if n == 1 { return 0, errors.New("temporary failure") } return 200, nil }) c.Set("key1", 42) // 等待过期 time.Sleep(time.Millisecond * 20) // 第一次 Get 触发后台刷新(会失败) v, ok := c.Get("key1") if !ok || v != 42 { t.Errorf("Get = %d, %v, want 42, true", v, ok) } // 等待刷新失败 time.Sleep(time.Millisecond * 50) // refreshing 标记应被清除,再次 Get 应能触发新的刷新 v, ok = c.Get("key1") if !ok || v != 42 { t.Errorf("Get after failed refresh = %d, %v, want 42, true", v, ok) } // 等待第二次刷新成功 time.Sleep(time.Millisecond * 50) v, ok = c.Get("key1") if !ok || v != 200 { t.Errorf("Get after successful refresh = %d, %v, want 200, true", v, ok) } } // TestTTLCache_ConcurrentSetGet 测试并发读写安全 func TestTTLCache_ConcurrentSetGet(t *testing.T) { c := NewTTLCache[int, int](time.Minute, nil) var wg sync.WaitGroup const n = 100 // 并发写 for i := 0; i < n; i++ { wg.Add(1) go func(v int) { defer wg.Done() c.Set(v, v*10) }(i) } // 并发读 for i := 0; i < n; i++ { wg.Add(1) go func(k int) { defer wg.Done() c.Get(k) }(i) } wg.Wait() // 所有 key 应该存在 if c.Len() != n { t.Errorf("Len = %d, want %d", c.Len(), n) } }