fixup! fixup! fixup! fixup! Further pursue of perfection.

This commit is contained in:
2025-09-05 16:17:02 +01:00
parent e267cb3a6f
commit b182951f22
18 changed files with 6256 additions and 1113 deletions
+308
View File
@@ -0,0 +1,308 @@
# Test Execution Guide
This guide explains how to run tests efficiently with the new test categorization and optimization system.
## Quick Start
### Fast Development Testing (Default - Target: < 30 seconds)
```bash
# Run quick smoke tests only
go test ./...
# Or explicitly run in short mode
go test ./... -short
```
### Extended Testing (Target: 2-5 minutes)
```bash
# Enable extended tests with more iterations and concurrency
RUN_EXTENDED_TESTS=1 go test ./...
# Or use the flag equivalent (if using test runner that supports it)
go test ./... -extended
```
### Long-Running Performance Tests (Target: 5-15 minutes)
```bash
# Enable comprehensive performance and stress tests
RUN_LONG_TESTS=1 go test ./...
```
### Full Stress Testing (Target: 10-30 minutes)
```bash
# Enable all stress tests with maximum parameters
RUN_STRESS_TESTS=1 go test ./...
```
## Test Categories
### 1. Quick Tests (Default)
- **Purpose**: Fast feedback during development
- **Duration**: < 30 seconds total
- **Features**:
- Basic functionality verification
- Limited iterations (1-3)
- Small data sets
- Minimal concurrency
- Essential memory leak checks
**Configuration**:
- Max Iterations: 3
- Max Concurrency: 5
- Memory Threshold: 2.0 MB
- Cache Size: 50
- Timeout: 10 seconds
### 2. Extended Tests
- **Purpose**: Comprehensive testing before commits
- **Duration**: 2-5 minutes
- **Features**:
- Increased test coverage
- More iterations (5-10)
- Medium concurrency tests
- Enhanced memory leak detection
**Configuration**:
- Max Iterations: 10
- Max Concurrency: 20
- Memory Threshold: 10.0 MB
- Cache Size: 200
- Timeout: 30 seconds
### 3. Long Tests
- **Purpose**: Performance validation and stress testing
- **Duration**: 5-15 minutes
- **Features**:
- High iteration counts (50-100)
- High concurrency scenarios
- Large data sets
- Comprehensive memory testing
**Configuration**:
- Max Iterations: 100
- Max Concurrency: 50
- Memory Threshold: 50.0 MB
- Cache Size: 1000
- Timeout: 60 seconds
### 4. Stress Tests
- **Purpose**: Maximum load testing and edge case validation
- **Duration**: 10-30 minutes
- **Features**:
- Extreme iteration counts (100-500)
- Maximum concurrency (100+)
- Large memory allocations
- Edge case combinations
**Configuration**:
- Max Iterations: 500
- Max Concurrency: 100
- Memory Threshold: 100.0 MB
- Cache Size: 2000
- Timeout: 120 seconds
## Environment Variables
### Test Execution Control
```bash
# Enable specific test types
export RUN_EXTENDED_TESTS=1 # Enable extended tests
export RUN_LONG_TESTS=1 # Enable long-running tests
export RUN_STRESS_TESTS=1 # Enable stress tests
# Disable specific features
export DISABLE_LEAK_DETECTION=1 # Skip memory leak detection
```
### Parameter Customization
```bash
# Customize concurrency limits
export TEST_MAX_CONCURRENCY=10 # Override max concurrent operations
# Customize iteration limits
export TEST_MAX_ITERATIONS=50 # Override max test iterations
# Customize memory thresholds
export TEST_MEMORY_THRESHOLD_MB=25.5 # Override memory growth limit (in MB)
```
## Test-Specific Behavior
### Memory Leak Tests
- **Quick Mode**: 1-3 iterations, small data sets, strict memory limits
- **Extended Mode**: 5-10 iterations, medium data sets, relaxed limits
- **Long Mode**: 50-100 iterations, large data sets, performance focus
- **Stress Mode**: 100-500 iterations, maximum data sets, stress focus
### Concurrency Tests
- **Quick Mode**: 2-5 concurrent operations, basic race detection
- **Extended Mode**: 10-20 concurrent operations, moderate stress
- **Long Mode**: 20-50 concurrent operations, high contention
- **Stress Mode**: 50-100+ concurrent operations, maximum stress
### Cache Tests
- **Quick Mode**: Small caches (50 items), basic operations
- **Extended Mode**: Medium caches (200 items), varied operations
- **Long Mode**: Large caches (1000 items), performance testing
- **Stress Mode**: Very large caches (2000+ items), stress testing
## Integration with CI/CD
### GitHub Actions Example
```yaml
# Quick tests for every push/PR
- name: Quick Tests
run: go test ./... -short
# Extended tests for main branch
- name: Extended Tests
if: github.ref == 'refs/heads/main'
run: RUN_EXTENDED_TESTS=1 go test ./...
# Nightly comprehensive testing
- name: Nightly Stress Tests
if: github.event_name == 'schedule'
run: RUN_STRESS_TESTS=1 go test ./...
```
### Local Development Workflow
```bash
# During active development
go test ./... -short
# Before committing
RUN_EXTENDED_TESTS=1 go test ./...
# Before major releases
RUN_LONG_TESTS=1 go test ./...
# Performance validation
RUN_STRESS_TESTS=1 go test ./...
```
## Performance Optimization Features
### Dynamic Test Scaling
The test system automatically adjusts parameters based on:
- Test mode (quick/extended/long/stress)
- Available resources
- Environment variables
- Previous test performance
### Memory Management
- **Garbage Collection**: Forced GC between test iterations
- **Memory Monitoring**: Real-time memory growth tracking
- **Leak Detection**: Goroutine and memory leak prevention
- **Resource Cleanup**: Automatic cleanup of test resources
### Timeout Management
- **Adaptive Timeouts**: Timeouts scale with test complexity
- **Graceful Degradation**: Tests adapt to slower environments
- **Early Termination**: Failed tests terminate quickly
## Troubleshooting
### Tests Taking Too Long
```bash
# Check if running in extended mode accidentally
echo $RUN_EXTENDED_TESTS $RUN_LONG_TESTS
# Force quick mode
unset RUN_EXTENDED_TESTS RUN_LONG_TESTS RUN_STRESS_TESTS
go test ./... -short
```
### Memory Issues
```bash
# Reduce memory limits for constrained environments
export TEST_MEMORY_THRESHOLD_MB=5.0
export TEST_MAX_CONCURRENCY=2
go test ./...
```
### Concurrency Issues
```bash
# Reduce concurrency for slower systems
export TEST_MAX_CONCURRENCY=5
export TEST_MAX_ITERATIONS=10
go test ./...
```
### Skip Specific Test Types
```bash
# Skip memory leak detection if problematic
export DISABLE_LEAK_DETECTION=1
go test ./...
```
## Benchmarking
### Running Benchmarks
```bash
# Quick benchmarks
go test -bench=. -short
# Extended benchmarks
RUN_EXTENDED_TESTS=1 go test -bench=.
# Memory profiling
go test -bench=. -memprofile=mem.prof
go tool pprof mem.prof
```
### Benchmark Categories
- **Basic Operations**: Set/Get performance
- **Concurrency**: Multi-threaded performance
- **Memory**: Allocation and cleanup performance
- **Cache**: Eviction and cleanup performance
## Best Practices
### For Developers
1. Always run quick tests during development (`go test ./... -short`)
2. Run extended tests before committing (`RUN_EXTENDED_TESTS=1 go test ./...`)
3. Use appropriate test categories for your use case
4. Monitor test execution time and adjust if needed
### For CI/CD
1. Use quick tests for fast feedback on PRs
2. Use extended tests for main branch validation
3. Use long tests for release validation
4. Use stress tests for nightly/weekly validation
### For Performance Testing
1. Use consistent environment variables
2. Run tests multiple times for statistical significance
3. Monitor both execution time and resource usage
4. Use profiling tools for detailed analysis
## Examples
### Daily Development
```bash
# Fast tests while coding
go test ./... -short
# Before git commit
RUN_EXTENDED_TESTS=1 go test ./...
```
### Release Testing
```bash
# Comprehensive validation
RUN_LONG_TESTS=1 go test ./...
# Stress testing
RUN_STRESS_TESTS=1 go test ./...
```
### Custom Configuration
```bash
# Custom limits for specific environment
export TEST_MAX_CONCURRENCY=8
export TEST_MAX_ITERATIONS=25
export TEST_MEMORY_THRESHOLD_MB=15.0
RUN_EXTENDED_TESTS=1 go test ./...
```
This test system provides flexible, scalable test execution that adapts to your development workflow and infrastructure constraints while maintaining comprehensive test coverage.
+61 -3
View File
@@ -42,6 +42,8 @@ type Cache struct {
autoCleanupInterval time.Duration
// mutex protects all cache operations for thread safety
mutex sync.RWMutex
// stopChan for controlled shutdown
stopChan chan struct{}
}
// DefaultMaxSize is the default maximum number of items in the cache.
@@ -68,6 +70,7 @@ func NewCacheWithLogger(logger *Logger) *Cache {
maxSize: DefaultMaxSize,
autoCleanupInterval: 2 * time.Minute,
logger: logger,
stopChan: make(chan struct{}),
}
c.startAutoCleanup()
return c
@@ -232,8 +235,63 @@ func (c *Cache) startAutoCleanup() {
// Close stops the background cleanup task and releases associated resources.
// It should be called when the cache is no longer needed to prevent resource leaks.
func (c *Cache) Close() {
if c.cleanupTask != nil {
c.cleanupTask.Stop()
c.cleanupTask = nil
// First, close the stop channel and get cleanup task reference without holding lock
c.mutex.Lock()
// Stop channel first
select {
case <-c.stopChan:
// Already closed
c.mutex.Unlock()
return
default:
close(c.stopChan)
}
// Get reference to cleanup task before unlocking
cleanupTask := c.cleanupTask
c.mutex.Unlock()
// Stop the cleanup task WITHOUT holding the lock to avoid deadlock
if cleanupTask != nil {
cleanupTask.Stop()
}
// Now safely clear cache entries with lock
c.mutex.Lock()
defer c.mutex.Unlock()
// Clear the cleanup task reference
c.cleanupTask = nil
// Clear all cache entries with secure cleanup for sensitive data
for key, entry := range c.items {
c.securelyZeroEntry(&entry)
delete(c.items, key)
}
if c.logger != nil {
c.logger.Debug("Cache closed and resources cleaned up")
}
}
// securelyZeroEntry securely clears sensitive data from a cache entry
func (c *Cache) securelyZeroEntry(entry *CacheItem) {
if entry == nil {
return
}
// Securely zero value if it contains sensitive data
switch v := entry.Value.(type) {
case []byte:
for i := range v {
v[i] = 0
}
case string:
// For string values that might contain tokens, set to empty to help GC
entry.Value = ""
default:
// Clear reference to help GC
entry.Value = nil
}
}
+559 -291
View File
@@ -3,334 +3,602 @@ package traefikoidc
import (
"container/list"
"fmt"
"runtime"
"sync"
"testing"
"time"
)
// TestCacheMemoryLeaks tests various cache scenarios for memory leaks
// TestCacheMemoryLeaks tests various cache scenarios for memory leaks using unified infrastructure
func TestCacheMemoryLeaks(t *testing.T) {
t.Run("Cache doesn't release expired items memory", func(t *testing.T) {
runtime.GC()
var m runtime.MemStats
runtime.ReadMemStats(&m)
baselineAlloc := m.Alloc
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeLeakDetection) {
return
}
cache := NewCache()
defer cache.Close()
runner := NewTestSuiteRunner()
runner.SetTimeout(config.DefaultTimeout)
// Add many large items with short expiration
largeData := make([]byte, 1024*1024) // 1MB
for i := 0; i < 100; i++ {
key := fmt.Sprintf("key-%d", i)
// Items expire in 1 second
cache.Set(key, largeData, 1*time.Second)
}
// Define table of memory leak test cases
tests := []MemoryLeakTestCase{
{
Name: "Cache expired items memory release",
Description: "Test that cache properly releases memory for expired items after cleanup",
Iterations: config.MaxIterations,
MaxGoroutineGrowth: config.GoroutineGrowth,
MaxMemoryGrowthMB: config.MemoryThreshold,
GCBetweenRuns: true,
Timeout: config.DefaultTimeout,
Operation: func() error {
cache := NewCache()
defer cache.Close()
// Wait for items to expire
time.Sleep(2 * time.Second)
// Force cleanup
cache.Cleanup()
// Check memory after cleanup
runtime.GC()
runtime.ReadMemStats(&m)
afterCleanupAlloc := m.Alloc
allocIncrease := float64(afterCleanupAlloc-baselineAlloc) / 1024 / 1024
t.Logf("Memory after adding and expiring 100MB of data: %.2f MB", allocIncrease)
// Memory should be mostly released after cleanup
if allocIncrease > 10.0 {
t.Errorf("Cache retains too much memory after cleanup: %.2f MB", allocIncrease)
}
})
t.Run("Token blacklist unbounded growth", func(t *testing.T) {
runtime.GC()
var m runtime.MemStats
runtime.ReadMemStats(&m)
baselineAlloc := m.Alloc
blacklist := NewCache()
blacklist.SetMaxSize(1000) // Limit size
defer blacklist.Close()
// Simulate continuous token blacklisting
for i := 0; i < 10000; i++ {
token := fmt.Sprintf("token-%d", i)
// All tokens expire in 24 hours (typical blacklist duration)
blacklist.Set(token, true, 24*time.Hour)
}
runtime.GC()
runtime.ReadMemStats(&m)
currentAlloc := m.Alloc
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
t.Logf("Memory after adding 10000 blacklisted tokens (max 1000): %.2f MB", allocIncrease)
// Should respect max size limit
if len(blacklist.items) > 1000 {
t.Errorf("Blacklist exceeded max size: %d items", len(blacklist.items))
}
// Memory should be bounded
if allocIncrease > 5.0 {
t.Errorf("Blacklist uses too much memory: %.2f MB for max 1000 items", allocIncrease)
}
})
t.Run("Replay cache with high JTI volume", func(t *testing.T) {
initReplayCache()
defer cleanupReplayCache()
runtime.GC()
var m runtime.MemStats
runtime.ReadMemStats(&m)
baselineAlloc := m.Alloc
// Simulate high volume of JTIs
for i := 0; i < 20000; i++ {
jti := fmt.Sprintf("jti-%d", i)
replayCacheMu.Lock()
if replayCache != nil {
// JTIs expire after token expiry (typically 1 hour)
replayCache.Set(jti, true, 1*time.Hour)
}
replayCacheMu.Unlock()
}
runtime.GC()
runtime.ReadMemStats(&m)
currentAlloc := m.Alloc
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
t.Logf("Memory after adding 20000 JTIs (max 10000): %.2f MB", allocIncrease)
// Check size limit is enforced
replayCacheMu.RLock()
cacheSize := 0
if replayCache != nil {
cacheSize = len(replayCache.items)
}
replayCacheMu.RUnlock()
if cacheSize > 10000 {
t.Errorf("Replay cache exceeded max size: %d items", cacheSize)
}
// Memory should be bounded
if allocIncrease > 10.0 {
t.Errorf("Replay cache uses too much memory: %.2f MB for max 10000 items", allocIncrease)
}
})
t.Run("Cache cleanup interval effectiveness", func(t *testing.T) {
// Create a cache with custom settings - don't use NewCache to avoid default cleanup
cache := &Cache{
items: make(map[string]CacheItem, DefaultMaxSize),
order: list.New(),
elems: make(map[string]*list.Element, DefaultMaxSize),
maxSize: DefaultMaxSize,
autoCleanupInterval: 200 * time.Millisecond, // Fast cleanup for test
logger: newNoOpLogger(),
}
// Start cleanup with our custom interval
cache.startAutoCleanup()
defer cache.Close()
// Add expired items
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("key-%d", i)
cache.Set(key, "data", 50*time.Millisecond) // Very short expiry
}
// Wait for items to expire and cleanup to run (at least 2 cleanup cycles)
time.Sleep(600 * time.Millisecond)
// Manually trigger cleanup to ensure it runs
cache.Cleanup()
// Check that expired items are removed
cache.mutex.RLock()
remainingItems := len(cache.items)
cache.mutex.RUnlock()
t.Logf("Remaining items after auto cleanup: %d", remainingItems)
if remainingItems > 100 {
t.Errorf("Auto cleanup not effective: %d items remain", remainingItems)
}
})
t.Run("Concurrent cache operations memory stability", func(t *testing.T) {
cache := NewCache()
defer cache.Close()
runtime.GC()
var m runtime.MemStats
runtime.ReadMemStats(&m)
baselineAlloc := m.Alloc
var wg sync.WaitGroup
stop := make(chan struct{})
// Writers continuously add items
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 1000; j++ {
select {
case <-stop:
return
default:
key := fmt.Sprintf("writer-%d-%d", id, j)
cache.Set(key, "data", 1*time.Second)
time.Sleep(1 * time.Millisecond)
}
// Add large items with short expiration - size based on config
dataSize := 1024 * 1024 // 1MB for extended tests
if config.QuickMode {
dataSize = 64 * 1024 // 64KB for quick tests
}
}(i)
}
// Readers continuously read items
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 1000; j++ {
select {
case <-stop:
return
default:
key := fmt.Sprintf("writer-%d-%d", id%5, j)
cache.Get(key)
time.Sleep(1 * time.Millisecond)
}
largeData := make([]byte, dataSize)
itemCount := config.MaxIterations
if itemCount > 50 {
itemCount = 50
}
for i := 0; i < itemCount; i++ {
key := fmt.Sprintf("key-%d", i)
cache.Set(key, largeData, 100*time.Millisecond)
}
}(i)
}
// Let it run for a bit
time.Sleep(5 * time.Second)
close(stop)
wg.Wait()
// Wait for items to expire
time.Sleep(200 * time.Millisecond)
runtime.GC()
runtime.ReadMemStats(&m)
finalAlloc := m.Alloc
// Force cleanup
cache.Cleanup()
return nil
},
},
{
Name: "Token blacklist bounded growth",
Description: "Test that token blacklist respects size limits and doesn't grow unbounded",
Iterations: config.MaxIterations,
MaxGoroutineGrowth: config.GoroutineGrowth,
MaxMemoryGrowthMB: config.MemoryThreshold,
GCBetweenRuns: true,
Timeout: config.DefaultTimeout,
Operation: func() error {
blacklist := NewCache()
cacheSize := config.GetCacheSize()
blacklist.SetMaxSize(cacheSize)
defer blacklist.Close()
// Handle potential underflow
var allocIncrease float64
if finalAlloc > baselineAlloc {
allocIncrease = float64(finalAlloc-baselineAlloc) / 1024 / 1024
} else {
allocIncrease = -float64(baselineAlloc-finalAlloc) / 1024 / 1024
}
t.Logf("Memory increase under concurrent load: %.2f MB", allocIncrease)
// Simulate token blacklisting beyond limit
testCount := cacheSize * 2 // Try to exceed the limit
for i := 0; i < testCount; i++ {
token := fmt.Sprintf("token-%d", i)
blacklist.Set(token, true, 24*time.Hour)
}
if allocIncrease > 5.0 {
t.Errorf("Memory leak under concurrent operations: %.2f MB", allocIncrease)
}
})
// Verify size limit is enforced
if len(blacklist.items) > cacheSize {
return fmt.Errorf("blacklist exceeded max size: %d items (limit: %d)", len(blacklist.items), cacheSize)
}
return nil
},
},
{
Name: "Replay cache high JTI volume",
Description: "Test replay cache memory behavior under high JTI volume",
Iterations: config.MaxIterations,
MaxGoroutineGrowth: config.GoroutineGrowth,
MaxMemoryGrowthMB: config.MemoryThreshold,
GCBetweenRuns: true,
Timeout: config.DefaultTimeout,
Setup: func() error {
initReplayCache()
return nil
},
Teardown: func() error {
cleanupReplayCache()
return nil
},
Operation: func() error {
// Scale volume based on config
volume := config.MaxIterations * 100
if volume > 1000 && config.QuickMode {
volume = 100
}
for i := 0; i < volume; i++ {
jti := fmt.Sprintf("jti-%d", i)
replayCacheMu.Lock()
if replayCache != nil {
replayCache.Set(jti, true, 1*time.Hour)
}
replayCacheMu.Unlock()
}
t.Run("LRU eviction memory release", func(t *testing.T) {
cache := NewCache()
cache.SetMaxSize(100) // Small cache
defer cache.Close()
// Check size limit is enforced
replayCacheMu.RLock()
cacheSize := 0
if replayCache != nil {
cacheSize = len(replayCache.items)
}
replayCacheMu.RUnlock()
runtime.GC()
var m runtime.MemStats
runtime.ReadMemStats(&m)
baselineAlloc := m.Alloc
if cacheSize > 10000 {
return fmt.Errorf("replay cache exceeded max size: %d items", cacheSize)
}
return nil
},
},
{
Name: "Concurrent cache operations stability",
Description: "Test memory stability under concurrent cache operations",
Iterations: config.MaxIterations,
MaxGoroutineGrowth: config.GoroutineGrowth * 2, // Allow for some goroutine fluctuation
MaxMemoryGrowthMB: config.MemoryThreshold,
GCBetweenRuns: true,
Timeout: config.DefaultTimeout,
Operation: func() error {
cache := NewCache()
defer cache.Close()
// Add many items to trigger eviction
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("key-%d", i)
data := make([]byte, 10240) // 10KB per item
cache.Set(key, data, 1*time.Hour)
}
var wg sync.WaitGroup
stop := make(chan struct{})
runtime.GC()
runtime.ReadMemStats(&m)
afterEvictionAlloc := m.Alloc
// Scale concurrency and operations based on config
writerCount := config.AdjustConcurrencyParams(5)
if config.QuickMode && writerCount > 2 {
writerCount = 2
}
operations := config.MaxIterations * 5
if operations > 50 && config.QuickMode {
operations = 10
}
allocIncrease := float64(afterEvictionAlloc-baselineAlloc) / 1024 / 1024
t.Logf("Memory after LRU eviction (1000 items, max 100): %.2f MB", allocIncrease)
for i := 0; i < writerCount; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < operations; j++ {
select {
case <-stop:
return
default:
key := fmt.Sprintf("writer-%d-%d", id, j)
cache.Set(key, "data", 1*time.Second)
time.Sleep(1 * time.Millisecond)
}
}
}(i)
}
// Should only keep 100 items worth of memory
if allocIncrease > 2.0 { // 100 * 10KB = ~1MB
t.Errorf("LRU eviction doesn't release memory properly: %.2f MB", allocIncrease)
}
// Readers - match writer count
for i := 0; i < writerCount; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < operations; j++ {
select {
case <-stop:
return
default:
key := fmt.Sprintf("writer-%d-%d", id%writerCount, j)
cache.Get(key)
time.Sleep(1 * time.Millisecond)
}
}
}(i)
}
// Verify cache size
if len(cache.items) > 100 {
t.Errorf("Cache size exceeded limit: %d items", len(cache.items))
}
})
// Let it run briefly - adjust based on config
runTime := config.GetCleanupInterval() * 5
if runTime > 500*time.Millisecond {
runTime = 500 * time.Millisecond
}
time.Sleep(runTime)
close(stop)
wg.Wait()
return nil
},
},
{
Name: "LRU eviction memory release",
Description: "Test that LRU eviction properly releases memory",
Iterations: 5,
MaxGoroutineGrowth: 1,
MaxMemoryGrowthMB: 2.0,
GCBetweenRuns: true,
Timeout: 10 * time.Second,
Operation: func() error {
cache := NewCache()
cache.SetMaxSize(10) // Very small cache for effective eviction
defer cache.Close()
t.Run("Token cache with claims memory", func(t *testing.T) {
tokenCache := NewTokenCache()
defer tokenCache.Close()
// Add items to trigger eviction
for i := 0; i < 50; i++ {
key := fmt.Sprintf("key-%d", i)
data := make([]byte, 1024) // 1KB per item
cache.Set(key, data, 1*time.Hour)
}
runtime.GC()
var m runtime.MemStats
runtime.ReadMemStats(&m)
baselineAlloc := m.Alloc
// Verify cache size limit
if len(cache.items) > 10 {
return fmt.Errorf("cache size exceeded limit: %d items", len(cache.items))
}
return nil
},
},
{
Name: "Token cache with claims memory",
Description: "Test memory usage of token cache with large claims",
Iterations: 3,
MaxGoroutineGrowth: 1,
MaxMemoryGrowthMB: 20.0,
GCBetweenRuns: true,
Timeout: 10 * time.Second,
Operation: func() error {
tokenCache := NewTokenCache()
defer tokenCache.Close()
// Add tokens with large claims
for i := 0; i < 1000; i++ {
token := fmt.Sprintf("token-%d", i)
claims := map[string]interface{}{
"sub": fmt.Sprintf("user-%d", i),
"email": fmt.Sprintf("user%[email protected]", i),
"groups": make([]string, 100), // Large groups list
"data": make([]byte, 1024), // Extra data
}
tokenCache.Set(token, claims, 1*time.Hour)
}
// Add tokens with large claims (reduced count for repeated iterations)
for i := 0; i < 100; i++ {
token := fmt.Sprintf("token-%d", i)
claims := map[string]interface{}{
"sub": fmt.Sprintf("user-%d", i),
"email": fmt.Sprintf("user%[email protected]", i),
"groups": make([]string, 10), // Smaller groups list
"data": make([]byte, 512), // Smaller data
}
tokenCache.Set(token, claims, 1*time.Hour)
}
return nil
},
},
{
Name: "Cache cleanup interval effectiveness",
Description: "Test that cache cleanup interval effectively removes expired items",
Iterations: 3,
MaxGoroutineGrowth: 2,
MaxMemoryGrowthMB: 5.0,
GCBetweenRuns: true,
Timeout: 5 * time.Second,
Operation: func() error {
// Create a cache with custom settings - don't use NewCache to avoid default cleanup
cache := &Cache{
items: make(map[string]CacheItem, DefaultMaxSize),
order: list.New(),
elems: make(map[string]*list.Element, DefaultMaxSize),
maxSize: DefaultMaxSize,
autoCleanupInterval: 100 * time.Millisecond, // Fast cleanup for test
logger: newNoOpLogger(),
stopChan: make(chan struct{}),
}
runtime.GC()
runtime.ReadMemStats(&m)
currentAlloc := m.Alloc
// Start cleanup with our custom interval
cache.startAutoCleanup()
defer cache.Close()
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
t.Logf("Memory after adding 1000 tokens with large claims: %.2f MB", allocIncrease)
// Add expired items (reduced count for repeated iterations)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("key-%d", i)
cache.Set(key, "data", 30*time.Millisecond) // Very short expiry
}
// Check if memory is reasonable
if allocIncrease > 20.0 {
t.Errorf("Token cache uses excessive memory: %.2f MB", allocIncrease)
}
})
// Wait for items to expire and cleanup to run
time.Sleep(300 * time.Millisecond)
// Manually trigger cleanup to ensure it runs
cache.Cleanup()
// Check that expired items are removed
cache.mutex.RLock()
remainingItems := len(cache.items)
cache.mutex.RUnlock()
if remainingItems > 10 {
return fmt.Errorf("auto cleanup not effective: %d items remain", remainingItems)
}
return nil
},
},
}
// Adjust all test cases based on configuration
for i := range tests {
config.AdjustMemoryLeakTestCase(&tests[i])
}
// Run memory leak tests using the unified infrastructure
runner.RunMemoryLeakTests(t, tests)
}
// TestCacheEvictionBug tests the inefficient eviction in evictOldest
func TestCacheEvictionBug(t *testing.T) {
t.Run("evictOldest scans entire list", func(t *testing.T) {
// TestCacheEvictionPerformance tests the performance of cache eviction using table-driven patterns
func TestCacheEvictionPerformance(t *testing.T) {
runner := NewTestSuiteRunner()
runner.SetTimeout(15 * time.Second)
// Generate edge cases for cache sizes
edgeGen := NewEdgeCaseGenerator()
cacheSizes := []int{10, 50, 100, 500, 1000}
var tests []TableTestCase
// Create table-driven tests for different cache sizes
for _, size := range cacheSizes {
tests = append(tests, TableTestCase{
Name: fmt.Sprintf("Eviction performance with cache size %d", size),
Description: fmt.Sprintf("Test eviction performance doesn't degrade with cache size %d", size),
Input: size,
Timeout: 5 * time.Second,
Setup: func(t *testing.T) error {
return nil
},
Teardown: func(t *testing.T) error {
return nil
},
})
}
// Add edge cases with unusual data
stringEdgeCases := edgeGen.GenerateStringEdgeCases()
for i, edgeString := range stringEdgeCases[:5] { // Limit to first 5 edge cases
tests = append(tests, TableTestCase{
Name: fmt.Sprintf("Eviction with edge case data %d", i),
Description: fmt.Sprintf("Test eviction with unusual string data: %q", edgeString),
Input: map[string]interface{}{"data": edgeString, "size": 50},
Timeout: 3 * time.Second,
})
}
// Custom test execution for eviction performance
for _, test := range tests {
test := test // Capture loop variable
t.Run(test.Name, func(t *testing.T) {
var cacheSize int
var testData interface{}
if size, ok := test.Input.(int); ok {
cacheSize = size
testData = "standard-data"
} else if inputMap, ok := test.Input.(map[string]interface{}); ok {
cacheSize = inputMap["size"].(int)
testData = inputMap["data"]
}
cache := NewCache()
cache.SetMaxSize(cacheSize)
defer cache.Close()
// Fill cache with items
for i := 0; i < cacheSize; i++ {
key := fmt.Sprintf("key-%d", i)
cache.Set(key, testData, 1*time.Hour) // Long expiry
}
// Measure eviction performance
perfHelper := NewPerformanceTestHelper()
// Perform multiple eviction operations and measure
for i := 0; i < 10; i++ {
triggerKey := fmt.Sprintf("trigger-%d", i)
elapsed := perfHelper.Measure(func() {
cache.Set(triggerKey, testData, 1*time.Hour)
})
// Individual operations should be fast
if elapsed > 10*time.Millisecond {
t.Errorf("Eviction too slow for operation %d: %v", i, elapsed)
}
}
avgTime := perfHelper.GetAverageTime()
t.Logf("Average eviction time for cache size %d: %v", cacheSize, avgTime)
// Average time should be reasonable
if avgTime > 5*time.Millisecond {
t.Errorf("Average eviction time too high: %v", avgTime)
}
})
}
}
// TestCacheMemoryLeakEdgeCases tests memory leak behavior with edge cases
func TestCacheMemoryLeakEdgeCases(t *testing.T) {
runner := NewTestSuiteRunner()
edgeGen := NewEdgeCaseGenerator()
// Generate edge cases for comprehensive testing
stringEdgeCases := edgeGen.GenerateStringEdgeCases()
intEdgeCases := edgeGen.GenerateIntegerEdgeCases()
timeEdgeCases := edgeGen.GenerateTimeEdgeCases()
tests := []MemoryLeakTestCase{
{
Name: "Edge case string data memory",
Description: "Test memory behavior with edge case string data",
Iterations: 5,
MaxGoroutineGrowth: 1,
MaxMemoryGrowthMB: 10.0,
GCBetweenRuns: true,
Timeout: 10 * time.Second,
Operation: func() error {
cache := NewCache()
cache.SetMaxSize(20)
defer cache.Close()
// Test with various edge case strings
for i, edgeString := range stringEdgeCases {
if i >= 10 { // Limit iterations
break
}
key := fmt.Sprintf("edge-key-%d", i)
cache.Set(key, edgeString, 1*time.Hour)
}
if len(cache.items) > 20 {
return fmt.Errorf("cache exceeded size limit with edge cases: %d", len(cache.items))
}
return nil
},
},
{
Name: "Edge case expiration times",
Description: "Test memory behavior with edge case expiration times",
Iterations: 3,
MaxGoroutineGrowth: 1,
MaxMemoryGrowthMB: 5.0,
GCBetweenRuns: true,
Timeout: 15 * time.Second,
Operation: func() error {
cache := NewCache()
defer cache.Close()
// Test with various edge case times
for i, edgeTime := range timeEdgeCases {
if i >= 5 { // Limit iterations
break
}
key := fmt.Sprintf("time-key-%d", i)
duration := time.Until(edgeTime)
if duration < 0 {
duration = 100 * time.Millisecond // Use short duration for past times
}
if duration > time.Hour {
duration = time.Hour // Cap at 1 hour for very large durations
}
cache.Set(key, fmt.Sprintf("data-%d", i), duration)
}
return nil
},
},
{
Name: "Edge case cache sizes",
Description: "Test memory behavior with edge case cache sizes",
Iterations: 3,
MaxGoroutineGrowth: 1,
MaxMemoryGrowthMB: 15.0,
GCBetweenRuns: true,
Timeout: 10 * time.Second,
Operation: func() error {
// Test with various edge case sizes
for i, edgeSize := range intEdgeCases {
if i >= 3 { // Limit iterations
break
}
if edgeSize <= 0 || edgeSize > 1000 {
continue // Skip invalid or too large sizes
}
cache := NewCache()
cache.SetMaxSize(edgeSize)
defer cache.Close()
// Add items up to the limit
itemsToAdd := edgeSize * 2 // Try to exceed the limit
if itemsToAdd > 100 {
itemsToAdd = 100 // Cap for test performance
}
for j := 0; j < itemsToAdd; j++ {
key := fmt.Sprintf("size-test-%d-%d", edgeSize, j)
cache.Set(key, "data", 1*time.Hour)
}
if len(cache.items) > edgeSize {
return fmt.Errorf("cache size %d exceeded limit: %d items", edgeSize, len(cache.items))
}
}
return nil
},
},
}
runner.RunMemoryLeakTests(t, tests)
}
// BenchmarkCacheOperations provides performance benchmarks for memory-critical operations
func BenchmarkCacheOperations(b *testing.B) {
// Benchmark cache set operations
b.Run("CacheSet", func(b *testing.B) {
cache := NewCache()
defer cache.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("bench-key-%d", i)
cache.Set(key, "benchmark-data", 1*time.Hour)
}
})
// Benchmark cache get operations
b.Run("CacheGet", func(b *testing.B) {
cache := NewCache()
defer cache.Close()
// Pre-populate cache
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("bench-key-%d", i)
cache.Set(key, "benchmark-data", 1*time.Hour)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("bench-key-%d", i%1000)
cache.Get(key)
}
})
// Benchmark eviction performance
b.Run("CacheEviction", func(b *testing.B) {
cache := NewCache()
cache.SetMaxSize(100)
defer cache.Close()
// Fill cache with non-expired items
// Pre-fill cache to trigger evictions
for i := 0; i < 100; i++ {
key := fmt.Sprintf("key-%d", i)
cache.Set(key, "data", 1*time.Hour) // Long expiry
key := fmt.Sprintf("initial-key-%d", i)
cache.Set(key, "initial-data", 1*time.Hour)
}
// Try to add one more item to trigger eviction
start := time.Now()
cache.Set("trigger", "data", 1*time.Hour)
elapsed := time.Since(start)
t.Logf("Time to evict and add one item: %v", elapsed)
// Should be fast even with full cache
if elapsed > 10*time.Millisecond {
t.Errorf("Eviction too slow, possibly scanning entire list: %v", elapsed)
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("evict-key-%d", i)
cache.Set(key, "evict-data", 1*time.Hour)
}
})
// Benchmark cleanup performance
b.Run("CacheCleanup", func(b *testing.B) {
cache := NewCache()
defer cache.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Add expired items
for j := 0; j < 50; j++ {
key := fmt.Sprintf("cleanup-key-%d-%d", i, j)
cache.Set(key, "cleanup-data", 1*time.Nanosecond) // Immediately expired
}
// Benchmark cleanup
cache.Cleanup()
}
})
// Benchmark concurrent operations
b.Run("CacheConcurrent", func(b *testing.B) {
cache := NewCache()
defer cache.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
key := fmt.Sprintf("concurrent-key-%d", i)
cache.Set(key, "concurrent-data", 1*time.Hour)
cache.Get(key)
i++
}
})
})
}
+106 -5
View File
@@ -1,6 +1,7 @@
package traefikoidc
import (
"context"
"crypto/sha256"
"encoding/hex"
"sync"
@@ -44,6 +45,14 @@ type OptimizedCache struct {
autoCleanupInterval time.Duration
// mutex provides thread safety for all operations
mutex sync.RWMutex
// ctx provides context for background operations
ctx context.Context
// cancel cancels background operations
cancel context.CancelFunc
// wg tracks background goroutines
wg sync.WaitGroup
// closed indicates if cache has been closed
closed bool
}
// normalizeKey ensures keys are within reasonable limits by hashing long keys.
@@ -93,6 +102,8 @@ func NewOptimizedCacheWithConfig(maxSize int, maxMemoryMB int, logger *Logger) *
maxMemoryBytes = 64 * 1024 * 1024
}
ctx, cancel := context.WithCancel(context.Background())
c := &OptimizedCache{
items: make(map[string]*OptimizedCacheEntry, maxSize),
head: head,
@@ -101,6 +112,8 @@ func NewOptimizedCacheWithConfig(maxSize int, maxMemoryMB int, logger *Logger) *
maxMemoryBytes: maxMemoryBytes,
autoCleanupInterval: 2 * time.Minute,
logger: logger,
ctx: ctx,
cancel: cancel,
}
c.startAutoCleanup()
@@ -115,6 +128,12 @@ func NewOptimizedCacheWithConfig(maxSize int, maxMemoryMB int, logger *Logger) *
// - value: The value to store
// - expiration: Time until the item expires
func (c *OptimizedCache) Set(key string, value interface{}, expiration time.Duration) {
c.mutex.RLock()
if c.closed {
c.mutex.RUnlock()
return
}
c.mutex.RUnlock()
// Normalize the key to handle long keys
normalizedKey := c.normalizeKey(key)
@@ -164,6 +183,12 @@ func (c *OptimizedCache) Set(key string, value interface{}, expiration time.Dura
// automatically removes expired items when encountered.
// Returns the value and true if found and valid, or nil and false otherwise.
func (c *OptimizedCache) Get(key string) (interface{}, bool) {
c.mutex.RLock()
if c.closed {
c.mutex.RUnlock()
return nil, false
}
c.mutex.RUnlock()
// Normalize the key to handle long keys
normalizedKey := c.normalizeKey(key)
@@ -209,7 +234,8 @@ func (c *OptimizedCache) Cleanup() {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.items == nil || c.head == nil || c.tail == nil {
// Check if cache is closed - if so, exit early
if c.closed || c.items == nil || c.head == nil || c.tail == nil {
return
}
@@ -319,6 +345,44 @@ func (c *OptimizedCache) estimateEntrySize(entry *OptimizedCacheEntry) int64 {
return size
}
// securelyZeroEntry securely clears sensitive data from a cache entry
func (c *OptimizedCache) securelyZeroEntry(entry *OptimizedCacheEntry) {
if entry == nil {
return
}
// Zero out the key - Cannot modify string directly, but we can ensure it's cleared by GC
_ = entry.Key
// Securely zero value if it contains sensitive data
switch v := entry.Value.(type) {
case []byte:
for i := range v {
v[i] = 0
}
case string:
// For string values that might contain tokens, we can't modify directly
// but we set to empty to help GC
entry.Value = ""
case map[string]interface{}:
for key, val := range v {
switch val := val.(type) {
case []byte:
for i := range val {
val[i] = 0
}
case string:
v[key] = ""
}
}
}
// Clear references to help GC
entry.Value = nil
entry.prev = nil
entry.next = nil
}
// SetMaxSize changes the maximum number of items the cache can hold.
// If the new limit is smaller than current size, least recently used items are evicted.
func (c *OptimizedCache) SetMaxSize(size int) {
@@ -360,15 +424,52 @@ func (c *OptimizedCache) SetMaxMemory(maxMemoryMB int) {
// startAutoCleanup starts the background cleanup task for automatic maintenance.
// The task runs periodically to remove expired entries and enforce memory limits.
func (c *OptimizedCache) startAutoCleanup() {
c.cleanupTask = NewBackgroundTask("optimized-cache-cleanup", c.autoCleanupInterval, c.Cleanup, c.logger)
c.cleanupTask = NewBackgroundTask("optimized-cache-cleanup", c.autoCleanupInterval, c.Cleanup, c.logger, &c.wg)
c.cleanupTask.Start()
}
// Close stops the automatic cleanup task and releases resources.
// Should be called when the cache is no longer needed to prevent resource leaks.
func (c *OptimizedCache) Close() {
if c.cleanupTask != nil {
c.cleanupTask.Stop()
c.cleanupTask = nil
// First, mark as closed and cancel context without holding the lock
c.mutex.Lock()
if c.closed {
c.mutex.Unlock()
return
}
c.closed = true
// Cancel context to stop all background operations
if c.cancel != nil {
c.cancel()
}
// Get reference to cleanup task before unlocking
cleanupTask := c.cleanupTask
c.mutex.Unlock()
// Stop the cleanup task WITHOUT holding the lock to avoid deadlock
if cleanupTask != nil {
cleanupTask.Stop()
}
// Wait for all background operations to complete
c.wg.Wait()
// Now safely clear all cache entries with lock
c.mutex.Lock()
defer c.mutex.Unlock()
// Clear the cleanup task reference
c.cleanupTask = nil
// Securely clear all cache entries containing sensitive data
for key, entry := range c.items {
c.securelyZeroEntry(entry)
delete(c.items, key)
}
if c.logger != nil {
c.logger.Debug("OptimizedCache closed and resources cleaned up")
}
}
+666 -3
View File
@@ -1,20 +1,682 @@
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
c := NewCache()
defer c.Close() // Stop the auto-cleanup goroutine
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
// Use the Set method to properly add items with synchronization
c.Set("expired", "expired-value", pastTime.Sub(now))
c.Set("valid", "valid-value", futureTime.Sub(now))
@@ -31,9 +693,10 @@ func TestCache_Cleanup(t *testing.T) {
}
}
// TestCache_SetMaxSize tests the specific max size functionality (converted from original test)
func TestCache_SetMaxSize(t *testing.T) {
c := NewCache()
defer c.Close() // Stop the auto-cleanup goroutine
defer c.Close()
// Set a lower max size
originalMaxSize := 10
+30 -2
View File
@@ -727,6 +727,12 @@ type GracefulDegradation struct {
config GracefulDegradationConfig
// mutex protects shared state
mutex sync.RWMutex
// healthCheckTask manages background health checking
healthCheckTask *BackgroundTask
// stopChan signals shutdown
stopChan chan struct{}
// shutdownOnce ensures shutdown happens only once
shutdownOnce sync.Once
}
// GracefulDegradationConfig holds configuration for graceful degradation behavior.
@@ -762,6 +768,7 @@ func NewGracefulDegradation(config GracefulDegradationConfig, logger *Logger) *G
config: config,
}
gd.stopChan = make(chan struct{})
go gd.startHealthCheckRoutine()
return gd
@@ -867,13 +874,13 @@ func (gd *GracefulDegradation) executeFallback(serviceName string) (interface{},
// startHealthCheckRoutine starts the background health check routine
func (gd *GracefulDegradation) startHealthCheckRoutine() {
healthCheckTask := NewBackgroundTask(
gd.healthCheckTask = NewBackgroundTask(
"graceful-degradation-health-check",
gd.config.HealthCheckInterval,
gd.performHealthChecks,
gd.BaseRecoveryMechanism.logger,
)
healthCheckTask.Start()
gd.healthCheckTask.Start()
}
// performHealthChecks runs health checks for all registered services
@@ -921,6 +928,27 @@ func (gd *GracefulDegradation) Reset() {
gd.LogInfo("Graceful degradation state has been reset")
}
// Close shuts down the graceful degradation system and cleans up resources
func (gd *GracefulDegradation) Close() {
gd.shutdownOnce.Do(func() {
// Signal shutdown
select {
case <-gd.stopChan:
// Already closed
default:
close(gd.stopChan)
}
// Stop health check task
if gd.healthCheckTask != nil {
gd.healthCheckTask.Stop()
gd.healthCheckTask = nil
}
gd.logger.Info("GracefulDegradation shut down successfully")
})
}
// IsAvailable returns whether the mechanism is available for use
func (gd *GracefulDegradation) IsAvailable() bool {
return true
+26 -3
View File
@@ -13,14 +13,19 @@ type ProviderRegistry struct {
typeMap map[ProviderType]OIDCProvider
providers []OIDCProvider
mu sync.RWMutex
// Bounded cache configuration to prevent memory leaks
maxCacheSize int
cacheCount int
}
// NewProviderRegistry creates and initializes a new ProviderRegistry.
func NewProviderRegistry() *ProviderRegistry {
return &ProviderRegistry{
providers: make([]OIDCProvider, 0),
cache: make(map[string]OIDCProvider),
typeMap: make(map[ProviderType]OIDCProvider),
providers: make([]OIDCProvider, 0),
cache: make(map[string]OIDCProvider),
typeMap: make(map[ProviderType]OIDCProvider),
maxCacheSize: 1000, // Prevent unbounded cache growth
cacheCount: 0,
}
}
@@ -59,6 +64,18 @@ func (r *ProviderRegistry) ClearCache() {
r.mu.Lock()
defer r.mu.Unlock()
r.cache = make(map[string]OIDCProvider)
r.cacheCount = 0
}
// evictOldestCacheEntry removes the first cache entry when cache is full
// This is a simple eviction strategy - in production, LRU might be preferred
func (r *ProviderRegistry) evictOldestCacheEntry() {
// Simple eviction: remove first entry found
for key := range r.cache {
delete(r.cache, key)
r.cacheCount--
break
}
}
// DetectProvider identifies the appropriate OIDC provider for an issuer URL.
@@ -80,7 +97,13 @@ func (r *ProviderRegistry) DetectProvider(issuerURL string) OIDCProvider {
detectedProvider := r.detectProviderUnsafe(issuerURL)
// Check if cache is full and evict if necessary
if r.cacheCount >= r.maxCacheSize {
r.evictOldestCacheEntry()
}
r.cache[issuerURL] = detectedProvider
r.cacheCount++
return detectedProvider
}
+12 -1
View File
@@ -692,7 +692,18 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
logger.Debugf("TraefikOidc.New: Final t.scopes initialized to: %v", t.scopes)
t.providerURL = config.ProviderURL
go t.initializeMetadata(config.ProviderURL)
// Initialize metadata in background but track the goroutine
if t.goroutineWG != nil {
t.goroutineWG.Add(1)
}
go func() {
defer func() {
if t.goroutineWG != nil {
t.goroutineWG.Done()
}
}()
t.initializeMetadata(config.ProviderURL)
}()
return t, nil
}
+1
View File
@@ -65,6 +65,7 @@ func NewLazyCacheWithLogger(logger *Logger) *Cache {
maxSize: DefaultMaxSize,
autoCleanupInterval: 10 * time.Minute,
logger: logger,
stopChan: make(chan struct{}),
}
return c
}
File diff suppressed because it is too large Load Diff
+37 -3
View File
@@ -192,8 +192,42 @@ func (c *MetadataCache) startAutoCleanup() {
// Close stops the automatic cleanup task and releases resources.
// This should be called when the cache is no longer needed to prevent resource leaks.
func (c *MetadataCache) Close() {
if c.cleanupTask != nil {
c.cleanupTask.Stop()
c.cleanupTask = nil
// First, close the stop channel and get cleanup task reference without holding lock
c.mutex.Lock()
// Stop channel first
select {
case <-c.stopChan:
// Already closed
c.mutex.Unlock()
return
default:
close(c.stopChan)
}
// Get reference to cleanup task before unlocking
cleanupTask := c.cleanupTask
c.mutex.Unlock()
// Stop the cleanup task WITHOUT holding the lock to avoid deadlock
if cleanupTask != nil {
cleanupTask.Stop()
}
// Wait for background operations if WaitGroup is provided
if c.wg != nil {
c.wg.Wait()
}
// Now safely clear the cleanup task reference
c.mutex.Lock()
defer c.mutex.Unlock()
c.cleanupTask = nil
// Clear cached metadata
c.metadata = nil
if c.logger != nil {
c.logger.Debug("MetadataCache closed and resources cleaned up")
}
}
+857 -72
View File
@@ -1,103 +1,888 @@
package traefikoidc
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
)
func TestCleanup(t *testing.T) {
pm := &ProviderMetadata{}
mc := &MetadataCache{
metadata: pm,
expiresAt: time.Now().Add(-1 * time.Hour),
}
mc.Cleanup()
if mc.metadata != nil {
t.Errorf("Expected metadata to be nil after cleanup")
// MetadataCacheTestSuite provides comprehensive testing for MetadataCache
type MetadataCacheTestSuite struct {
runner *TestSuiteRunner
factory *TestDataFactory
edgeGen *EdgeCaseGenerator
perfTest *PerformanceTestHelper
mockWG *sync.WaitGroup
logger *Logger
testCache *MetadataCache
}
// NewMetadataCacheTestSuite creates a new test suite for MetadataCache
func NewMetadataCacheTestSuite() *MetadataCacheTestSuite {
return &MetadataCacheTestSuite{
runner: NewTestSuiteRunner(),
factory: NewTestDataFactory(),
edgeGen: NewEdgeCaseGenerator(),
perfTest: NewPerformanceTestHelper(),
mockWG: &sync.WaitGroup{},
logger: NewLogger("error"),
}
}
func TestGetMetadata_Cached(t *testing.T) {
dummyData := &ProviderMetadata{}
// Construct MetadataCache manually to avoid interference from auto cleanup.
mc := &MetadataCache{
metadata: dummyData,
expiresAt: time.Now().Add(1 * time.Hour),
autoCleanupInterval: 5 * time.Minute,
logger: newNoOpLogger(),
// setup creates a new MetadataCache for testing
func (suite *MetadataCacheTestSuite) setup() {
suite.testCache = NewMetadataCacheWithLogger(suite.mockWG, suite.logger)
}
// cleanup cleans up test resources
func (suite *MetadataCacheTestSuite) cleanup() {
if suite.testCache != nil {
suite.testCache.Close()
}
// Use NewLogger to create a logger that writes errors only.
logger := NewLogger("error")
result, err := mc.GetMetadata("http://example.com", http.DefaultClient, logger)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if result != dummyData {
t.Errorf("Expected cached metadata to be returned")
suite.perfTest.Reset()
}
// createTestServer creates a mock HTTP server for testing
func (suite *MetadataCacheTestSuite) createTestServer(metadata *ProviderMetadata, statusCode int, delay time.Duration) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if delay > 0 {
time.Sleep(delay)
}
w.WriteHeader(statusCode)
if statusCode == http.StatusOK && metadata != nil {
json.NewEncoder(w).Encode(metadata)
}
}))
}
// createErrorClient creates an HTTP client that returns errors
func (suite *MetadataCacheTestSuite) createErrorClient(err error) *http.Client {
return &http.Client{
Transport: &errorRoundTripper{err: err},
}
}
func TestMetadataCacheAutoCleanup(t *testing.T) {
mc := &MetadataCache{
autoCleanupInterval: 50 * time.Millisecond,
logger: newNoOpLogger(),
}
// Start auto cleanup.
mc.startAutoCleanup()
mc.mutex.Lock()
mc.metadata = &ProviderMetadata{}
mc.expiresAt = time.Now().Add(-50 * time.Millisecond)
mc.mutex.Unlock()
// Wait enough time for the auto cleanup to run.
time.Sleep(200 * time.Millisecond)
mc.Close()
mc.mutex.RLock()
defer mc.mutex.RUnlock()
if mc.metadata != nil {
t.Errorf("Expected metadata to be cleared by auto cleanup")
// createTimeoutClient creates an HTTP client with timeout
func (suite *MetadataCacheTestSuite) createTimeoutClient(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
}
}
// errorRoundTripper implements http.RoundTripper for error testing
type errorRoundTripper struct {
err error
}
func (e errorRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
func (e *errorRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return nil, e.err
}
func TestGetMetadata_FetchError(t *testing.T) {
// Create an HTTP client that always returns an error.
errorClient := &http.Client{
Transport: errorRoundTripper{err: fmt.Errorf("fake fetch error")},
// TestMetadataCache_BasicOperations tests basic cache operations
func TestMetadataCache_BasicOperations(t *testing.T) {
suite := NewMetadataCacheTestSuite()
tests := []TableTestCase{
{
Name: "CleanupExpiredMetadata",
Description: "Test that expired metadata is removed during cleanup",
Setup: func(t *testing.T) error {
suite.setup()
// Set expired metadata
suite.testCache.metadata = &ProviderMetadata{Issuer: "test"}
suite.testCache.expiresAt = time.Now().Add(-1 * time.Hour)
return nil
},
Teardown: func(t *testing.T) error {
suite.cleanup()
return nil
},
},
{
Name: "CleanupValidMetadata",
Description: "Test that valid metadata is not removed during cleanup",
Setup: func(t *testing.T) error {
suite.setup()
// Set valid metadata
suite.testCache.metadata = &ProviderMetadata{Issuer: "test"}
suite.testCache.expiresAt = time.Now().Add(1 * time.Hour)
return nil
},
Teardown: func(t *testing.T) error {
suite.cleanup()
return nil
},
},
{
Name: "CleanupNilMetadata",
Description: "Test cleanup when metadata is nil",
Setup: func(t *testing.T) error {
suite.setup()
suite.testCache.metadata = nil
return nil
},
Teardown: func(t *testing.T) error {
suite.cleanup()
return nil
},
},
}
// Case 1: Cache is empty.
mc := &MetadataCache{
logger: newNoOpLogger(),
}
logger := NewLogger("error")
metadata, err := mc.GetMetadata("http://example.com", errorClient, logger)
if err == nil {
t.Errorf("Expected error, got nil")
}
if metadata != nil {
t.Errorf("Expected nil metadata, got %v", metadata)
}
// Run basic cleanup tests
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
if test.Setup != nil {
if err := test.Setup(t); err != nil {
t.Fatalf("Setup failed: %v", err)
}
}
defer func() {
if test.Teardown != nil {
if err := test.Teardown(t); err != nil {
t.Errorf("Teardown failed: %v", err)
}
}
}()
// Case 2: Cache has old metadata.
dummy := &ProviderMetadata{}
mc.metadata = dummy
mc.expiresAt = time.Now().Add(-1 * time.Minute)
logger2 := NewLogger("error")
metadata, err = mc.GetMetadata("http://example.com", errorClient, logger2)
if err != nil {
t.Errorf("Expected no error when cached metadata exists, got %v", err)
}
if metadata != dummy {
t.Errorf("Expected cached metadata to be returned")
originalMetadata := suite.testCache.metadata
suite.testCache.Cleanup()
switch test.Name {
case "CleanupExpiredMetadata":
if suite.testCache.metadata != nil {
t.Error("Expected expired metadata to be nil after cleanup")
}
case "CleanupValidMetadata":
if suite.testCache.metadata != originalMetadata {
t.Error("Expected valid metadata to remain after cleanup")
}
case "CleanupNilMetadata":
if suite.testCache.metadata != nil {
t.Error("Expected metadata to remain nil after cleanup")
}
}
})
}
}
// TestMetadataCache_CacheHitMiss tests cache hit and miss scenarios
func TestMetadataCache_CacheHitMiss(t *testing.T) {
suite := NewMetadataCacheTestSuite()
testMetadata := &ProviderMetadata{
Issuer: "https://example.com",
AuthURL: "https://example.com/auth",
TokenURL: "https://example.com/token",
JWKSURL: "https://example.com/jwks",
}
server := suite.createTestServer(testMetadata, http.StatusOK, 0)
defer server.Close()
tests := []TableTestCase{
{
Name: "CacheHit_ValidMetadata",
Description: "Test cache hit with valid cached metadata",
Setup: func(t *testing.T) error {
suite.setup()
suite.testCache.metadata = testMetadata
suite.testCache.expiresAt = time.Now().Add(1 * time.Hour)
return nil
},
Teardown: func(t *testing.T) error {
suite.cleanup()
return nil
},
},
{
Name: "CacheMiss_ExpiredMetadata",
Description: "Test cache miss with expired metadata triggers refresh",
Setup: func(t *testing.T) error {
suite.setup()
suite.testCache.metadata = testMetadata
suite.testCache.expiresAt = time.Now().Add(-1 * time.Hour)
return nil
},
Teardown: func(t *testing.T) error {
suite.cleanup()
return nil
},
},
{
Name: "CacheMiss_NoMetadata",
Description: "Test cache miss when no metadata exists",
Setup: func(t *testing.T) error {
suite.setup()
return nil
},
Teardown: func(t *testing.T) error {
suite.cleanup()
return nil
},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
if test.Setup != nil {
if err := test.Setup(t); err != nil {
t.Fatalf("Setup failed: %v", err)
}
}
defer func() {
if test.Teardown != nil {
if err := test.Teardown(t); err != nil {
t.Errorf("Teardown failed: %v", err)
}
}
}()
result, err := suite.testCache.GetMetadata(server.URL, server.Client(), suite.logger)
switch test.Name {
case "CacheHit_ValidMetadata":
if err != nil {
t.Errorf("Expected no error for cache hit, got: %v", err)
}
if result != testMetadata {
t.Error("Expected cached metadata to be returned")
}
case "CacheMiss_ExpiredMetadata", "CacheMiss_NoMetadata":
if err != nil {
t.Errorf("Expected no error after refresh, got: %v", err)
}
if result == nil {
t.Error("Expected metadata to be fetched and returned")
}
}
})
}
}
// TestMetadataCache_ErrorHandling tests error scenarios
func TestMetadataCache_ErrorHandling(t *testing.T) {
suite := NewMetadataCacheTestSuite()
tests := []TableTestCase{
{
Name: "NetworkError_NoCache",
Description: "Test network error when no cached data exists",
Setup: func(t *testing.T) error {
suite.setup()
return nil
},
Teardown: func(t *testing.T) error {
suite.cleanup()
return nil
},
},
{
Name: "NetworkError_WithExpiredCache",
Description: "Test network error with expired cached data falls back to cache",
Setup: func(t *testing.T) error {
suite.setup()
suite.testCache.metadata = &ProviderMetadata{Issuer: "cached"}
suite.testCache.expiresAt = time.Now().Add(-1 * time.Minute)
return nil
},
Teardown: func(t *testing.T) error {
suite.cleanup()
return nil
},
},
{
Name: "HTTPError_404",
Description: "Test HTTP 404 error handling",
Setup: func(t *testing.T) error {
suite.setup()
return nil
},
Teardown: func(t *testing.T) error {
suite.cleanup()
return nil
},
},
{
Name: "HTTPError_500",
Description: "Test HTTP 500 error handling",
Setup: func(t *testing.T) error {
suite.setup()
return nil
},
Teardown: func(t *testing.T) error {
suite.cleanup()
return nil
},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
if test.Setup != nil {
if err := test.Setup(t); err != nil {
t.Fatalf("Setup failed: %v", err)
}
}
defer func() {
if test.Teardown != nil {
if err := test.Teardown(t); err != nil {
t.Errorf("Teardown failed: %v", err)
}
}
}()
var client *http.Client
var url string
switch test.Name {
case "NetworkError_NoCache", "NetworkError_WithExpiredCache":
client = suite.createErrorClient(fmt.Errorf("network error"))
url = "http://example.com"
case "HTTPError_404":
server := suite.createTestServer(nil, http.StatusNotFound, 0)
defer server.Close()
client = server.Client()
url = server.URL
case "HTTPError_500":
server := suite.createTestServer(nil, http.StatusInternalServerError, 0)
defer server.Close()
client = server.Client()
url = server.URL
}
result, err := suite.testCache.GetMetadata(url, client, suite.logger)
switch test.Name {
case "NetworkError_NoCache", "HTTPError_404", "HTTPError_500":
if err == nil {
t.Error("Expected error when no cached data and network/HTTP error")
}
if result != nil {
t.Error("Expected nil result on error without cached data")
}
case "NetworkError_WithExpiredCache":
if err != nil {
t.Errorf("Expected no error with fallback to cache, got: %v", err)
}
if result == nil || result.Issuer != "cached" {
t.Error("Expected fallback to cached data")
}
}
})
}
}
// TestMetadataCache_Concurrency tests concurrent access patterns
func TestMetadataCache_Concurrency(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeConcurrencyStress) {
return
}
suite := NewMetadataCacheTestSuite()
suite.setup()
defer suite.cleanup()
testMetadata := &ProviderMetadata{
Issuer: "https://example.com",
AuthURL: "https://example.com/auth",
TokenURL: "https://example.com/token",
}
server := suite.createTestServer(testMetadata, http.StatusOK, 10*time.Millisecond)
defer server.Close()
numGoroutines := config.AdjustConcurrencyParams(50)
numRequests := config.MaxIterations
if numRequests > 10 {
numRequests = 10
}
var wg sync.WaitGroup
results := make(chan *ProviderMetadata, numGoroutines*numRequests)
errors := make(chan error, numGoroutines*numRequests)
// Test concurrent GetMetadata calls
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < numRequests; j++ {
result, err := suite.testCache.GetMetadata(server.URL, server.Client(), suite.logger)
if err != nil {
errors <- err
} else {
results <- result
}
}
}()
}
wg.Wait()
close(results)
close(errors)
// Check for errors
for err := range errors {
t.Errorf("Unexpected error in concurrent test: %v", err)
}
// Check results consistency
var firstResult *ProviderMetadata
resultCount := 0
for result := range results {
resultCount++
if firstResult == nil {
firstResult = result
}
if result.Issuer != firstResult.Issuer {
t.Error("Inconsistent results from concurrent calls")
}
}
expectedResults := numGoroutines * numRequests
if resultCount != expectedResults {
t.Errorf("Expected %d results, got %d", expectedResults, resultCount)
}
}
// TestMetadataCache_AutoCleanup tests automatic cleanup functionality
func TestMetadataCache_AutoCleanup(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeExtended) {
return
}
suite := NewMetadataCacheTestSuite()
// Create cache with cleanup interval adjusted for test mode
cleanupInterval := config.GetCleanupInterval()
cache := &MetadataCache{
autoCleanupInterval: cleanupInterval,
logger: suite.logger,
wg: suite.mockWG,
stopChan: make(chan struct{}),
}
cache.startAutoCleanup()
defer cache.Close()
// Set expired metadata
cache.mutex.Lock()
cache.metadata = &ProviderMetadata{Issuer: "test"}
cache.expiresAt = time.Now().Add(-cleanupInterval)
cache.mutex.Unlock()
// Wait for auto cleanup (adjusted for config)
waitTime := cleanupInterval * 3
if waitTime > 500*time.Millisecond {
waitTime = 500 * time.Millisecond
}
time.Sleep(waitTime)
cache.mutex.RLock()
result := cache.metadata
cache.mutex.RUnlock()
if result != nil {
t.Error("Expected auto cleanup to clear expired metadata")
}
}
// TestMetadataCache_EdgeCases tests edge cases
func TestMetadataCache_EdgeCases(t *testing.T) {
suite := NewMetadataCacheTestSuite()
edgeCaseURLs := []string{
"", // Empty URL
"invalid-url", // Invalid URL format
"http://", // Incomplete URL
"https://nonexistent.domain.local", // Non-existent domain
"http://localhost:99999", // Invalid port
strings.Repeat("http://very-long-domain", 100), // Very long URL
}
for _, url := range edgeCaseURLs {
t.Run(fmt.Sprintf("EdgeCase_%s", url), func(t *testing.T) {
suite.setup()
defer suite.cleanup()
client := suite.createTimeoutClient(1 * time.Second)
result, err := suite.testCache.GetMetadata(url, client, suite.logger)
// Edge cases should generally fail
if err == nil && url == "" {
t.Error("Expected error for empty URL")
}
if result != nil && strings.HasPrefix(url, "http://nonexistent") {
t.Error("Expected nil result for non-existent domain")
}
})
}
}
// TestMetadataCache_MemoryLeaks tests for memory leaks
func TestMetadataCache_MemoryLeaks(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeLeakDetection) {
return
}
suite := NewMetadataCacheTestSuite()
leakTests := []MemoryLeakTestCase{
{
Name: "CreateAndDestroy_Cache",
Description: "Test creating and destroying cache instances",
Iterations: config.MaxIterations * 10, // Scale with config
MaxGoroutineGrowth: config.GoroutineGrowth,
MaxMemoryGrowthMB: config.MemoryThreshold,
GCBetweenRuns: true,
Operation: func() error {
cache := NewMetadataCacheWithLogger(&sync.WaitGroup{}, suite.logger)
defer cache.Close()
// Add some metadata
cache.metadata = &ProviderMetadata{Issuer: "test"}
cache.expiresAt = time.Now().Add(1 * time.Hour)
return nil
},
},
{
Name: "ConcurrentOperations",
Description: "Test concurrent cache operations",
Iterations: config.MaxIterations * 5,
MaxGoroutineGrowth: config.GoroutineGrowth * 2,
MaxMemoryGrowthMB: config.MemoryThreshold * 2,
GCBetweenRuns: true,
Operation: func() error {
cache := NewMetadataCacheWithLogger(&sync.WaitGroup{}, suite.logger)
defer cache.Close()
testMetadata := &ProviderMetadata{Issuer: "test"}
server := suite.createTestServer(testMetadata, http.StatusOK, 0)
defer server.Close()
var wg sync.WaitGroup
concurrency := config.AdjustConcurrencyParams(10)
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
cache.GetMetadata(server.URL, server.Client(), suite.logger)
}()
}
wg.Wait()
return nil
},
},
}
// Adjust all test cases based on configuration
for i := range leakTests {
config.AdjustMemoryLeakTestCase(&leakTests[i])
}
runner := NewTestSuiteRunner()
runner.RunMemoryLeakTests(t, leakTests)
}
// TestMetadataCache_Performance tests performance characteristics
func TestMetadataCache_Performance(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeExtended) {
return
}
suite := NewMetadataCacheTestSuite()
suite.setup()
defer suite.cleanup()
testMetadata := &ProviderMetadata{
Issuer: "https://example.com",
AuthURL: "https://example.com/auth",
TokenURL: "https://example.com/token",
}
server := suite.createTestServer(testMetadata, http.StatusOK, 1*time.Millisecond)
defer server.Close()
// Warm up the cache
suite.testCache.GetMetadata(server.URL, server.Client(), suite.logger)
// Test cache hit performance with adjusted iterations
iterations := config.MaxIterations * 100
if iterations > 1000 && config.QuickMode {
iterations = 100
}
for i := 0; i < iterations; i++ {
duration := suite.perfTest.Measure(func() {
suite.testCache.GetMetadata(server.URL, server.Client(), suite.logger)
})
// Cache hits should be fast (< 1ms typically)
if duration > 10*time.Millisecond {
t.Errorf("Cache hit too slow: %v (iteration %d)", duration, i)
}
}
avgTime := suite.perfTest.GetAverageTime()
t.Logf("Average cache hit time: %v over %d iterations", avgTime, iterations)
if avgTime > 5*time.Millisecond {
t.Errorf("Average cache hit time too slow: %v", avgTime)
}
}
// TestMetadataCache_ThreadSafety tests thread safety with race conditions
func TestMetadataCache_ThreadSafety(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeConcurrencyStress) {
return
}
suite := NewMetadataCacheTestSuite()
suite.setup()
defer suite.cleanup()
testMetadata := &ProviderMetadata{
Issuer: "https://example.com",
AuthURL: "https://example.com/auth",
TokenURL: "https://example.com/token",
}
server := suite.createTestServer(testMetadata, http.StatusOK, 50*time.Millisecond)
defer server.Close()
// Test with race detector enabled - adjust based on config
numGoroutines := config.AdjustConcurrencyParams(20)
numOperations := config.MaxIterations * 10
if numOperations > 100 && config.QuickMode {
numOperations = 10
}
var wg sync.WaitGroup
errChan := make(chan error, numGoroutines*numOperations)
// Run many concurrent operations
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < numOperations; j++ {
// Mix different operations to test race conditions
switch j % 4 {
case 0:
// Test GetMetadata
_, err := suite.testCache.GetMetadata(server.URL, server.Client(), suite.logger)
if err != nil {
errChan <- fmt.Errorf("goroutine %d GetMetadata failed: %v", id, err)
}
case 1:
// Test Cleanup
suite.testCache.Cleanup()
case 2:
// Test direct cache access (simulating expiration check)
suite.testCache.mutex.RLock()
_ = suite.testCache.metadata
suite.testCache.mutex.RUnlock()
case 3:
// Test setting expired metadata
suite.testCache.mutex.Lock()
if suite.testCache.metadata != nil {
suite.testCache.expiresAt = time.Now().Add(-1 * time.Minute)
}
suite.testCache.mutex.Unlock()
}
}
}(i)
}
wg.Wait()
close(errChan)
// Check for race condition errors
for err := range errChan {
t.Errorf("Thread safety test failed: %v", err)
}
}
// TestMetadataCache_TimeoutHandling tests various timeout scenarios
func TestMetadataCache_TimeoutHandling(t *testing.T) {
suite := NewMetadataCacheTestSuite()
tests := []struct {
name string
serverDelay time.Duration
clientTimeout time.Duration
expectTimeout bool
}{
{
name: "FastResponse",
serverDelay: 10 * time.Millisecond,
clientTimeout: 1 * time.Second,
expectTimeout: false,
},
{
name: "SlowResponse_WithinTimeout",
serverDelay: 100 * time.Millisecond,
clientTimeout: 200 * time.Millisecond,
expectTimeout: false,
},
{
name: "SlowResponse_ExceedsTimeout",
serverDelay: 200 * time.Millisecond,
clientTimeout: 100 * time.Millisecond,
expectTimeout: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
suite.setup()
defer suite.cleanup()
testMetadata := &ProviderMetadata{Issuer: "https://example.com"}
server := suite.createTestServer(testMetadata, http.StatusOK, test.serverDelay)
defer server.Close()
client := suite.createTimeoutClient(test.clientTimeout)
result, err := suite.testCache.GetMetadata(server.URL, client, suite.logger)
if test.expectTimeout {
if err == nil {
t.Error("Expected timeout error but got none")
}
if result != nil {
t.Error("Expected nil result on timeout")
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
if result == nil {
t.Error("Expected metadata result")
}
}
})
}
}
// TestMetadataCache_ErrorRecovery tests error recovery scenarios
func TestMetadataCache_ErrorRecovery(t *testing.T) {
suite := NewMetadataCacheTestSuite()
suite.setup()
defer suite.cleanup()
// Skip this test if ErrorRecoveryManager isn't available
// This tests the GetMetadataWithRecovery method when available
t.Run("ErrorRecovery_Integration", func(t *testing.T) {
// This is a placeholder test - in a real scenario, you would test
// the GetMetadataWithRecovery method if ErrorRecoveryManager is available
// For now, we'll just verify the basic GetMetadata functionality
testMetadata := &ProviderMetadata{Issuer: "https://example.com"}
server := suite.createTestServer(testMetadata, http.StatusOK, 0)
defer server.Close()
result, err := suite.testCache.GetMetadata(server.URL, server.Client(), suite.logger)
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
if result == nil {
t.Error("Expected metadata result")
}
})
}
// TestMetadataCache_Close tests proper resource cleanup
func TestMetadataCache_Close(t *testing.T) {
suite := NewMetadataCacheTestSuite()
t.Run("Close_MultipleCallsSafe", func(t *testing.T) {
cache := NewMetadataCacheWithLogger(&sync.WaitGroup{}, suite.logger)
// Close multiple times should be safe
cache.Close()
cache.Close()
cache.Close()
// Verify metadata is cleared
if cache.metadata != nil {
t.Error("Expected metadata to be cleared after close")
}
})
t.Run("Close_WithActiveOperations", func(t *testing.T) {
cache := NewMetadataCacheWithLogger(&sync.WaitGroup{}, suite.logger)
testMetadata := &ProviderMetadata{Issuer: "test"}
server := suite.createTestServer(testMetadata, http.StatusOK, 100*time.Millisecond)
defer server.Close()
// Start some operations
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
cache.GetMetadata(server.URL, server.Client(), suite.logger)
}()
}
// Close while operations are running
go func() {
time.Sleep(50 * time.Millisecond)
cache.Close()
}()
wg.Wait()
// Wait a bit more to ensure Close() completes
time.Sleep(10 * time.Millisecond)
// Should not panic or cause issues - main test is that it doesn't deadlock
// Note: metadata might still be present if operations completed successfully before close
// The main goal is to test that Close() doesn't cause deadlocks or crashes
})
}
// Benchmark tests for performance analysis
func BenchmarkMetadataCache_CacheHit(b *testing.B) {
suite := NewMetadataCacheTestSuite()
suite.setup()
defer suite.cleanup()
// Set up cached data
suite.testCache.metadata = &ProviderMetadata{Issuer: "test"}
suite.testCache.expiresAt = time.Now().Add(1 * time.Hour)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
suite.testCache.GetMetadata("http://example.com", http.DefaultClient, suite.logger)
}
})
}
func BenchmarkMetadataCache_ConcurrentAccess(b *testing.B) {
suite := NewMetadataCacheTestSuite()
suite.setup()
defer suite.cleanup()
testMetadata := &ProviderMetadata{Issuer: "test"}
server := suite.createTestServer(testMetadata, http.StatusOK, 0)
defer server.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
suite.testCache.GetMetadata(server.URL, server.Client(), suite.logger)
}
})
}
+5
View File
@@ -291,6 +291,11 @@ func (sm *SessionManager) PeriodicChunkCleanup() {
}
}
// Cleanup expired sessions in chunk manager to prevent memory leaks
if sm.chunkManager != nil {
sm.chunkManager.CleanupExpiredSessions()
}
poolCleaned := 0
for i := 0; i < 10; i++ {
if poolSession := sm.sessionPool.Get(); poolSession != nil {
+91 -2
View File
@@ -72,6 +72,18 @@ type TokenRetrievalResult struct {
type ChunkManager struct {
logger *Logger
mutex *sync.RWMutex
// sessionMap provides bounded session storage to prevent memory leaks
sessionMap map[string]*SessionEntry
maxSessions int
sessionTTL time.Duration
lastCleanup time.Time
}
// SessionEntry represents a session with expiration tracking
type SessionEntry struct {
Session *sessions.Session
ExpiresAt time.Time
LastUsed time.Time
}
// NewChunkManager creates a new ChunkManager instance with proper initialization.
@@ -87,8 +99,12 @@ func NewChunkManager(logger *Logger) *ChunkManager {
}
return &ChunkManager{
logger: logger,
mutex: &sync.RWMutex{},
logger: logger,
mutex: &sync.RWMutex{},
sessionMap: make(map[string]*SessionEntry),
maxSessions: 1000, // Prevent unbounded growth
sessionTTL: 24 * time.Hour, // Auto-expire old sessions
lastCleanup: time.Now(),
}
}
@@ -952,3 +968,76 @@ func (cm *ChunkManager) extractJWTIssuedAt(token string) (*time.Time, error) {
return &iatTime, nil
}
// CleanupExpiredSessions removes expired sessions to prevent memory leaks.
// This is called periodically to maintain memory efficiency and prevent unbounded growth.
func (cm *ChunkManager) CleanupExpiredSessions() {
cm.mutex.Lock()
defer cm.mutex.Unlock()
// Only cleanup if enough time has passed
if time.Since(cm.lastCleanup) < time.Hour {
return
}
now := time.Now()
expiredKeys := make([]string, 0)
// Find expired sessions
for key, entry := range cm.sessionMap {
if now.After(entry.ExpiresAt) || now.Sub(entry.LastUsed) > cm.sessionTTL {
expiredKeys = append(expiredKeys, key)
}
}
// Remove expired sessions
for _, key := range expiredKeys {
delete(cm.sessionMap, key)
}
cm.lastCleanup = now
if len(expiredKeys) > 0 {
cm.logger.Debug("Cleaned up %d expired sessions", len(expiredKeys))
}
// Enforce max sessions limit
if len(cm.sessionMap) > cm.maxSessions {
cm.enforceSessionLimit()
}
}
// enforceSessionLimit removes oldest sessions when limit is exceeded
func (cm *ChunkManager) enforceSessionLimit() {
if len(cm.sessionMap) <= cm.maxSessions {
return
}
// Find oldest sessions to remove
type sessionAge struct {
key string
lastUsed time.Time
}
sessions := make([]sessionAge, 0, len(cm.sessionMap))
for key, entry := range cm.sessionMap {
sessions = append(sessions, sessionAge{key: key, lastUsed: entry.LastUsed})
}
// Sort by last used time (oldest first)
for i := 0; i < len(sessions)-1; i++ {
for j := i + 1; j < len(sessions); j++ {
if sessions[i].lastUsed.After(sessions[j].lastUsed) {
sessions[i], sessions[j] = sessions[j], sessions[i]
}
}
}
// Remove excess sessions
excessCount := len(cm.sessionMap) - cm.maxSessions
for i := 0; i < excessCount && i < len(sessions); i++ {
delete(cm.sessionMap, sessions[i].key)
}
cm.logger.Info("Enforced session limit: removed %d excess sessions", excessCount)
}
+1066 -728
View File
File diff suppressed because it is too large Load Diff
+310
View File
@@ -0,0 +1,310 @@
package traefikoidc
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// SimpleMemoryTestSuite provides focused memory verification tests using unified infrastructure
type SimpleMemoryTestSuite struct {
runner *TestSuiteRunner
factory *TestDataFactory
edgeGen *EdgeCaseGenerator
perfTest *PerformanceTestHelper
logger *Logger
}
// NewSimpleMemoryTestSuite creates a new test suite for simple memory verification
func NewSimpleMemoryTestSuite() *SimpleMemoryTestSuite {
return &SimpleMemoryTestSuite{
runner: NewTestSuiteRunner(),
factory: NewTestDataFactory(),
edgeGen: NewEdgeCaseGenerator(),
perfTest: NewPerformanceTestHelper(),
logger: GetSingletonNoOpLogger(),
}
}
// TestSimpleCacheOperations verifies basic cache operations using table-driven tests
func TestSimpleCacheOperations(t *testing.T) {
config := GetTestConfig()
if config.ShouldSkipTest(t, TestTypeQuick) {
return
}
suite := NewSimpleMemoryTestSuite()
tests := []MemoryLeakTestCase{
{
Name: "Basic cache creation and cleanup",
Description: "Verify cache can be created, used briefly, and cleaned up without leaks",
Operation: func() error {
cache := NewOptimizedCache()
if cache == nil {
return fmt.Errorf("cache creation failed")
}
// Use the cache briefly
cache.Set("test", "value", time.Minute)
val, found := cache.Get("test")
if !found {
return fmt.Errorf("cache get failed: key not found")
}
if val != "value" {
return fmt.Errorf("cache get failed: expected 'value', got %v", val)
}
// Close immediately
cache.Close()
// Brief wait for cleanup
time.Sleep(50 * time.Millisecond)
return nil
},
Iterations: 3,
MaxGoroutineGrowth: 2, // Allow minimal framework overhead
MaxMemoryGrowthMB: 0.5, // Very strict for simple operations
GCBetweenRuns: true,
Timeout: 5 * time.Second,
},
{
Name: "Cache with multiple quick operations",
Description: "Test cache with rapid set/get operations",
Operation: func() error {
cache := NewOptimizedCache()
defer cache.Close()
// Perform multiple quick operations
for i := 0; i < 10; i++ {
key := fmt.Sprintf("key-%d", i)
value := fmt.Sprintf("value-%d", i)
cache.Set(key, value, time.Minute)
val, found := cache.Get(key)
if !found {
return fmt.Errorf("cache get failed for key %s", key)
}
if val != value {
return fmt.Errorf("cache value mismatch for key %s: expected %s, got %v", key, value, val)
}
}
return nil
},
Iterations: 5,
MaxGoroutineGrowth: 2,
MaxMemoryGrowthMB: 1.0,
GCBetweenRuns: true,
Timeout: 8 * time.Second,
},
{
Name: "Cache with edge case values",
Description: "Test cache with edge case string values",
Operation: func() error {
cache := NewOptimizedCache()
defer cache.Close()
// Test with edge case strings from the generator
edgeCases := suite.edgeGen.GenerateStringEdgeCases()
for i, value := range edgeCases[:5] { // Limit to first 5 for simplicity
key := fmt.Sprintf("edge-key-%d", i)
cache.Set(key, value, time.Minute)
val, found := cache.Get(key)
if !found {
return fmt.Errorf("cache get failed for edge case key %s", key)
}
if val != value {
return fmt.Errorf("cache value mismatch for edge case key %s", key)
}
}
return nil
},
Iterations: 3,
MaxGoroutineGrowth: 2,
MaxMemoryGrowthMB: 1.0,
GCBetweenRuns: true,
Timeout: 10 * time.Second,
},
}
suite.runner.RunMemoryLeakTests(t, tests)
}
// TestChunkManagerInitialization verifies chunk manager initialization using table-driven tests
func TestChunkManagerInitialization(t *testing.T) {
suite := NewSimpleMemoryTestSuite()
tests := []TableTestCase{
{
Name: "Basic chunk manager creation",
Description: "Verify chunk manager can be created with proper defaults",
Input: nil, // No specific input needed
Expected: map[string]interface{}{
"maxSessions": 1000,
"sessionTTL": 24 * time.Hour,
"notNil": true,
},
Timeout: 5 * time.Second,
Setup: func(t *testing.T) error {
// No setup needed
return nil
},
Teardown: func(t *testing.T) error {
// No teardown needed
return nil
},
},
{
Name: "Chunk manager with logger verification",
Description: "Verify chunk manager works correctly with singleton logger",
Input: "singleton_logger",
Expected: map[string]interface{}{
"hasLogger": true,
"maxSessions": 1000,
"sessionTTL": 24 * time.Hour,
},
Timeout: 5 * time.Second,
},
}
// Custom test execution since TableTestCase doesn't have built-in logic
for _, test := range tests {
test := test // Capture loop variable
t.Run(test.Name, func(t *testing.T) {
if test.Setup != nil {
require.NoError(t, test.Setup(t))
}
if test.Teardown != nil {
defer func() {
require.NoError(t, test.Teardown(t))
}()
}
// Execute the actual test logic
logger := suite.logger
cm := NewChunkManager(logger)
// Verify basic properties
assert.NotNil(t, cm, "Chunk manager should not be nil")
assert.NotNil(t, cm.sessionMap, "Session map should not be nil")
// Verify expected values from test case
expected := test.Expected.(map[string]interface{})
assert.Equal(t, expected["maxSessions"], cm.maxSessions, "Max sessions should match expected value")
assert.Equal(t, expected["sessionTTL"], cm.sessionTTL, "Session TTL should match expected value")
if hasLogger, exists := expected["hasLogger"]; exists && hasLogger.(bool) {
assert.NotNil(t, logger, "Logger should not be nil when expected")
}
})
}
}
// TestSimpleMemoryLeakDetection provides memory leak detection using the infrastructure
func TestSimpleMemoryLeakDetection(t *testing.T) {
suite := NewSimpleMemoryTestSuite()
tests := []MemoryLeakTestCase{
{
Name: "Repeated cache creation and destruction",
Description: "Verify no leaks from repeated cache lifecycle",
Operation: func() error {
cache := NewOptimizedCache()
cache.Set("test", "value", 100*time.Millisecond)
_, _ = cache.Get("test")
cache.Close()
// Small delay to allow cleanup
time.Sleep(20 * time.Millisecond)
return nil
},
Iterations: 20,
MaxGoroutineGrowth: 3, // Slightly more tolerance for repeated operations
MaxMemoryGrowthMB: 2.0, // Allow for some accumulation over iterations
GCBetweenRuns: true,
Timeout: 15 * time.Second,
},
{
Name: "Chunk manager creation cycles",
Description: "Verify no leaks from chunk manager creation",
Operation: func() error {
logger := suite.logger
cm := NewChunkManager(logger)
// Verify it was created properly
if cm == nil {
return fmt.Errorf("chunk manager creation failed")
}
if cm.sessionMap == nil {
return fmt.Errorf("chunk manager session map not initialized")
}
return nil
},
Iterations: 15,
MaxGoroutineGrowth: 2,
MaxMemoryGrowthMB: 1.0,
GCBetweenRuns: true,
Timeout: 10 * time.Second,
},
}
suite.runner.RunMemoryLeakTests(t, tests)
}
// BenchmarkSimpleCacheOperations provides basic performance benchmarks
func BenchmarkSimpleCacheOperations(b *testing.B) {
cache := NewOptimizedCache()
defer cache.Close()
b.Run("Set operations", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("bench-key-%d", i)
cache.Set(key, "benchmark-value", time.Minute)
}
})
b.Run("Get operations", func(b *testing.B) {
// Pre-populate cache
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("bench-key-%d", i)
cache.Set(key, "benchmark-value", time.Minute)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("bench-key-%d", i%1000)
cache.Get(key)
}
})
b.Run("Set and Get combined", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("bench-key-%d", i)
cache.Set(key, "benchmark-value", time.Minute)
cache.Get(key)
}
})
}
// BenchmarkChunkManagerCreation provides benchmarks for chunk manager operations
func BenchmarkChunkManagerCreation(b *testing.B) {
logger := GetSingletonNoOpLogger()
b.Run("Chunk manager creation", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
cm := NewChunkManager(logger)
_ = cm // Avoid compiler optimization
}
})
}
+287
View File
@@ -0,0 +1,287 @@
package traefikoidc
import (
"os"
"strconv"
"testing"
"time"
)
// TestConfig manages test execution configuration and performance settings
type TestConfig struct {
// Test execution modes
ExtendedTests bool // Run extended/stress tests
LongTests bool // Run long-running performance tests
QuickMode bool // Quick smoke tests only
// Performance settings
MaxConcurrency int // Maximum concurrent operations
MaxIterations int // Maximum test iterations
DefaultTimeout time.Duration // Default test timeout
MemoryThreshold float64 // Memory growth threshold in MB
GoroutineGrowth int // Acceptable goroutine growth
// Cache settings for tests
CacheSize int // Default cache size for tests
CleanupInterval time.Duration // Cleanup interval for tests
// Environment-specific overrides
MemoryStressTest bool // Enable memory stress tests
ConcurrencyTest bool // Enable high concurrency tests
LeakDetection bool // Enable memory leak detection
}
// NewTestConfig creates a test configuration based on flags and environment
func NewTestConfig() *TestConfig {
config := &TestConfig{
// Default quick mode settings - very conservative for 30s target
ExtendedTests: false,
LongTests: false,
QuickMode: true,
MaxConcurrency: 2, // Reduced for quick mode
MaxIterations: 1, // Minimal iterations for quick smoke tests
DefaultTimeout: 5 * time.Second, // Shorter timeout
MemoryThreshold: 1.0, // Strict memory limit
GoroutineGrowth: 1, // Very strict goroutine limit
CacheSize: 10, // Small cache size
CleanupInterval: 50 * time.Millisecond, // Faster cleanup
MemoryStressTest: false,
ConcurrencyTest: false,
LeakDetection: false, // Disable by default in quick mode for speed
}
// Check for extended test flag
if os.Getenv("RUN_EXTENDED_TESTS") == "1" || os.Getenv("RUN_EXTENDED_TESTS") == "true" {
config.EnableExtendedTests()
}
// Check for long test flag
if os.Getenv("RUN_LONG_TESTS") == "1" || os.Getenv("RUN_LONG_TESTS") == "true" {
config.EnableLongTests()
}
// Check for stress tests
if os.Getenv("RUN_STRESS_TESTS") == "1" || os.Getenv("RUN_STRESS_TESTS") == "true" {
config.EnableStressTests()
}
// Check for memory leak detection override
if os.Getenv("DISABLE_LEAK_DETECTION") == "1" || os.Getenv("DISABLE_LEAK_DETECTION") == "true" {
config.LeakDetection = false
}
// Parse custom concurrency limit
if concStr := os.Getenv("TEST_MAX_CONCURRENCY"); concStr != "" {
if conc, err := strconv.Atoi(concStr); err == nil && conc > 0 {
config.MaxConcurrency = conc
}
}
// Parse custom iteration limit
if iterStr := os.Getenv("TEST_MAX_ITERATIONS"); iterStr != "" {
if iter, err := strconv.Atoi(iterStr); err == nil && iter > 0 {
config.MaxIterations = iter
}
}
// Parse memory threshold
if memStr := os.Getenv("TEST_MEMORY_THRESHOLD_MB"); memStr != "" {
if mem, err := strconv.ParseFloat(memStr, 64); err == nil && mem > 0 {
config.MemoryThreshold = mem
}
}
return config
}
// EnableExtendedTests switches to extended test mode
func (c *TestConfig) EnableExtendedTests() {
c.ExtendedTests = true
c.QuickMode = false
c.MaxConcurrency = 20
c.MaxIterations = 10
c.DefaultTimeout = 30 * time.Second
c.MemoryThreshold = 10.0
c.GoroutineGrowth = 5
c.CacheSize = 200
c.CleanupInterval = 50 * time.Millisecond
c.ConcurrencyTest = true
}
// EnableLongTests switches to long-running test mode
func (c *TestConfig) EnableLongTests() {
c.LongTests = true
c.QuickMode = false
c.MaxConcurrency = 50
c.MaxIterations = 100
c.DefaultTimeout = 60 * time.Second
c.MemoryThreshold = 50.0
c.GoroutineGrowth = 10
c.CacheSize = 1000
c.CleanupInterval = 10 * time.Millisecond
c.ConcurrencyTest = true
c.MemoryStressTest = true
}
// EnableStressTests switches to stress test mode
func (c *TestConfig) EnableStressTests() {
c.ExtendedTests = true
c.LongTests = true
c.QuickMode = false
c.MaxConcurrency = 100
c.MaxIterations = 500
c.DefaultTimeout = 120 * time.Second
c.MemoryThreshold = 100.0
c.GoroutineGrowth = 20
c.CacheSize = 2000
c.CleanupInterval = 5 * time.Millisecond
c.ConcurrencyTest = true
c.MemoryStressTest = true
}
// ShouldSkipTest determines if a test should be skipped based on config
func (c *TestConfig) ShouldSkipTest(t *testing.T, testType TestType) bool {
// Always respect testing.Short() - skip everything except basic quick tests
if testing.Short() {
switch testType {
case TestTypeQuick:
return false // Allow quick tests
case TestTypeExtended, TestTypeLong, TestTypeMemoryStress, TestTypeConcurrencyStress:
t.Skip("Skipping extended test in short mode")
return true
case TestTypeLeakDetection:
// Skip leak detection in short mode unless explicitly enabled
if !c.LeakDetection {
t.Skip("Skipping leak detection test in short mode (use RUN_EXTENDED_TESTS=1 to enable)")
return true
}
}
}
// Check specific test type flags
switch testType {
case TestTypeExtended:
if !c.ExtendedTests {
t.Skip("Skipping extended test (use RUN_EXTENDED_TESTS=1 to enable)")
return true
}
case TestTypeLong:
if !c.LongTests {
t.Skip("Skipping long test (use RUN_LONG_TESTS=1 to enable)")
return true
}
case TestTypeMemoryStress:
if !c.MemoryStressTest {
t.Skip("Skipping memory stress test (use RUN_STRESS_TESTS=1 to enable)")
return true
}
case TestTypeConcurrencyStress:
if !c.ConcurrencyTest {
t.Skip("Skipping concurrency stress test (use RUN_EXTENDED_TESTS=1 to enable)")
return true
}
case TestTypeLeakDetection:
if !c.LeakDetection {
t.Skip("Skipping leak detection test (DISABLE_LEAK_DETECTION=1 set)")
return true
}
}
return false
}
// AdjustMemoryLeakTestCase adjusts a memory leak test case based on configuration
func (c *TestConfig) AdjustMemoryLeakTestCase(testCase *MemoryLeakTestCase) {
// Adjust iterations
if testCase.Iterations > c.MaxIterations {
testCase.Iterations = c.MaxIterations
}
// Ensure minimum of 1 iteration
if testCase.Iterations < 1 {
testCase.Iterations = 1
}
// Adjust memory threshold
if testCase.MaxMemoryGrowthMB > c.MemoryThreshold && c.QuickMode {
testCase.MaxMemoryGrowthMB = c.MemoryThreshold
}
// Adjust goroutine growth
if testCase.MaxGoroutineGrowth > c.GoroutineGrowth && c.QuickMode {
testCase.MaxGoroutineGrowth = c.GoroutineGrowth
}
// Adjust timeout
if testCase.Timeout > c.DefaultTimeout && c.QuickMode {
testCase.Timeout = c.DefaultTimeout
} else if testCase.Timeout == 0 {
testCase.Timeout = c.DefaultTimeout
}
}
// AdjustConcurrencyParams adjusts concurrency parameters for tests
func (c *TestConfig) AdjustConcurrencyParams(requested int) int {
if requested > c.MaxConcurrency {
return c.MaxConcurrency
}
return requested
}
// GetCacheSize returns appropriate cache size for tests
func (c *TestConfig) GetCacheSize() int {
return c.CacheSize
}
// GetCleanupInterval returns appropriate cleanup interval for tests
func (c *TestConfig) GetCleanupInterval() time.Duration {
return c.CleanupInterval
}
// TestType represents different categories of tests
type TestType int
const (
TestTypeQuick TestType = iota
TestTypeExtended
TestTypeLong
TestTypeMemoryStress
TestTypeConcurrencyStress
TestTypeLeakDetection
)
// String returns string representation of test type
func (tt TestType) String() string {
switch tt {
case TestTypeQuick:
return "quick"
case TestTypeExtended:
return "extended"
case TestTypeLong:
return "long"
case TestTypeMemoryStress:
return "memory-stress"
case TestTypeConcurrencyStress:
return "concurrency-stress"
case TestTypeLeakDetection:
return "leak-detection"
default:
return "unknown"
}
}
// Global test configuration instance
var globalTestConfig *TestConfig
// GetTestConfig returns the global test configuration
func GetTestConfig() *TestConfig {
if globalTestConfig == nil {
globalTestConfig = NewTestConfig()
}
return globalTestConfig
}
// SetTestConfig sets the global test configuration (useful for testing)
func SetTestConfig(config *TestConfig) {
globalTestConfig = config
}
+789
View File
@@ -0,0 +1,789 @@
package traefikoidc
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
)
// UnifiedMockSession provides a comprehensive mock for the Session interface
type UnifiedMockSession struct {
mu sync.RWMutex
data map[string]interface{}
callCounts map[string]int64
errors map[string]error
delays map[string]time.Duration
destroyed bool
destroyCount int64
}
// NewUnifiedMockSession creates a new mock session with default behavior
func NewUnifiedMockSession() *UnifiedMockSession {
return &UnifiedMockSession{
data: make(map[string]interface{}),
callCounts: make(map[string]int64),
errors: make(map[string]error),
delays: make(map[string]time.Duration),
}
}
// SetError configures the mock to return an error for specific method calls
func (m *UnifiedMockSession) SetError(method string, err error) {
m.mu.Lock()
defer m.mu.Unlock()
m.errors[method] = err
}
// SetDelay configures the mock to add delay for specific method calls
func (m *UnifiedMockSession) SetDelay(method string, delay time.Duration) {
m.mu.Lock()
defer m.mu.Unlock()
m.delays[method] = delay
}
// GetCallCount returns the number of times a method was called
func (m *UnifiedMockSession) GetCallCount(method string) int64 {
m.mu.RLock()
defer m.mu.RUnlock()
return m.callCounts[method]
}
func (m *UnifiedMockSession) recordCall(method string) {
m.mu.Lock()
m.callCounts[method]++
m.mu.Unlock()
}
func (m *UnifiedMockSession) checkError(method string) error {
m.mu.RLock()
defer m.mu.RUnlock()
if err, exists := m.errors[method]; exists {
return err
}
return nil
}
func (m *UnifiedMockSession) applyDelay(method string) {
m.mu.RLock()
delay, exists := m.delays[method]
m.mu.RUnlock()
if exists && delay > 0 {
time.Sleep(delay)
}
}
// Session interface implementation
func (m *UnifiedMockSession) Get(key string) (interface{}, bool) {
m.recordCall("Get")
if err := m.checkError("Get"); err != nil {
return nil, false
}
m.applyDelay("Get")
m.mu.RLock()
defer m.mu.RUnlock()
val, exists := m.data[key]
return val, exists
}
func (m *UnifiedMockSession) Set(key string, value interface{}) {
m.recordCall("Set")
if err := m.checkError("Set"); err != nil {
return
}
m.applyDelay("Set")
m.mu.Lock()
defer m.mu.Unlock()
m.data[key] = value
}
func (m *UnifiedMockSession) Delete(key string) {
m.recordCall("Delete")
if err := m.checkError("Delete"); err != nil {
return
}
m.applyDelay("Delete")
m.mu.Lock()
defer m.mu.Unlock()
delete(m.data, key)
}
func (m *UnifiedMockSession) Destroy() error {
m.recordCall("Destroy")
if err := m.checkError("Destroy"); err != nil {
return err
}
m.applyDelay("Destroy")
m.mu.Lock()
defer m.mu.Unlock()
if m.destroyed {
return fmt.Errorf("session already destroyed")
}
m.destroyed = true
atomic.AddInt64(&m.destroyCount, 1)
// Clear data to help with memory leak detection
for k := range m.data {
delete(m.data, k)
}
return nil
}
func (m *UnifiedMockSession) IsDestroyed() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.destroyed
}
func (m *UnifiedMockSession) GetDestroyCount() int64 {
return atomic.LoadInt64(&m.destroyCount)
}
// UnifiedMockTokenVerifier provides a comprehensive mock for token verification
type UnifiedMockTokenVerifier struct {
mu sync.RWMutex
validTokens map[string]bool
tokenMetadata map[string]map[string]interface{}
callCounts map[string]int64
errors map[string]error
delays map[string]time.Duration
verificationFunc func(string) error
}
// NewUnifiedMockTokenVerifier creates a new mock token verifier
func NewUnifiedMockTokenVerifier() *UnifiedMockTokenVerifier {
return &UnifiedMockTokenVerifier{
validTokens: make(map[string]bool),
tokenMetadata: make(map[string]map[string]interface{}),
callCounts: make(map[string]int64),
errors: make(map[string]error),
delays: make(map[string]time.Duration),
}
}
// SetTokenValid configures whether a token should be considered valid
func (m *UnifiedMockTokenVerifier) SetTokenValid(token string, valid bool) {
m.mu.Lock()
defer m.mu.Unlock()
m.validTokens[token] = valid
}
// SetTokenMetadata configures metadata for a token
func (m *UnifiedMockTokenVerifier) SetTokenMetadata(token string, metadata map[string]interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
m.tokenMetadata[token] = metadata
}
// SetVerificationFunc allows custom verification logic
func (m *UnifiedMockTokenVerifier) SetVerificationFunc(fn func(string) error) {
m.mu.Lock()
defer m.mu.Unlock()
m.verificationFunc = fn
}
// SetError configures the mock to return an error for specific method calls
func (m *UnifiedMockTokenVerifier) SetError(method string, err error) {
m.mu.Lock()
defer m.mu.Unlock()
m.errors[method] = err
}
// GetCallCount returns the number of times a method was called
func (m *UnifiedMockTokenVerifier) GetCallCount(method string) int64 {
m.mu.RLock()
defer m.mu.RUnlock()
return m.callCounts[method]
}
func (m *UnifiedMockTokenVerifier) recordCall(method string) {
m.mu.Lock()
m.callCounts[method]++
m.mu.Unlock()
}
func (m *UnifiedMockTokenVerifier) VerifyToken(token string) error {
m.recordCall("VerifyToken")
if err := m.errors["VerifyToken"]; err != nil {
return err
}
if delay := m.delays["VerifyToken"]; delay > 0 {
time.Sleep(delay)
}
m.mu.RLock()
defer m.mu.RUnlock()
if m.verificationFunc != nil {
return m.verificationFunc(token)
}
if valid, exists := m.validTokens[token]; exists && valid {
return nil
}
return fmt.Errorf("invalid token")
}
// UnifiedMockTokenCache provides a comprehensive mock for token caching
type UnifiedMockTokenCache struct {
mu sync.RWMutex
cache map[string]TestCacheEntry
callCounts map[string]int64
errors map[string]error
delays map[string]time.Duration
hitRate float64
}
// TestCacheEntry represents a cached token entry for testing
type TestCacheEntry struct {
Token string
ExpiresAt time.Time
Metadata map[string]interface{}
}
// NewUnifiedMockTokenCache creates a new mock token cache
func NewUnifiedMockTokenCache() *UnifiedMockTokenCache {
return &UnifiedMockTokenCache{
cache: make(map[string]TestCacheEntry),
callCounts: make(map[string]int64),
errors: make(map[string]error),
delays: make(map[string]time.Duration),
hitRate: 1.0, // Default to 100% hit rate
}
}
// SetError configures the mock to return an error for specific method calls
func (m *UnifiedMockTokenCache) SetError(method string, err error) {
m.mu.Lock()
defer m.mu.Unlock()
m.errors[method] = err
}
// SetHitRate configures the cache hit rate (0.0 to 1.0)
func (m *UnifiedMockTokenCache) SetHitRate(rate float64) {
m.mu.Lock()
defer m.mu.Unlock()
m.hitRate = rate
}
// GetCallCount returns the number of times a method was called
func (m *UnifiedMockTokenCache) GetCallCount(method string) int64 {
m.mu.RLock()
defer m.mu.RUnlock()
return m.callCounts[method]
}
func (m *UnifiedMockTokenCache) recordCall(method string) {
m.mu.Lock()
m.callCounts[method]++
m.mu.Unlock()
}
func (m *UnifiedMockTokenCache) Get(key string) (string, bool) {
m.recordCall("Get")
if err := m.errors["Get"]; err != nil {
return "", false
}
if delay := m.delays["Get"]; delay > 0 {
time.Sleep(delay)
}
m.mu.RLock()
defer m.mu.RUnlock()
// Simulate cache miss based on hit rate
if m.hitRate < 1.0 {
// Simple random check (in real tests, you might want deterministic behavior)
if float64(len(key)%100)/100.0 > m.hitRate {
return "", false
}
}
entry, exists := m.cache[key]
if !exists {
return "", false
}
if time.Now().After(entry.ExpiresAt) {
return "", false
}
return entry.Token, true
}
func (m *UnifiedMockTokenCache) Set(key, token string, expiry time.Time) {
m.recordCall("Set")
if delay := m.delays["Set"]; delay > 0 {
time.Sleep(delay)
}
m.mu.Lock()
defer m.mu.Unlock()
m.cache[key] = TestCacheEntry{
Token: token,
ExpiresAt: expiry,
Metadata: make(map[string]interface{}),
}
}
func (m *UnifiedMockTokenCache) Delete(key string) {
m.recordCall("Delete")
m.mu.Lock()
defer m.mu.Unlock()
delete(m.cache, key)
}
func (m *UnifiedMockTokenCache) Clear() {
m.recordCall("Clear")
m.mu.Lock()
defer m.mu.Unlock()
for k := range m.cache {
delete(m.cache, k)
}
}
// TableTestCase represents a standardized test case structure
type TableTestCase struct {
Name string
Description string
Input interface{}
Expected interface{}
ExpectedError error
Setup func(*testing.T) error
Teardown func(*testing.T) error
Timeout time.Duration
SkipReason string
Tags []string
Parallel bool
}
// MemoryLeakTestCase represents a test case specifically for memory leak detection
type MemoryLeakTestCase struct {
Name string
Description string
Operation func() error
Iterations int
MaxGoroutineGrowth int
MaxMemoryGrowthMB float64
Setup func() error
Teardown func() error
GCBetweenRuns bool
Timeout time.Duration
}
// TestSuiteRunner provides utilities for running table-driven tests
type TestSuiteRunner struct {
parallelTests bool
timeout time.Duration
beforeEach func(*testing.T)
afterEach func(*testing.T)
}
// NewTestSuiteRunner creates a new test suite runner
func NewTestSuiteRunner() *TestSuiteRunner {
return &TestSuiteRunner{
timeout: 30 * time.Second,
}
}
// SetParallel enables or disables parallel test execution
func (r *TestSuiteRunner) SetParallel(parallel bool) {
r.parallelTests = parallel
}
// SetTimeout sets the default timeout for tests
func (r *TestSuiteRunner) SetTimeout(timeout time.Duration) {
r.timeout = timeout
}
// SetBeforeEach sets a function to run before each test
func (r *TestSuiteRunner) SetBeforeEach(fn func(*testing.T)) {
r.beforeEach = fn
}
// SetAfterEach sets a function to run after each test
func (r *TestSuiteRunner) SetAfterEach(fn func(*testing.T)) {
r.afterEach = fn
}
// RunTests executes a table of test cases
func (r *TestSuiteRunner) RunTests(t *testing.T, tests []TableTestCase) {
for _, test := range tests {
test := test // Capture loop variable
if test.SkipReason != "" {
t.Skip(test.SkipReason)
continue
}
testFunc := func(t *testing.T) {
if r.beforeEach != nil {
r.beforeEach(t)
}
if r.afterEach != nil {
defer r.afterEach(t)
}
timeout := test.Timeout
if timeout == 0 {
timeout = r.timeout
}
done := make(chan bool, 1)
var testErr error
go func() {
defer func() {
if r := recover(); r != nil {
testErr = fmt.Errorf("test panicked: %v", r)
}
done <- true
}()
if test.Setup != nil {
if err := test.Setup(t); err != nil {
testErr = fmt.Errorf("setup failed: %w", err)
return
}
}
if test.Teardown != nil {
defer func() {
if err := test.Teardown(t); err != nil {
t.Errorf("teardown failed: %v", err)
}
}()
}
// Execute the actual test logic here
// This would be filled in by specific test implementations
}()
select {
case <-done:
if testErr != nil {
t.Error(testErr)
}
case <-time.After(timeout):
t.Errorf("test timed out after %v", timeout)
}
}
if test.Parallel || r.parallelTests {
t.Run(test.Name, func(t *testing.T) {
t.Parallel()
testFunc(t)
})
} else {
t.Run(test.Name, testFunc)
}
}
}
// RunMemoryLeakTests executes memory leak test cases
func (r *TestSuiteRunner) RunMemoryLeakTests(t *testing.T, tests []MemoryLeakTestCase) {
for _, test := range tests {
test := test // Capture loop variable
t.Run(test.Name, func(t *testing.T) {
if test.Setup != nil {
if err := test.Setup(); err != nil {
t.Fatalf("setup failed: %v", err)
}
}
if test.Teardown != nil {
defer func() {
if err := test.Teardown(); err != nil {
t.Errorf("teardown failed: %v", err)
}
}()
}
// Record initial state
runtime.GC()
initialGoroutines := runtime.NumGoroutine()
var initialMem runtime.MemStats
runtime.ReadMemStats(&initialMem)
// Run the operation multiple times
for i := 0; i < test.Iterations; i++ {
if test.Operation != nil {
if err := test.Operation(); err != nil {
t.Errorf("iteration %d failed: %v", i, err)
return
}
}
if test.GCBetweenRuns {
runtime.GC()
}
}
// Force garbage collection and check final state
runtime.GC()
runtime.GC() // Double GC to ensure cleanup
finalGoroutines := runtime.NumGoroutine()
var finalMem runtime.MemStats
runtime.ReadMemStats(&finalMem)
// Check goroutine growth
goroutineGrowth := finalGoroutines - initialGoroutines
if test.MaxGoroutineGrowth >= 0 && goroutineGrowth > test.MaxGoroutineGrowth {
t.Errorf("goroutine leak detected: started with %d, ended with %d (growth: %d, max allowed: %d)",
initialGoroutines, finalGoroutines, goroutineGrowth, test.MaxGoroutineGrowth)
}
// Check memory growth
memoryGrowthBytes := int64(finalMem.Alloc) - int64(initialMem.Alloc)
memoryGrowthMB := float64(memoryGrowthBytes) / (1024 * 1024)
if test.MaxMemoryGrowthMB >= 0 && memoryGrowthMB > test.MaxMemoryGrowthMB {
t.Errorf("memory leak detected: memory grew by %.2f MB (max allowed: %.2f MB)",
memoryGrowthMB, test.MaxMemoryGrowthMB)
}
t.Logf("Memory test completed: goroutines %d->%d (Δ%d), memory %.2f MB growth",
initialGoroutines, finalGoroutines, goroutineGrowth, memoryGrowthMB)
})
}
}
// TestDataFactory provides utilities for generating test data
type TestDataFactory struct{}
// NewTestDataFactory creates a new test data factory
func NewTestDataFactory() *TestDataFactory {
return &TestDataFactory{}
}
// GenerateRandomString generates a random string of specified length
func (f *TestDataFactory) GenerateRandomString(length int) string {
if length <= 0 {
return ""
}
if length == 1 {
return "a" // Return a simple character for length 1
}
bytes := make([]byte, (length+1)/2) // Ensure we have enough bytes
if _, err := rand.Read(bytes); err != nil {
return fmt.Sprintf("test-string-%d", time.Now().UnixNano())[:length]
}
encoded := hex.EncodeToString(bytes)
if len(encoded) >= length {
return encoded[:length]
}
return encoded
}
// GenerateTestToken generates a test JWT-like token
func (f *TestDataFactory) GenerateTestToken() string {
header := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
payload := "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
signature := f.GenerateRandomString(32)
return fmt.Sprintf("%s.%s.%s", header, payload, signature)
}
// GenerateTestHTTPRequest generates a test HTTP request
func (f *TestDataFactory) GenerateTestHTTPRequest() *http.Request {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
req.Header.Set("User-Agent", "test-agent")
req.Header.Set("Authorization", "Bearer "+f.GenerateTestToken())
return req
}
// GenerateTestSession generates a test session with random data
func (f *TestDataFactory) GenerateTestSession() *UnifiedMockSession {
session := NewUnifiedMockSession()
session.Set("user_id", f.GenerateRandomString(16))
session.Set("email", fmt.Sprintf("user%[email protected]", f.GenerateRandomString(8)))
session.Set("created_at", time.Now())
return session
}
// EdgeCaseGenerator provides utilities for generating comprehensive edge cases
type EdgeCaseGenerator struct {
factory *TestDataFactory
}
// NewEdgeCaseGenerator creates a new edge case generator
func NewEdgeCaseGenerator() *EdgeCaseGenerator {
return &EdgeCaseGenerator{
factory: NewTestDataFactory(),
}
}
// GenerateStringEdgeCases generates edge cases for string inputs
func (g *EdgeCaseGenerator) GenerateStringEdgeCases() []string {
return []string{
"", // Empty string
" ", // Single space
" ", // Multiple spaces
"\t", // Tab
"\n", // Newline
"\r\n", // Windows newline
"a", // Single character
g.factory.GenerateRandomString(1), // Random single char
g.factory.GenerateRandomString(1000), // Long string
g.factory.GenerateRandomString(10000), // Very long string
"特殊字符", // Unicode characters
"🚀🎯📊", // Emojis
"'DROP TABLE users;", // SQL injection attempt
"<script>alert('xss')</script>", // XSS attempt
"../../etc/passwd", // Path traversal attempt
string([]byte{0, 1, 2, 255}), // Binary data
}
}
// GenerateIntegerEdgeCases generates edge cases for integer inputs
func (g *EdgeCaseGenerator) GenerateIntegerEdgeCases() []int {
return []int{
0,
1,
-1,
42,
-42,
2147483647, // max int32
-2147483648, // min int32
1000000,
-1000000,
}
}
// GenerateTimeEdgeCases generates edge cases for time inputs
func (g *EdgeCaseGenerator) GenerateTimeEdgeCases() []time.Time {
now := time.Now()
return []time.Time{
time.Time{}, // Zero time
now, // Current time
now.Add(-time.Hour), // One hour ago
now.Add(time.Hour), // One hour from now
now.Add(-24 * time.Hour), // One day ago
now.Add(24 * time.Hour), // One day from now
now.Add(-365 * 24 * time.Hour), // One year ago
now.Add(365 * 24 * time.Hour), // One year from now
time.Unix(0, 0), // Unix epoch
time.Unix(1<<63-62135596801, 0), // Max time
}
}
// GenerateHTTPRequestEdgeCases generates edge cases for HTTP requests
func (g *EdgeCaseGenerator) GenerateHTTPRequestEdgeCases() []*http.Request {
cases := make([]*http.Request, 0)
// Basic cases
req1, _ := http.NewRequest("GET", "http://example.com", nil)
cases = append(cases, req1)
// Request with headers
req2, _ := http.NewRequest("POST", "https://api.example.com/endpoint", nil)
req2.Header.Set("Content-Type", "application/json")
req2.Header.Set("Authorization", "Bearer "+g.factory.GenerateTestToken())
cases = append(cases, req2)
// Request with query parameters
req3, _ := http.NewRequest("GET", "http://example.com/search?q=test&limit=10", nil)
cases = append(cases, req3)
// Request with unusual headers
req4, _ := http.NewRequest("GET", "http://example.com", nil)
req4.Header.Set("X-Custom-Header", g.factory.GenerateRandomString(1000))
req4.Header.Set("User-Agent", "")
cases = append(cases, req4)
return cases
}
// PerformanceTestHelper provides utilities for performance testing
type PerformanceTestHelper struct {
samples []time.Duration
mu sync.Mutex
}
// NewPerformanceTestHelper creates a new performance test helper
func NewPerformanceTestHelper() *PerformanceTestHelper {
return &PerformanceTestHelper{
samples: make([]time.Duration, 0),
}
}
// Measure measures the execution time of a function
func (h *PerformanceTestHelper) Measure(fn func()) time.Duration {
start := time.Now()
fn()
duration := time.Since(start)
h.mu.Lock()
h.samples = append(h.samples, duration)
h.mu.Unlock()
return duration
}
// GetAverageTime returns the average execution time
func (h *PerformanceTestHelper) GetAverageTime() time.Duration {
h.mu.Lock()
defer h.mu.Unlock()
if len(h.samples) == 0 {
return 0
}
var total time.Duration
for _, sample := range h.samples {
total += sample
}
return total / time.Duration(len(h.samples))
}
// GetPercentile returns the nth percentile of execution times
func (h *PerformanceTestHelper) GetPercentile(percentile float64) time.Duration {
h.mu.Lock()
defer h.mu.Unlock()
if len(h.samples) == 0 {
return 0
}
// Simple percentile calculation (could be improved with sorting)
index := int(float64(len(h.samples)) * percentile / 100.0)
if index >= len(h.samples) {
index = len(h.samples) - 1
}
return h.samples[index]
}
// Reset clears all performance samples
func (h *PerformanceTestHelper) Reset() {
h.mu.Lock()
defer h.mu.Unlock()
h.samples = h.samples[:0]
}