// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package cache import ( "sync" "testing" "time" ) func TestTTLCacheBasic(t *testing.T) { c := NewTTLCache(8) if _, ok := c.Get("missing"); ok { t.Fatal("expected miss for empty cache") } c.Set("k", "v", time.Minute) v, ok := c.Get("k") if !ok || v.(string) != "v" { t.Fatalf("expected hit with v, got %v %v", v, ok) } } func TestTTLCacheExpiry(t *testing.T) { c := NewTTLCache(8) c.Set("k", "v", time.Nanosecond) time.Sleep(2 * time.Millisecond) if _, ok := c.Get("k"); ok { t.Fatal("expected expired entry to miss") } } func TestTTLCacheCapacityEviction(t *testing.T) { c := NewTTLCache(4) for i := 0; i < 100; i++ { c.Set(string(rune('a'+i%26))+string(rune('0'+i%10)), i, time.Minute) } // 不 panic 且仍可读 if _, ok := c.Get("a0"); ok { // 命中与否均可,关键是内存不失控且无 panic _ = ok } } func TestTTLCacheConcurrent(t *testing.T) { c := NewTTLCache(16) var wg sync.WaitGroup for g := 0; g < 20; g++ { wg.Add(1) go func(n int) { defer wg.Done() for i := 0; i < 200; i++ { key := string(rune('a' + (n+i)%26)) c.Set(key, n, time.Minute) c.Get(key) } }(g) } wg.Wait() } // TestTTLCacheGetWithExpiryKeepsEntry 锁住一个关键语义: // GetWithExpiry **不删除**过期条目,否则 stale-while-revalidate 拿不到旧值, // 只能退化成「过期即彻底 miss → 同步重算」,那正是投票结果 O(N²) 的成因。 func TestTTLCacheGetWithExpiryKeepsEntry(t *testing.T) { c := NewTTLCache(8) c.Set("k", "v", time.Nanosecond) time.Sleep(2 * time.Millisecond) v, exp, ok := c.GetWithExpiry("k") if !ok { t.Fatal("expired entry should still be readable via GetWithExpiry") } if v.(string) != "v" { t.Fatalf("expected stale value v, got %v", v) } if !time.Now().After(exp) { t.Fatal("entry should already be expired") } // 再读一次仍然在(证明上一次调用没有顺手删掉它) if _, _, ok := c.GetWithExpiry("k"); !ok { t.Fatal("GetWithExpiry must not evict the entry") } // 而普通 Get 仍按原语义返回 miss(并清掉它) if _, ok := c.Get("k"); ok { t.Fatal("Get should report miss for an expired entry") } } // TestTTLCacheGetWithExpiryMissing 不存在的 key 必须返回 ok=false, // 且不能凭空造出一个零值过期时间让调用方误判为「过期但有值」。 func TestTTLCacheGetWithExpiryMissing(t *testing.T) { c := NewTTLCache(8) if _, _, ok := c.GetWithExpiry("nope"); ok { t.Fatal("expected miss for unknown key") } } // TestTTLCacheExpireKeepsValue 锁住 Expire 与 Delete 的区别: // Expire 后条目仍在(可拿到旧值并触发后台刷新),而 Delete 之后彻底 miss。 func TestTTLCacheExpireKeepsValue(t *testing.T) { c := NewTTLCache(8) c.Set("k", "v", time.Hour) c.Expire("k") v, exp, ok := c.GetWithExpiry("k") if !ok || v.(string) != "v" { t.Fatalf("Expire must keep the value, got %v %v", v, ok) } if !time.Now().After(exp) { t.Fatal("Expire must push expiresAt into the past") } // 普通 Get 立刻视为过期 if _, ok := c.Get("k"); ok { t.Fatal("Get should see the expired entry as a miss") } // 对不存在的 key 调用 Expire 不应 panic 也不应创建条目 c.Expire("absent") if _, _, ok := c.GetWithExpiry("absent"); ok { t.Fatal("Expire must not create an entry") } }