shithub: furgit

ref: df1f2fb3daa1acd25c88510f259d5535fb482126
dir: /internal/lru/lru_test.go/

View raw version
package lru_test

import (
	"slices"
	"testing"

	"codeberg.org/lindenii/furgit/internal/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) {
		t.Parallel()

		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) {
		t.Parallel()

		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) {
		t.Parallel()

		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) {
		t.Parallel()

		cache := lru.New[string, testValue](10, weightFn, nil)

		defer func() {
			if recover() == nil {
				t.Fatalf("expected panic")
			}
		}()

		cache.SetMaxWeight(-1)
	})
}