377 lines
8.8 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|