Files
x/cache/tiered_test.go
Colin Henry 54aae5f242
All checks were successful
Go / build (1.23) (push) Successful in 3m51s
big updates: tests, bug fixed, documentation. oh my
2026-01-03 15:53:50 -08:00

377 lines
8.8 KiB
Go

package cache
import (
"sync"
"testing"
"time"
)
// mockCache is a simple in-memory cache implementation for testing
type mockCache[K comparable, V any] struct {
mu sync.RWMutex
data map[K]V
calls struct {
gets int
puts int
}
}
func newMockCache[K comparable, V any]() *mockCache[K, V] {
return &mockCache[K, V]{
data: make(map[K]V),
}
}
func (m *mockCache[K, V]) Get(key K) V {
m.mu.RLock()
defer m.mu.RUnlock()
m.calls.gets++
return m.data[key]
}
func (m *mockCache[K, V]) Put(key K, value V) {
m.mu.Lock()
defer m.mu.Unlock()
m.calls.puts++
m.data[key] = value
}
func (m *mockCache[K, V]) getCallCounts() (gets, puts int) {
m.mu.RLock()
defer m.mu.RUnlock()
return m.calls.gets, m.calls.puts
}
func (m *mockCache[K, V]) has(key K) bool {
m.mu.RLock()
defer m.mu.RUnlock()
_, exists := m.data[key]
return exists
}
func TestNewTieredCache(t *testing.T) {
t.Run("successful creation", func(t *testing.T) {
inner := newMockCache[string, int]()
outer := newMockCache[string, int]()
cache := NewTieredCache[string, int](inner, outer)
if cache == nil {
t.Fatal("expected non-nil cache")
}
// Verify it's the right type
if _, ok := cache.(*tieredCache[string, int]); !ok {
t.Error("expected cache to be *tieredCache")
}
})
t.Run("panic on nil inner cache", func(t *testing.T) {
outer := newMockCache[string, int]()
defer func() {
if r := recover(); r == nil {
t.Fatal("expected panic for nil inner cache")
}
}()
NewTieredCache[string, int](nil, outer)
})
t.Run("panic on nil outer cache", func(t *testing.T) {
inner := newMockCache[string, int]()
defer func() {
if r := recover(); r == nil {
t.Fatal("expected panic for nil outer cache")
}
}()
NewTieredCache[string, int](inner, nil)
})
t.Run("panic on both nil caches", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatal("expected panic for nil caches")
}
}()
NewTieredCache[string, int](nil, nil)
})
}
func TestTieredCacheGet(t *testing.T) {
t.Run("get from inner cache", func(t *testing.T) {
inner := newMockCache[string, int]()
outer := newMockCache[string, int]()
cache := NewTieredCache[string, int](inner, outer)
// Put directly in inner cache
inner.Put("key1", 42)
value := cache.Get("key1")
if value != 42 {
t.Errorf("expected value 42, got %d", value)
}
// Verify only inner was accessed
innerGets, _ := inner.getCallCounts()
outerGets, _ := outer.getCallCounts()
if innerGets != 1 {
t.Errorf("expected 1 inner get, got %d", innerGets)
}
if outerGets != 0 {
t.Errorf("expected 0 outer gets, got %d", outerGets)
}
})
t.Run("get from outer cache when not in inner", func(t *testing.T) {
inner := newMockCache[string, int]()
outer := newMockCache[string, int]()
cache := NewTieredCache[string, int](inner, outer)
// Put only in outer cache
outer.Put("key2", 99)
value := cache.Get("key2")
if value != 99 {
t.Errorf("expected value 99, got %d", value)
}
// Verify both were accessed
innerGets, _ := inner.getCallCounts()
outerGets, _ := outer.getCallCounts()
if innerGets != 1 {
t.Errorf("expected 1 inner get, got %d", innerGets)
}
if outerGets != 1 {
t.Errorf("expected 1 outer get, got %d", outerGets)
}
})
t.Run("get missing key returns zero value", func(t *testing.T) {
inner := newMockCache[string, int]()
outer := newMockCache[string, int]()
cache := NewTieredCache[string, int](inner, outer)
value := cache.Get("nonexistent")
if value != 0 {
t.Errorf("expected zero value 0, got %d", value)
}
// Verify both were accessed
innerGets, _ := inner.getCallCounts()
outerGets, _ := outer.getCallCounts()
if innerGets != 1 {
t.Errorf("expected 1 inner get, got %d", innerGets)
}
if outerGets != 1 {
t.Errorf("expected 1 outer get, got %d", outerGets)
}
})
t.Run("get with string values", func(t *testing.T) {
inner := newMockCache[int, string]()
outer := newMockCache[int, string]()
cache := NewTieredCache[int, string](inner, outer)
inner.Put(1, "hello")
outer.Put(2, "world")
if cache.Get(1) != "hello" {
t.Error("expected 'hello' from inner cache")
}
if cache.Get(2) != "world" {
t.Error("expected 'world' from outer cache")
}
if cache.Get(3) != "" {
t.Error("expected empty string for missing key")
}
})
}
func TestTieredCachePut(t *testing.T) {
t.Run("put to both caches", func(t *testing.T) {
inner := newMockCache[string, int]()
outer := newMockCache[string, int]()
cache := NewTieredCache[string, int](inner, outer)
cache.Put("key1", 42)
// Inner should have it immediately
if !inner.has("key1") {
t.Error("expected key1 to be in inner cache")
}
// Outer is written asynchronously, so wait a bit
time.Sleep(50 * time.Millisecond)
if !outer.has("key1") {
t.Error("expected key1 to be in outer cache")
}
// Verify both caches have the same value
if inner.Get("key1") != 42 {
t.Error("expected inner cache to have value 42")
}
if outer.Get("key1") != 42 {
t.Error("expected outer cache to have value 42")
}
})
t.Run("put multiple values", func(t *testing.T) {
inner := newMockCache[string, string]()
outer := newMockCache[string, string]()
cache := NewTieredCache[string, string](inner, outer)
cache.Put("a", "alpha")
cache.Put("b", "beta")
cache.Put("c", "gamma")
// Wait for async writes
time.Sleep(50 * time.Millisecond)
// Verify all values in both caches
for _, key := range []string{"a", "b", "c"} {
if !inner.has(key) {
t.Errorf("expected key %s to be in inner cache", key)
}
if !outer.has(key) {
t.Errorf("expected key %s to be in outer cache", key)
}
}
})
t.Run("put overwrites existing values", func(t *testing.T) {
inner := newMockCache[string, int]()
outer := newMockCache[string, int]()
cache := NewTieredCache[string, int](inner, outer)
cache.Put("key", 1)
time.Sleep(50 * time.Millisecond)
cache.Put("key", 2)
time.Sleep(50 * time.Millisecond)
if inner.Get("key") != 2 {
t.Errorf("expected inner cache to have value 2, got %d", inner.Get("key"))
}
if outer.Get("key") != 2 {
t.Errorf("expected outer cache to have value 2, got %d", outer.Get("key"))
}
})
t.Run("concurrent puts", func(t *testing.T) {
inner := newMockCache[int, int]()
outer := newMockCache[int, int]()
cache := NewTieredCache[int, int](inner, outer)
var wg sync.WaitGroup
n := 100
// Launch concurrent Put operations
for i := 0; i < n; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
cache.Put(val, val*2)
}(i)
}
wg.Wait()
time.Sleep(100 * time.Millisecond)
// Verify all values are present
_, innerPuts := inner.getCallCounts()
_, outerPuts := outer.getCallCounts()
if innerPuts != n {
t.Errorf("expected %d inner puts, got %d", n, innerPuts)
}
if outerPuts != n {
t.Errorf("expected %d outer puts, got %d", n, outerPuts)
}
})
}
func TestTieredCacheIntegration(t *testing.T) {
t.Run("full workflow", func(t *testing.T) {
inner := newMockCache[string, int]()
outer := newMockCache[string, int]()
cache := NewTieredCache[string, int](inner, outer)
// Put some values
cache.Put("a", 1)
cache.Put("b", 2)
cache.Put("c", 3)
// Wait for async writes to outer
time.Sleep(100 * time.Millisecond)
// Get values (should hit inner cache)
if cache.Get("a") != 1 {
t.Error("expected a=1")
}
if cache.Get("b") != 2 {
t.Error("expected b=2")
}
if cache.Get("c") != 3 {
t.Error("expected c=3")
}
// Simulate inner cache eviction by clearing it
inner.data = make(map[string]int)
// Get values again (should hit outer cache)
if cache.Get("a") != 1 {
t.Error("expected a=1 from outer cache")
}
if cache.Get("b") != 2 {
t.Error("expected b=2 from outer cache")
}
if cache.Get("c") != 3 {
t.Error("expected c=3 from outer cache")
}
})
}
func TestTieredCacheWithStructs(t *testing.T) {
type User struct {
ID int
Name string
}
t.Run("cache structs", func(t *testing.T) {
inner := newMockCache[int, User]()
outer := newMockCache[int, User]()
cache := NewTieredCache[int, User](inner, outer)
user := User{ID: 1, Name: "Alice"}
cache.Put(1, user)
retrieved := cache.Get(1)
if retrieved.ID != 1 || retrieved.Name != "Alice" {
t.Errorf("expected user {1, Alice}, got {%d, %s}", retrieved.ID, retrieved.Name)
}
})
t.Run("cache pointers", func(t *testing.T) {
inner := newMockCache[int, *User]()
outer := newMockCache[int, *User]()
cache := NewTieredCache[int, *User](inner, outer)
user := &User{ID: 2, Name: "Bob"}
cache.Put(2, user)
time.Sleep(50 * time.Millisecond)
retrieved := cache.Get(2)
if retrieved == nil {
t.Fatal("expected non-nil user")
}
if retrieved.ID != 2 || retrieved.Name != "Bob" {
t.Errorf("expected user {2, Bob}, got {%d, %s}", retrieved.ID, retrieved.Name)
}
})
}