mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-15 04:18:03 +00:00
fixup! fixup! fixup! fixup! Further pursue of perfection.
This commit is contained in:
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
Reference in New Issue
Block a user