diff options
| author | 2026-02-21 10:30:53 +0800 | |
|---|---|---|
| committer | 2026-02-21 11:00:52 +0800 | |
| commit | 19dc6aedddde8b5306f1fb0dc4d46ba57f318cce (patch) | |
| tree | 06ed3241ff837818005e46c8f0da7a370d6e482d /internal/cache/lru/lru_test.go | |
| parent | objectid: Add RawBytes (diff) | |
| signature | No signature | |
cache/lru: Add basic LRU
Diffstat (limited to 'internal/cache/lru/lru_test.go')
| -rw-r--r-- | internal/cache/lru/lru_test.go | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/internal/cache/lru/lru_test.go b/internal/cache/lru/lru_test.go new file mode 100644 index 00000000..9ce113f0 --- /dev/null +++ b/internal/cache/lru/lru_test.go @@ -0,0 +1,210 @@ +package lru_test + +import ( + "slices" + "testing" + + "codeberg.org/lindenii/furgit/internal/cache/lru" +) + +type testValue struct { + weight int64 + label string +} + +func weightFn(key string, value testValue) int64 { + return value.weight +} + +func TestCacheEvictsLRUAndGetUpdatesRecency(t *testing.T) { + t.Parallel() + + cache := lru.New[string, testValue](8, weightFn, nil) + cache.Add("a", testValue{weight: 4, label: "a"}) + cache.Add("b", testValue{weight: 4, label: "b"}) + cache.Add("c", testValue{weight: 4, label: "c"}) + + if _, ok := cache.Peek("a"); ok { + t.Fatalf("expected a to be evicted") + } + if _, ok := cache.Peek("b"); !ok { + t.Fatalf("expected b to be present") + } + if _, ok := cache.Peek("c"); !ok { + t.Fatalf("expected c to be present") + } + + if _, ok := cache.Get("b"); !ok { + t.Fatalf("Get(b) should hit") + } + cache.Add("d", testValue{weight: 4, label: "d"}) + + if _, ok := cache.Peek("c"); ok { + t.Fatalf("expected c to be evicted after b was touched") + } + if _, ok := cache.Peek("b"); !ok { + t.Fatalf("expected b to remain present") + } + if _, ok := cache.Peek("d"); !ok { + t.Fatalf("expected d to be present") + } +} + +func TestCachePeekDoesNotUpdateRecency(t *testing.T) { + t.Parallel() + + cache := lru.New[string, testValue](4, weightFn, nil) + cache.Add("a", testValue{weight: 2, label: "a"}) + cache.Add("b", testValue{weight: 2, label: "b"}) + + if _, ok := cache.Peek("a"); !ok { + t.Fatalf("Peek(a) should hit") + } + cache.Add("c", testValue{weight: 2, label: "c"}) + + if _, ok := cache.Peek("a"); ok { + t.Fatalf("expected a to be evicted; Peek must not update recency") + } + if _, ok := cache.Peek("b"); !ok { + t.Fatalf("expected b to remain present") + } +} + +func TestCacheReplaceAndResize(t *testing.T) { + t.Parallel() + + var evicted []string + cache := lru.New[string, testValue](10, weightFn, func(key string, value testValue) { + evicted = append(evicted, key+":"+value.label) + }) + + cache.Add("a", testValue{weight: 4, label: "old"}) + cache.Add("b", testValue{weight: 4, label: "b"}) + cache.Add("a", testValue{weight: 6, label: "new"}) + + if cache.Weight() != 10 { + t.Fatalf("Weight() = %d, want 10", cache.Weight()) + } + if got, ok := cache.Peek("a"); !ok || got.label != "new" { + t.Fatalf("Peek(a) = (%+v,%v), want new,true", got, ok) + } + if !slices.Equal(evicted, []string{"a:old"}) { + t.Fatalf("evicted = %v, want [a:old]", evicted) + } + + cache.SetMaxWeight(8) + if _, ok := cache.Peek("b"); ok { + t.Fatalf("expected b to be evicted after shrinking max weight") + } + if !slices.Equal(evicted, []string{"a:old", "b:b"}) { + t.Fatalf("evicted = %v, want [a:old b:b]", evicted) + } +} + +func TestCacheRejectsOversizedWithoutMutation(t *testing.T) { + t.Parallel() + + var evicted []string + cache := lru.New[string, testValue](5, weightFn, func(key string, value testValue) { + evicted = append(evicted, key) + }) + cache.Add("a", testValue{weight: 3, label: "a"}) + + if ok := cache.Add("b", testValue{weight: 6, label: "b"}); ok { + t.Fatalf("Add oversized should return false") + } + if got, ok := cache.Peek("a"); !ok || got.label != "a" { + t.Fatalf("cache should remain unchanged after oversized add") + } + if cache.Weight() != 3 { + t.Fatalf("Weight() = %d, want 3", cache.Weight()) + } + if len(evicted) != 0 { + t.Fatalf("evicted = %v, want none", evicted) + } + + if ok := cache.Add("a", testValue{weight: 6, label: "new"}); ok { + t.Fatalf("oversized replace should return false") + } + if got, ok := cache.Peek("a"); !ok || got.label != "a" { + t.Fatalf("existing key should remain unchanged after oversized replace") + } + if len(evicted) != 0 { + t.Fatalf("evicted = %v, want none", evicted) + } +} + +func TestCacheRemoveAndClear(t *testing.T) { + t.Parallel() + + var evicted []string + cache := lru.New[string, testValue](10, weightFn, func(key string, value testValue) { + evicted = append(evicted, key) + }) + + cache.Add("a", testValue{weight: 2, label: "a"}) + cache.Add("b", testValue{weight: 3, label: "b"}) + cache.Add("c", testValue{weight: 4, label: "c"}) + + removed, ok := cache.Remove("b") + if !ok || removed.label != "b" { + t.Fatalf("Remove(b) = (%+v,%v), want b,true", removed, ok) + } + if cache.Len() != 2 || cache.Weight() != 6 { + t.Fatalf("post-remove Len/Weight = %d/%d, want 2/6", cache.Len(), cache.Weight()) + } + + cache.Clear() + if cache.Len() != 0 || cache.Weight() != 0 { + t.Fatalf("post-clear Len/Weight = %d/%d, want 0/0", cache.Len(), cache.Weight()) + } + + // Remove emits b, then Clear emits oldest-to-newest among remaining: a, c. + if !slices.Equal(evicted, []string{"b", "a", "c"}) { + t.Fatalf("evicted = %v, want [b a c]", evicted) + } +} + +func TestCachePanicsForInvalidConfiguration(t *testing.T) { + t.Parallel() + + t.Run("negative max", func(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatalf("expected panic") + } + }() + _ = lru.New[string, testValue](-1, weightFn, nil) + }) + + t.Run("nil weight function", func(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatalf("expected panic") + } + }() + _ = lru.New[string, testValue](1, nil, nil) + }) + + t.Run("negative entry weight", func(t *testing.T) { + cache := lru.New[string, testValue](10, func(_ string, _ testValue) int64 { + return -1 + }, nil) + defer func() { + if recover() == nil { + t.Fatalf("expected panic") + } + }() + cache.Add("x", testValue{weight: 1, label: "x"}) + }) + + t.Run("set negative max", func(t *testing.T) { + cache := lru.New[string, testValue](10, weightFn, nil) + defer func() { + if recover() == nil { + t.Fatalf("expected panic") + } + }() + cache.SetMaxWeight(-1) + }) +} |
