aboutsummaryrefslogtreecommitdiff
path: root/internal/cache/lru/lru_test.go
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-02-21 10:30:53 +0800
committerGravatar Runxi Yu2026-02-21 11:00:52 +0800
commit19dc6aedddde8b5306f1fb0dc4d46ba57f318cce (patch)
tree06ed3241ff837818005e46c8f0da7a370d6e482d /internal/cache/lru/lru_test.go
parentobjectid: Add RawBytes (diff)
signatureNo signature
cache/lru: Add basic LRU
Diffstat (limited to 'internal/cache/lru/lru_test.go')
-rw-r--r--internal/cache/lru/lru_test.go210
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)
+ })
+}