mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
256 lines
5.0 KiB
Go
256 lines
5.0 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestBoundedCache(t *testing.T) {
|
|
t.Run("basic operations", func(t *testing.T) {
|
|
cache := NewBoundedCache(10)
|
|
defer cache.Close()
|
|
|
|
// Set and get
|
|
cache.Set("key1", "value1", time.Minute)
|
|
val, found := cache.Get("key1")
|
|
|
|
if !found {
|
|
t.Error("Expected to find key1")
|
|
}
|
|
if val != "value1" {
|
|
t.Errorf("Expected value1, got %v", val)
|
|
}
|
|
|
|
// Delete
|
|
cache.Delete("key1")
|
|
_, found = cache.Get("key1")
|
|
if found {
|
|
t.Error("Expected key1 to be deleted")
|
|
}
|
|
})
|
|
|
|
t.Run("size limit enforcement", func(t *testing.T) {
|
|
cache := NewBoundedCache(5)
|
|
defer cache.Close()
|
|
|
|
// Add items up to the limit
|
|
for i := 0; i < 10; i++ {
|
|
key := string(rune('a' + i))
|
|
cache.Set(key, i, time.Minute)
|
|
}
|
|
|
|
// Cache should only have 5 items
|
|
if cache.Size() > 5 {
|
|
t.Errorf("Cache size %d exceeds limit of 5", cache.Size())
|
|
}
|
|
|
|
// Oldest items should be evicted
|
|
_, found := cache.Get("a")
|
|
if found {
|
|
t.Error("Expected oldest item 'a' to be evicted")
|
|
}
|
|
|
|
// Newest items should still be present
|
|
_, found = cache.Get("j")
|
|
if !found {
|
|
t.Error("Expected newest item 'j' to be present")
|
|
}
|
|
})
|
|
|
|
t.Run("LRU eviction", func(t *testing.T) {
|
|
cache := NewBoundedCache(3)
|
|
defer cache.Close()
|
|
|
|
// Add 3 items
|
|
cache.Set("a", 1, time.Minute)
|
|
cache.Set("b", 2, time.Minute)
|
|
cache.Set("c", 3, time.Minute)
|
|
|
|
// Access 'a' to make it recently used
|
|
cache.Get("a")
|
|
|
|
// Add new item, should evict 'b' (least recently used)
|
|
cache.Set("d", 4, time.Minute)
|
|
|
|
_, foundA := cache.Get("a")
|
|
_, foundB := cache.Get("b")
|
|
_, foundC := cache.Get("c")
|
|
_, foundD := cache.Get("d")
|
|
|
|
if !foundA {
|
|
t.Error("Expected 'a' to be present (recently accessed)")
|
|
}
|
|
if foundB {
|
|
t.Error("Expected 'b' to be evicted (LRU)")
|
|
}
|
|
if !foundC {
|
|
t.Error("Expected 'c' to be present")
|
|
}
|
|
if !foundD {
|
|
t.Error("Expected 'd' to be present (newest)")
|
|
}
|
|
})
|
|
|
|
t.Run("expiration", func(t *testing.T) {
|
|
cache := NewBoundedCache(10)
|
|
defer cache.Close()
|
|
|
|
// Set with short TTL
|
|
cache.Set("expire", "value", 100*time.Millisecond)
|
|
|
|
// Should exist initially
|
|
_, found := cache.Get("expire")
|
|
if !found {
|
|
t.Error("Expected to find key before expiration")
|
|
}
|
|
|
|
// Wait for expiration
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
// Should be expired
|
|
_, found = cache.Get("expire")
|
|
if found {
|
|
t.Error("Expected key to be expired")
|
|
}
|
|
})
|
|
|
|
t.Run("concurrent access", func(t *testing.T) {
|
|
cache := NewBoundedCache(100)
|
|
defer cache.Close()
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Concurrent writes
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
key := string(rune('a' + (id % 26)))
|
|
cache.Set(key, id, time.Minute)
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent reads
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
key := string(rune('a' + (id % 26)))
|
|
cache.Get(key)
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify cache is still functional
|
|
cache.Set("test", "value", time.Minute)
|
|
val, found := cache.Get("test")
|
|
if !found || val != "value" {
|
|
t.Error("Cache not functional after concurrent access")
|
|
}
|
|
})
|
|
|
|
t.Run("clear operation", func(t *testing.T) {
|
|
cache := NewBoundedCache(10)
|
|
defer cache.Close()
|
|
|
|
// Add items
|
|
for i := 0; i < 5; i++ {
|
|
cache.Set(string(rune('a'+i)), i, time.Minute)
|
|
}
|
|
|
|
// Clear cache
|
|
cache.Clear()
|
|
|
|
if cache.Size() != 0 {
|
|
t.Errorf("Expected size 0 after clear, got %d", cache.Size())
|
|
}
|
|
|
|
// Verify items are gone
|
|
_, found := cache.Get("a")
|
|
if found {
|
|
t.Error("Expected cache to be empty after clear")
|
|
}
|
|
})
|
|
|
|
t.Run("stats", func(t *testing.T) {
|
|
cache := NewBoundedCache(10)
|
|
defer cache.Close()
|
|
|
|
// Add some items
|
|
for i := 0; i < 5; i++ {
|
|
cache.Set(string(rune('a'+i)), i, time.Minute)
|
|
}
|
|
|
|
stats := cache.GetStats()
|
|
|
|
if stats["max_size"] != 10 {
|
|
t.Errorf("Expected max_size 10, got %v", stats["max_size"])
|
|
}
|
|
|
|
if size, ok := stats["size"].(int64); !ok || size != 5 {
|
|
t.Errorf("Expected size 5, got %v (type: %T)", stats["size"], stats["size"])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBoundedCacheRaceConditions(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping race condition test in short mode")
|
|
}
|
|
|
|
cache := NewBoundedCache(50)
|
|
defer cache.Close()
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Concurrent operations
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
key := string(rune('a' + (id % 26)))
|
|
|
|
// Mix of operations
|
|
cache.Set(key, id, time.Second)
|
|
cache.Get(key)
|
|
if id%10 == 0 {
|
|
cache.Delete(key)
|
|
}
|
|
if id%20 == 0 {
|
|
cache.Clear()
|
|
}
|
|
cache.GetStats()
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func BenchmarkBoundedCache(b *testing.B) {
|
|
cache := NewBoundedCache(1000)
|
|
defer cache.Close()
|
|
|
|
b.Run("Set", func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
key := string(rune(i % 1000))
|
|
cache.Set(key, i, time.Minute)
|
|
}
|
|
})
|
|
|
|
b.Run("Get", func(b *testing.B) {
|
|
// Pre-populate
|
|
for i := 0; i < 1000; i++ {
|
|
cache.Set(string(rune(i)), i, time.Minute)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
key := string(rune(i % 1000))
|
|
cache.Get(key)
|
|
}
|
|
})
|
|
}
|