Files
traefikoidc/cache_test.go
T

782 lines
21 KiB
Go

package traefikoidc
import (
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestCacheBasicOperations tests the fundamental cache operations using table-driven tests
func TestCacheBasicOperations(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeQuick) {
return
}
runner := NewTestSuiteRunner()
runner.SetTimeout(10 * time.Second)
testCases := []TableTestCase{
{
Name: "Set and Get - Valid Key",
Description: "Basic set and get operation with valid data",
Setup: func(t *testing.T) error {
return nil
},
Teardown: func(t *testing.T) error {
return nil
},
},
{
Name: "Get Non-existent Key",
Description: "Getting a key that doesn't exist should return false",
},
{
Name: "Set and Get - Zero Value",
Description: "Test caching zero values like empty strings, nil, zero numbers",
},
{
Name: "Delete Existing Key",
Description: "Delete should remove the key and make it unavailable",
},
{
Name: "Delete Non-existent Key",
Description: "Deleting a non-existent key should not cause errors",
},
}
// Execute basic operation tests with custom logic
for _, tc := range testCases {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
cache := NewCache()
defer cache.Close()
switch tc.Name {
case "Set and Get - Valid Key":
testValue := "test-value-123"
cache.Set("test-key", testValue, time.Hour)
result, found := cache.Get("test-key")
assert.True(t, found, "Expected key to be found")
assert.Equal(t, testValue, result, "Expected values to match")
case "Get Non-existent Key":
result, found := cache.Get("non-existent")
assert.False(t, found, "Expected key not to be found")
assert.Nil(t, result, "Expected nil result for non-existent key")
case "Set and Get - Zero Value":
// Test various zero values
cache.Set("empty-string", "", time.Hour)
cache.Set("zero-int", 0, time.Hour)
cache.Set("nil-value", nil, time.Hour)
cache.Set("false-value", false, time.Hour)
val, found := cache.Get("empty-string")
assert.True(t, found)
assert.Equal(t, "", val)
val, found = cache.Get("zero-int")
assert.True(t, found)
assert.Equal(t, 0, val)
val, found = cache.Get("nil-value")
assert.True(t, found)
assert.Nil(t, val)
val, found = cache.Get("false-value")
assert.True(t, found)
assert.Equal(t, false, val)
case "Delete Existing Key":
cache.Set("delete-me", "value", time.Hour)
_, found := cache.Get("delete-me")
assert.True(t, found, "Key should exist before deletion")
cache.Delete("delete-me")
_, found = cache.Get("delete-me")
assert.False(t, found, "Key should not exist after deletion")
case "Delete Non-existent Key":
// Should not panic or cause errors
cache.Delete("does-not-exist")
// If we reach here, the test passes
assert.True(t, true)
}
})
}
}
// TestCacheExpiration tests TTL functionality with edge cases
func TestCacheExpiration(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeQuick) {
return
}
edgeGen := NewEdgeCaseGenerator()
expirationTimes := []time.Duration{
10 * time.Millisecond, // Very short but still measurable
50 * time.Millisecond, // Short
100 * time.Millisecond, // Medium short
500 * time.Millisecond, // Medium
1 * time.Second, // Long
time.Hour, // Very long
}
t.Run("Expiration Times", func(t *testing.T) {
for _, expiration := range expirationTimes {
expiration := expiration
t.Run(fmt.Sprintf("Expiration_%s", expiration), func(t *testing.T) {
cache := NewCache()
defer cache.Close()
key := fmt.Sprintf("test-key-%s", expiration)
value := fmt.Sprintf("test-value-%s", expiration)
cache.Set(key, value, expiration)
// Immediate retrieval should work
result, found := cache.Get(key)
assert.True(t, found, "Key should be found immediately after set")
assert.Equal(t, value, result)
// Wait for expiration
if expiration < time.Second {
time.Sleep(expiration + 10*time.Millisecond)
} else {
// For long expiration times, don't actually wait
t.Skip("Skipping long expiration test to save time")
}
// Should be expired now
result, found = cache.Get(key)
assert.False(t, found, "Key should not be found after expiration")
assert.Nil(t, result)
})
}
})
t.Run("Expiration Edge Cases", func(t *testing.T) {
cache := NewCache()
defer cache.Close()
// Test with edge case strings
edgeCases := edgeGen.GenerateStringEdgeCases()
for i, testStr := range edgeCases {
if i > 5 { // Limit to first 5 to avoid too many tests
break
}
key := fmt.Sprintf("edge-key-%d", i)
cache.Set(key, testStr, 10*time.Millisecond)
}
// Wait for expiration
time.Sleep(50 * time.Millisecond)
// All should be expired
for i := 0; i < 6; i++ {
key := fmt.Sprintf("edge-key-%d", i)
_, found := cache.Get(key)
assert.False(t, found, "Edge case key %s should be expired", key)
}
})
}
// TestCacheSize tests size management and eviction policies
func TestCacheSize(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeQuick) {
return
}
t.Run("SetMaxSize Eviction", func(t *testing.T) {
cache := NewCache()
defer cache.Close()
originalMaxSize := 10
newMaxSize := 3
// Add more items than the new max size
for i := 0; i < originalMaxSize; i++ {
key := fmt.Sprintf("key%c", rune('A'+i))
cache.Set(key, i, time.Hour)
}
// Verify items were added
for i := 0; i < originalMaxSize; i++ {
key := fmt.Sprintf("key%c", rune('A'+i))
_, exists := cache.Get(key)
require.True(t, exists, "Expected key %s to exist before SetMaxSize", key)
}
// Change the max size to a smaller value
cache.SetMaxSize(newMaxSize)
// Count remaining items
count := 0
for i := 0; i < originalMaxSize; i++ {
key := fmt.Sprintf("key%c", rune('A'+i))
if _, exists := cache.Get(key); exists {
count++
}
}
// Check that the cache was reduced to the new max size
assert.LessOrEqual(t, count, newMaxSize, "Cache size %d exceeds new max size %d after SetMaxSize", count, newMaxSize)
// Check that the oldest items were evicted (keyA should be evicted)
_, exists := cache.Get("keyA")
assert.False(t, exists, "Expected oldest item 'keyA' to be evicted, but it still exists")
})
t.Run("Eviction With Mixed Expiration Times", func(t *testing.T) {
cache := NewCache()
cache.SetMaxSize(5)
defer cache.Close()
// Add items with different expiration times
cache.Set("expired1", "val1", -time.Hour) // Already expired
cache.Set("valid1", "val1", time.Hour) // Valid
cache.Set("expired2", "val2", -time.Hour) // Already expired
cache.Set("valid2", "val2", time.Hour) // Valid
cache.Set("valid3", "val3", time.Hour) // Valid
// Add one more to trigger eviction (should preferably evict expired items)
cache.Set("valid4", "val4", time.Hour)
// Count valid items (should be 4 valid ones)
validCount := 0
expiredCount := 0
keys := []string{"expired1", "valid1", "expired2", "valid2", "valid3", "valid4"}
for _, key := range keys {
if _, found := cache.Get(key); found {
if len(key) >= 7 && key[:7] == "expired" {
expiredCount++
} else {
validCount++
}
}
}
assert.LessOrEqual(t, validCount+expiredCount, 5, "Total items should not exceed max size")
assert.LessOrEqual(t, expiredCount, 2, "Should prioritize evicting expired items")
})
}
// TestCacheConcurrency tests concurrent operations and thread safety
func TestCacheConcurrency(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeConcurrencyStress) {
return
}
t.Run("Concurrent Set and Get", func(t *testing.T) {
cache := NewCache()
defer cache.Close()
const numGoroutines = 100
const operationsPerGoroutine = 50
var wg sync.WaitGroup
// Statistics tracking
var setOperations int64
var getOperations int64
var successfulGets int64
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
key := fmt.Sprintf("key-%d-%d", goroutineID, j)
value := fmt.Sprintf("value-%d-%d", goroutineID, j)
// Set operation
cache.Set(key, value, time.Minute)
atomic.AddInt64(&setOperations, 1)
// Get operation
if result, found := cache.Get(key); found {
atomic.AddInt64(&successfulGets, 1)
if result != value {
t.Errorf("Got unexpected value: expected %s, got %s", value, result)
}
}
atomic.AddInt64(&getOperations, 1)
}
}(i)
}
wg.Wait()
totalExpectedOps := int64(numGoroutines * operationsPerGoroutine)
assert.Equal(t, totalExpectedOps, atomic.LoadInt64(&setOperations), "All set operations should complete")
assert.Equal(t, totalExpectedOps, atomic.LoadInt64(&getOperations), "All get operations should complete")
// Most gets should succeed (allowing for some eviction)
successRate := float64(atomic.LoadInt64(&successfulGets)) / float64(atomic.LoadInt64(&getOperations))
assert.Greater(t, successRate, 0.7, "At least 70%% of gets should succeed")
})
t.Run("Concurrent Set and Delete", func(t *testing.T) {
cache := NewCache()
defer cache.Close()
const numItems = 1000
var wg sync.WaitGroup
// Add items concurrently
wg.Add(numItems)
for i := 0; i < numItems; i++ {
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("concurrent-key-%d", id)
cache.Set(key, fmt.Sprintf("value-%d", id), time.Minute)
}(i)
}
wg.Wait()
// Delete items concurrently
wg.Add(numItems)
for i := 0; i < numItems; i++ {
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("concurrent-key-%d", id)
cache.Delete(key)
}(i)
}
wg.Wait()
// Verify all items are deleted
for i := 0; i < numItems; i++ {
key := fmt.Sprintf("concurrent-key-%d", i)
_, found := cache.Get(key)
assert.False(t, found, "Key %s should be deleted", key)
}
})
}
// TestCacheUnifiedMemoryLeaks tests for memory leaks using unified test infrastructure
func TestCacheUnifiedMemoryLeaks(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeLeakDetection) {
return
}
runner := NewTestSuiteRunner()
memoryLeakTests := []MemoryLeakTestCase{
{
Name: "Cache Creation and Destruction",
Description: "Creating and destroying caches should not leak memory",
Iterations: 50,
MaxGoroutineGrowth: 2, // Allow some tolerance for test framework
MaxMemoryGrowthMB: 5.0,
GCBetweenRuns: true,
Operation: func() error {
cache := NewCache()
cache.Set("test", "value", time.Minute)
_, _ = cache.Get("test")
cache.Close()
return nil
},
},
{
Name: "Heavy Cache Usage",
Description: "Heavy cache usage should not accumulate memory",
Iterations: 10,
MaxGoroutineGrowth: 2,
MaxMemoryGrowthMB: 10.0,
GCBetweenRuns: true,
Operation: func() error {
cache := NewCache()
defer cache.Close()
// Add many items
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("key-%d", i)
cache.Set(key, fmt.Sprintf("value-%d", i), time.Second)
}
// Access items randomly
for i := 0; i < 500; i++ {
key := fmt.Sprintf("key-%d", i%1000)
cache.Get(key)
}
// Delete some items
for i := 0; i < 250; i++ {
key := fmt.Sprintf("key-%d", i*2)
cache.Delete(key)
}
return nil
},
},
{
Name: "Cleanup Functionality",
Description: "Manual cleanup should free memory properly",
Iterations: 20,
MaxGoroutineGrowth: 1,
MaxMemoryGrowthMB: 3.0,
GCBetweenRuns: true,
Operation: func() error {
cache := NewCache()
defer cache.Close()
// Add items that will expire
for i := 0; i < 100; i++ {
key := fmt.Sprintf("key-%d", i)
cache.Set(key, fmt.Sprintf("value-%d", i), -time.Hour) // Already expired
}
// Run cleanup
cache.Cleanup()
return nil
},
},
}
runner.RunMemoryLeakTests(t, memoryLeakTests)
}
// TestCachePerformance includes benchmarks and performance validation
func TestCachePerformance(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeExtended) {
return
}
t.Run("Basic Operations Performance", func(t *testing.T) {
cache := NewCache()
defer cache.Close()
perfHelper := NewPerformanceTestHelper()
// Test Set performance
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("perf-key-%d", i)
value := fmt.Sprintf("perf-value-%d", i)
perfHelper.Measure(func() {
cache.Set(key, value, time.Hour)
})
}
avgSetTime := perfHelper.GetAverageTime()
t.Logf("Average Set time: %v", avgSetTime)
assert.Less(t, avgSetTime.Nanoseconds(), int64(10*time.Microsecond), "Set operations should be fast")
// Reset and test Get performance
perfHelper.Reset()
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("perf-key-%d", i)
perfHelper.Measure(func() {
cache.Get(key)
})
}
avgGetTime := perfHelper.GetAverageTime()
t.Logf("Average Get time: %v", avgGetTime)
assert.Less(t, avgGetTime.Nanoseconds(), int64(5*time.Microsecond), "Get operations should be very fast")
})
}
// TestCacheEdgeCases tests various edge cases and error conditions
func TestCacheEdgeCases(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeExtended) {
return
}
edgeGen := NewEdgeCaseGenerator()
t.Run("Edge Case Keys and Values", func(t *testing.T) {
cache := NewCache()
defer cache.Close()
keys := edgeGen.GenerateStringEdgeCases()
values := edgeGen.GenerateStringEdgeCases()
// Test combinations of edge case keys and values
for i, key := range keys[:5] { // Limit to first 5 for performance
for j, value := range values[:3] { // Limit to first 3 for performance
testName := fmt.Sprintf("Key_%d_Value_%d", i, j)
t.Run(testName, func(t *testing.T) {
cache.Set(key, value, time.Minute)
result, found := cache.Get(key)
assert.True(t, found, "Should find edge case key")
assert.Equal(t, value, result, "Should get correct edge case value")
// Clean up
cache.Delete(key)
_, found = cache.Get(key)
assert.False(t, found, "Should not find deleted edge case key")
})
}
}
})
t.Run("Boundary Conditions", func(t *testing.T) {
cache := NewCache()
defer cache.Close()
// Test with zero and negative TTL
cache.Set("zero-ttl", "value", 0)
cache.Set("negative-ttl", "value", -time.Hour)
// Should be immediately expired
_, found := cache.Get("zero-ttl")
assert.False(t, found, "Zero TTL should expire immediately")
_, found = cache.Get("negative-ttl")
assert.False(t, found, "Negative TTL should expire immediately")
// Test max size boundaries
cache.SetMaxSize(1)
cache.Set("first", "value1", time.Hour)
cache.Set("second", "value2", time.Hour)
// Should only have one item
firstFound := false
secondFound := false
if _, found := cache.Get("first"); found {
firstFound = true
}
if _, found := cache.Get("second"); found {
secondFound = true
}
assert.True(t, firstFound != secondFound, "Should have exactly one item when max size is 1")
})
t.Run("Invalid Operations", func(t *testing.T) {
cache := NewCache()
defer cache.Close()
// Test invalid max size
cache.SetMaxSize(0) // Should be ignored
cache.SetMaxSize(-10) // Should be ignored
// Cache should still work
cache.Set("test", "value", time.Hour)
result, found := cache.Get("test")
assert.True(t, found)
assert.Equal(t, "value", result)
})
}
// TestCacheLRUBehavior specifically tests the LRU (Least Recently Used) eviction policy
func TestCacheLRUBehavior(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeExtended) {
return
}
t.Run("LRU Eviction Order", func(t *testing.T) {
cache := NewCache()
cache.SetMaxSize(3)
defer cache.Close()
// Add items in order
cache.Set("first", "1", time.Hour)
cache.Set("second", "2", time.Hour)
cache.Set("third", "3", time.Hour)
// Access first item to make it recently used
cache.Get("first")
// Add fourth item, should evict "second" (least recently used)
cache.Set("fourth", "4", time.Hour)
// Verify eviction order
_, found := cache.Get("first")
assert.True(t, found, "First should still exist (recently accessed)")
_, found = cache.Get("second")
assert.False(t, found, "Second should be evicted (least recently used)")
_, found = cache.Get("third")
assert.True(t, found, "Third should still exist")
_, found = cache.Get("fourth")
assert.True(t, found, "Fourth should exist (just added)")
})
t.Run("LRU Update on Set", func(t *testing.T) {
cache := NewCache()
cache.SetMaxSize(3)
defer cache.Close()
// Add items
cache.Set("a", "1", time.Hour)
cache.Set("b", "2", time.Hour)
cache.Set("c", "3", time.Hour)
// Update existing item (should move to end of LRU)
cache.Set("a", "updated", time.Hour)
// Add new item, should evict "b" (now least recently used)
cache.Set("d", "4", time.Hour)
_, found := cache.Get("a")
assert.True(t, found, "A should still exist (recently updated)")
_, found = cache.Get("b")
assert.False(t, found, "B should be evicted")
_, found = cache.Get("c")
assert.True(t, found, "C should still exist")
_, found = cache.Get("d")
assert.True(t, found, "D should exist")
})
}
// BenchmarkCache provides performance benchmarks
func BenchmarkCache(b *testing.B) {
cache := NewCache()
defer cache.Close()
b.Run("Set", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("benchmark-key-%d", i)
cache.Set(key, fmt.Sprintf("value-%d", i), time.Hour)
}
})
b.Run("Get", func(b *testing.B) {
// Pre-populate cache
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("benchmark-key-%d", i)
cache.Set(key, fmt.Sprintf("value-%d", i), time.Hour)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("benchmark-key-%d", i%1000)
cache.Get(key)
}
})
b.Run("Delete", func(b *testing.B) {
// Pre-populate cache
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("benchmark-delete-key-%d", i)
cache.Set(key, fmt.Sprintf("value-%d", i), time.Hour)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("benchmark-delete-key-%d", i)
cache.Delete(key)
}
})
b.Run("Concurrent", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
key := fmt.Sprintf("concurrent-key-%d", i)
value := fmt.Sprintf("concurrent-value-%d", i)
cache.Set(key, value, time.Minute)
cache.Get(key)
if i%10 == 0 {
cache.Delete(key)
}
i++
}
})
})
}
// TestCache_Cleanup tests the specific cleanup functionality (converted from original test)
func TestCache_Cleanup(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeQuick) {
return
}
c := NewCache()
defer c.Close()
// Add some items with different expiration times
now := time.Now()
pastTime := now.Add(-1 * time.Hour) // Already expired
futureTime := now.Add(1 * time.Hour) // Not expired
c.Set("expired", "expired-value", pastTime.Sub(now))
c.Set("valid", "valid-value", futureTime.Sub(now))
// Call cleanup, which should only remove expired items
c.Cleanup()
// Check that only the expired item was removed
if _, exists := c.Get("expired"); exists {
t.Error("Expired item was not removed by Cleanup()")
}
if _, exists := c.Get("valid"); !exists {
t.Error("Valid item was incorrectly removed by Cleanup()")
}
}
// TestCache_SetMaxSize tests the specific max size functionality (converted from original test)
func TestCache_SetMaxSize(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeQuick) {
return
}
c := NewCache()
defer c.Close()
// Set a lower max size
originalMaxSize := 10
newMaxSize := 3
// Add more items than the new max size
for i := 0; i < originalMaxSize; i++ {
key := "key" + string(rune('A'+i))
c.Set(key, i, 1*time.Hour)
}
// Verify items were added by checking a few keys
for i := 0; i < originalMaxSize; i++ {
key := "key" + string(rune('A'+i))
if _, exists := c.Get(key); !exists {
t.Errorf("Expected key %s to exist before SetMaxSize", key)
}
}
// Change the max size to a smaller value
c.SetMaxSize(newMaxSize)
// Count remaining items
count := 0
for i := 0; i < originalMaxSize; i++ {
key := "key" + string(rune('A'+i))
if _, exists := c.Get(key); exists {
count++
}
}
// Check that the cache was reduced to the new max size
if count > newMaxSize {
t.Errorf("Cache size %d exceeds new max size %d after SetMaxSize", count, newMaxSize)
}
// Check that the oldest items were evicted (keyA should be evicted)
if _, exists := c.Get("keyA"); exists {
t.Error("Expected oldest item 'keyA' to be evicted, but it still exists")
}
}