map[string]*list.Element + doubly linked list; Get/Put move hits to the front, Put evicts from the back when at capacity, and every method (including Len) takes the mutex. Add TestLenRacesWithPut, which calls Len concurrently with a writer — the existing test only read Len after wg.Wait(), so an unsynchronized Len would never trip the race detector there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MpK9niZm4jcs3mAJbxMfi
125 lines
2.6 KiB
Go
125 lines
2.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestBasicPutGet(t *testing.T) {
|
|
c := NewLRU(2)
|
|
c.Put("a", 1)
|
|
if v, ok := c.Get("a"); !ok || v.(int) != 1 {
|
|
t.Fatalf("want (1,true), got (%v,%v)", v, ok)
|
|
}
|
|
if _, ok := c.Get("missing"); ok {
|
|
t.Fatal("miss reported as hit")
|
|
}
|
|
}
|
|
|
|
func TestEvictionOrder(t *testing.T) {
|
|
c := NewLRU(2)
|
|
c.Put("a", 1)
|
|
c.Put("b", 2)
|
|
c.Put("c", 3) // evicts a (LRU)
|
|
if _, ok := c.Get("a"); ok {
|
|
t.Fatal("a should have been evicted")
|
|
}
|
|
for _, k := range []string{"b", "c"} {
|
|
if _, ok := c.Get(k); !ok {
|
|
t.Fatalf("%s should still be present", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetRefreshesRecency(t *testing.T) {
|
|
c := NewLRU(2)
|
|
c.Put("a", 1)
|
|
c.Put("b", 2)
|
|
c.Get("a") // a becomes MRU
|
|
c.Put("c", 3) // must evict b, not a
|
|
if _, ok := c.Get("a"); !ok {
|
|
t.Fatal("a was evicted despite being MRU — Get must refresh recency")
|
|
}
|
|
if _, ok := c.Get("b"); ok {
|
|
t.Fatal("b should have been evicted")
|
|
}
|
|
}
|
|
|
|
func TestUpdateExistingKey(t *testing.T) {
|
|
c := NewLRU(2)
|
|
c.Put("a", 1)
|
|
c.Put("b", 2)
|
|
c.Put("a", 99) // update, not insert — must NOT evict
|
|
if c.Len() != 2 {
|
|
t.Fatalf("update inflated size: len=%d", c.Len())
|
|
}
|
|
if v, _ := c.Get("a"); v.(int) != 99 {
|
|
t.Fatalf("update lost: got %v", v)
|
|
}
|
|
c.Put("c", 3) // evicts b (a was refreshed by the update)
|
|
if _, ok := c.Get("b"); ok {
|
|
t.Fatal("b should have been evicted after a's update refreshed it")
|
|
}
|
|
}
|
|
|
|
func TestConcurrentAccess(t *testing.T) {
|
|
c := NewLRU(64)
|
|
var wg sync.WaitGroup
|
|
for g := 0; g < 8; g++ {
|
|
wg.Add(1)
|
|
go func(g int) {
|
|
defer wg.Done()
|
|
for i := 0; i < 500; i++ {
|
|
k := fmt.Sprintf("k%d", i%100)
|
|
c.Put(k, i)
|
|
c.Get(k)
|
|
}
|
|
}(g)
|
|
}
|
|
wg.Wait()
|
|
if c.Len() > 64 {
|
|
t.Fatalf("capacity exceeded under concurrency: %d", c.Len())
|
|
}
|
|
}
|
|
|
|
// TestLenRacesWithPut calls Len concurrently WITH writers, not after them.
|
|
// TestConcurrentAccess only reads Len after wg.Wait(), which establishes
|
|
// happens-before with every writer — so an unsynchronized Len can't race
|
|
// there. The race detector reports only races that actually occur.
|
|
func TestLenRacesWithPut(t *testing.T) {
|
|
c := NewLRU(64)
|
|
done := make(chan struct{})
|
|
var wg sync.WaitGroup
|
|
|
|
wg.Add(1)
|
|
go func() { // writer
|
|
defer wg.Done()
|
|
for i := 0; ; i++ {
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
c.Put(fmt.Sprintf("k%d", i%128), i)
|
|
}
|
|
}
|
|
}()
|
|
|
|
wg.Add(1)
|
|
go func() { // reader, concurrently
|
|
defer wg.Done()
|
|
for i := 0; i < 10000; i++ {
|
|
if n := c.Len(); n > 64 {
|
|
t.Errorf("Len observed %d > capacity 64 (mid-Put state leaked)", n)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() { wg.Wait() }()
|
|
time.Sleep(50 * time.Millisecond)
|
|
close(done)
|
|
wg.Wait()
|
|
}
|