mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-20 04:57:02 +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
|
autoCleanupInterval time.Duration
|
||||||
// mutex protects all cache operations for thread safety
|
// mutex protects all cache operations for thread safety
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
|
// stopChan for controlled shutdown
|
||||||
|
stopChan chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultMaxSize is the default maximum number of items in the cache.
|
// DefaultMaxSize is the default maximum number of items in the cache.
|
||||||
@@ -68,6 +70,7 @@ func NewCacheWithLogger(logger *Logger) *Cache {
|
|||||||
maxSize: DefaultMaxSize,
|
maxSize: DefaultMaxSize,
|
||||||
autoCleanupInterval: 2 * time.Minute,
|
autoCleanupInterval: 2 * time.Minute,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
c.startAutoCleanup()
|
c.startAutoCleanup()
|
||||||
return c
|
return c
|
||||||
@@ -232,8 +235,63 @@ func (c *Cache) startAutoCleanup() {
|
|||||||
// Close stops the background cleanup task and releases associated resources.
|
// 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.
|
// It should be called when the cache is no longer needed to prevent resource leaks.
|
||||||
func (c *Cache) Close() {
|
func (c *Cache) Close() {
|
||||||
if c.cleanupTask != nil {
|
// First, close the stop channel and get cleanup task reference without holding lock
|
||||||
c.cleanupTask.Stop()
|
c.mutex.Lock()
|
||||||
c.cleanupTask = nil
|
|
||||||
|
// 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 (
|
import (
|
||||||
"container/list"
|
"container/list"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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) {
|
func TestCacheMemoryLeaks(t *testing.T) {
|
||||||
t.Run("Cache doesn't release expired items memory", func(t *testing.T) {
|
config := GetTestConfig()
|
||||||
runtime.GC()
|
if config.ShouldSkipTest(t, TestTypeLeakDetection) {
|
||||||
var m runtime.MemStats
|
return
|
||||||
runtime.ReadMemStats(&m)
|
}
|
||||||
baselineAlloc := m.Alloc
|
|
||||||
|
|
||||||
cache := NewCache()
|
runner := NewTestSuiteRunner()
|
||||||
defer cache.Close()
|
runner.SetTimeout(config.DefaultTimeout)
|
||||||
|
|
||||||
// Add many large items with short expiration
|
// Define table of memory leak test cases
|
||||||
largeData := make([]byte, 1024*1024) // 1MB
|
tests := []MemoryLeakTestCase{
|
||||||
for i := 0; i < 100; i++ {
|
{
|
||||||
key := fmt.Sprintf("key-%d", i)
|
Name: "Cache expired items memory release",
|
||||||
// Items expire in 1 second
|
Description: "Test that cache properly releases memory for expired items after cleanup",
|
||||||
cache.Set(key, largeData, 1*time.Second)
|
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
|
// Add large items with short expiration - size based on config
|
||||||
time.Sleep(2 * time.Second)
|
dataSize := 1024 * 1024 // 1MB for extended tests
|
||||||
|
if config.QuickMode {
|
||||||
// Force cleanup
|
dataSize = 64 * 1024 // 64KB for quick tests
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}(i)
|
largeData := make([]byte, dataSize)
|
||||||
}
|
itemCount := config.MaxIterations
|
||||||
|
if itemCount > 50 {
|
||||||
// Readers continuously read items
|
itemCount = 50
|
||||||
for i := 0; i < 5; i++ {
|
}
|
||||||
wg.Add(1)
|
for i := 0; i < itemCount; i++ {
|
||||||
go func(id int) {
|
key := fmt.Sprintf("key-%d", i)
|
||||||
defer wg.Done()
|
cache.Set(key, largeData, 100*time.Millisecond)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let it run for a bit
|
// Wait for items to expire
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(200 * time.Millisecond)
|
||||||
close(stop)
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
runtime.GC()
|
// Force cleanup
|
||||||
runtime.ReadMemStats(&m)
|
cache.Cleanup()
|
||||||
finalAlloc := m.Alloc
|
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
|
// Simulate token blacklisting beyond limit
|
||||||
var allocIncrease float64
|
testCount := cacheSize * 2 // Try to exceed the limit
|
||||||
if finalAlloc > baselineAlloc {
|
for i := 0; i < testCount; i++ {
|
||||||
allocIncrease = float64(finalAlloc-baselineAlloc) / 1024 / 1024
|
token := fmt.Sprintf("token-%d", i)
|
||||||
} else {
|
blacklist.Set(token, true, 24*time.Hour)
|
||||||
allocIncrease = -float64(baselineAlloc-finalAlloc) / 1024 / 1024
|
}
|
||||||
}
|
|
||||||
t.Logf("Memory increase under concurrent load: %.2f MB", allocIncrease)
|
|
||||||
|
|
||||||
if allocIncrease > 5.0 {
|
// Verify size limit is enforced
|
||||||
t.Errorf("Memory leak under concurrent operations: %.2f MB", allocIncrease)
|
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) {
|
// Check size limit is enforced
|
||||||
cache := NewCache()
|
replayCacheMu.RLock()
|
||||||
cache.SetMaxSize(100) // Small cache
|
cacheSize := 0
|
||||||
defer cache.Close()
|
if replayCache != nil {
|
||||||
|
cacheSize = len(replayCache.items)
|
||||||
|
}
|
||||||
|
replayCacheMu.RUnlock()
|
||||||
|
|
||||||
runtime.GC()
|
if cacheSize > 10000 {
|
||||||
var m runtime.MemStats
|
return fmt.Errorf("replay cache exceeded max size: %d items", cacheSize)
|
||||||
runtime.ReadMemStats(&m)
|
}
|
||||||
baselineAlloc := m.Alloc
|
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
|
var wg sync.WaitGroup
|
||||||
for i := 0; i < 1000; i++ {
|
stop := make(chan struct{})
|
||||||
key := fmt.Sprintf("key-%d", i)
|
|
||||||
data := make([]byte, 10240) // 10KB per item
|
|
||||||
cache.Set(key, data, 1*time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime.GC()
|
// Scale concurrency and operations based on config
|
||||||
runtime.ReadMemStats(&m)
|
writerCount := config.AdjustConcurrencyParams(5)
|
||||||
afterEvictionAlloc := m.Alloc
|
if config.QuickMode && writerCount > 2 {
|
||||||
|
writerCount = 2
|
||||||
|
}
|
||||||
|
operations := config.MaxIterations * 5
|
||||||
|
if operations > 50 && config.QuickMode {
|
||||||
|
operations = 10
|
||||||
|
}
|
||||||
|
|
||||||
allocIncrease := float64(afterEvictionAlloc-baselineAlloc) / 1024 / 1024
|
for i := 0; i < writerCount; i++ {
|
||||||
t.Logf("Memory after LRU eviction (1000 items, max 100): %.2f MB", allocIncrease)
|
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
|
// Readers - match writer count
|
||||||
if allocIncrease > 2.0 { // 100 * 10KB = ~1MB
|
for i := 0; i < writerCount; i++ {
|
||||||
t.Errorf("LRU eviction doesn't release memory properly: %.2f MB", allocIncrease)
|
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
|
// Let it run briefly - adjust based on config
|
||||||
if len(cache.items) > 100 {
|
runTime := config.GetCleanupInterval() * 5
|
||||||
t.Errorf("Cache size exceeded limit: %d items", len(cache.items))
|
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) {
|
// Add items to trigger eviction
|
||||||
tokenCache := NewTokenCache()
|
for i := 0; i < 50; i++ {
|
||||||
defer tokenCache.Close()
|
key := fmt.Sprintf("key-%d", i)
|
||||||
|
data := make([]byte, 1024) // 1KB per item
|
||||||
|
cache.Set(key, data, 1*time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
runtime.GC()
|
// Verify cache size limit
|
||||||
var m runtime.MemStats
|
if len(cache.items) > 10 {
|
||||||
runtime.ReadMemStats(&m)
|
return fmt.Errorf("cache size exceeded limit: %d items", len(cache.items))
|
||||||
baselineAlloc := m.Alloc
|
}
|
||||||
|
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
|
// Add tokens with large claims (reduced count for repeated iterations)
|
||||||
for i := 0; i < 1000; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
token := fmt.Sprintf("token-%d", i)
|
token := fmt.Sprintf("token-%d", i)
|
||||||
claims := map[string]interface{}{
|
claims := map[string]interface{}{
|
||||||
"sub": fmt.Sprintf("user-%d", i),
|
"sub": fmt.Sprintf("user-%d", i),
|
||||||
"email": fmt.Sprintf("user%[email protected]", i),
|
"email": fmt.Sprintf("user%[email protected]", i),
|
||||||
"groups": make([]string, 100), // Large groups list
|
"groups": make([]string, 10), // Smaller groups list
|
||||||
"data": make([]byte, 1024), // Extra data
|
"data": make([]byte, 512), // Smaller data
|
||||||
}
|
}
|
||||||
tokenCache.Set(token, claims, 1*time.Hour)
|
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()
|
// Start cleanup with our custom interval
|
||||||
runtime.ReadMemStats(&m)
|
cache.startAutoCleanup()
|
||||||
currentAlloc := m.Alloc
|
defer cache.Close()
|
||||||
|
|
||||||
allocIncrease := float64(currentAlloc-baselineAlloc) / 1024 / 1024
|
// Add expired items (reduced count for repeated iterations)
|
||||||
t.Logf("Memory after adding 1000 tokens with large claims: %.2f MB", allocIncrease)
|
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
|
// Wait for items to expire and cleanup to run
|
||||||
if allocIncrease > 20.0 {
|
time.Sleep(300 * time.Millisecond)
|
||||||
t.Errorf("Token cache uses excessive memory: %.2f MB", allocIncrease)
|
|
||||||
}
|
// 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
|
// TestCacheEvictionPerformance tests the performance of cache eviction using table-driven patterns
|
||||||
func TestCacheEvictionBug(t *testing.T) {
|
func TestCacheEvictionPerformance(t *testing.T) {
|
||||||
t.Run("evictOldest scans entire list", func(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 := NewCache()
|
||||||
cache.SetMaxSize(100)
|
cache.SetMaxSize(100)
|
||||||
defer cache.Close()
|
defer cache.Close()
|
||||||
|
|
||||||
// Fill cache with non-expired items
|
// Pre-fill cache to trigger evictions
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
key := fmt.Sprintf("key-%d", i)
|
key := fmt.Sprintf("initial-key-%d", i)
|
||||||
cache.Set(key, "data", 1*time.Hour) // Long expiry
|
cache.Set(key, "initial-data", 1*time.Hour)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to add one more item to trigger eviction
|
b.ResetTimer()
|
||||||
start := time.Now()
|
for i := 0; i < b.N; i++ {
|
||||||
cache.Set("trigger", "data", 1*time.Hour)
|
key := fmt.Sprintf("evict-key-%d", i)
|
||||||
elapsed := time.Since(start)
|
cache.Set(key, "evict-data", 1*time.Hour)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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
|
package traefikoidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -44,6 +45,14 @@ type OptimizedCache struct {
|
|||||||
autoCleanupInterval time.Duration
|
autoCleanupInterval time.Duration
|
||||||
// mutex provides thread safety for all operations
|
// mutex provides thread safety for all operations
|
||||||
mutex sync.RWMutex
|
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.
|
// 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
|
maxMemoryBytes = 64 * 1024 * 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
c := &OptimizedCache{
|
c := &OptimizedCache{
|
||||||
items: make(map[string]*OptimizedCacheEntry, maxSize),
|
items: make(map[string]*OptimizedCacheEntry, maxSize),
|
||||||
head: head,
|
head: head,
|
||||||
@@ -101,6 +112,8 @@ func NewOptimizedCacheWithConfig(maxSize int, maxMemoryMB int, logger *Logger) *
|
|||||||
maxMemoryBytes: maxMemoryBytes,
|
maxMemoryBytes: maxMemoryBytes,
|
||||||
autoCleanupInterval: 2 * time.Minute,
|
autoCleanupInterval: 2 * time.Minute,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.startAutoCleanup()
|
c.startAutoCleanup()
|
||||||
@@ -115,6 +128,12 @@ func NewOptimizedCacheWithConfig(maxSize int, maxMemoryMB int, logger *Logger) *
|
|||||||
// - value: The value to store
|
// - value: The value to store
|
||||||
// - expiration: Time until the item expires
|
// - expiration: Time until the item expires
|
||||||
func (c *OptimizedCache) Set(key string, value interface{}, expiration time.Duration) {
|
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
|
// Normalize the key to handle long keys
|
||||||
normalizedKey := c.normalizeKey(key)
|
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.
|
// automatically removes expired items when encountered.
|
||||||
// Returns the value and true if found and valid, or nil and false otherwise.
|
// Returns the value and true if found and valid, or nil and false otherwise.
|
||||||
func (c *OptimizedCache) Get(key string) (interface{}, bool) {
|
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
|
// Normalize the key to handle long keys
|
||||||
normalizedKey := c.normalizeKey(key)
|
normalizedKey := c.normalizeKey(key)
|
||||||
|
|
||||||
@@ -209,7 +234,8 @@ func (c *OptimizedCache) Cleanup() {
|
|||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +345,44 @@ func (c *OptimizedCache) estimateEntrySize(entry *OptimizedCacheEntry) int64 {
|
|||||||
return size
|
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.
|
// 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.
|
// If the new limit is smaller than current size, least recently used items are evicted.
|
||||||
func (c *OptimizedCache) SetMaxSize(size int) {
|
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.
|
// startAutoCleanup starts the background cleanup task for automatic maintenance.
|
||||||
// The task runs periodically to remove expired entries and enforce memory limits.
|
// The task runs periodically to remove expired entries and enforce memory limits.
|
||||||
func (c *OptimizedCache) startAutoCleanup() {
|
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()
|
c.cleanupTask.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close stops the automatic cleanup task and releases resources.
|
// Close stops the automatic cleanup task and releases resources.
|
||||||
// Should be called when the cache is no longer needed to prevent resource leaks.
|
// Should be called when the cache is no longer needed to prevent resource leaks.
|
||||||
func (c *OptimizedCache) Close() {
|
func (c *OptimizedCache) Close() {
|
||||||
if c.cleanupTask != nil {
|
// First, mark as closed and cancel context without holding the lock
|
||||||
c.cleanupTask.Stop()
|
c.mutex.Lock()
|
||||||
c.cleanupTask = nil
|
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
|
package traefikoidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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) {
|
func TestCache_Cleanup(t *testing.T) {
|
||||||
c := NewCache()
|
c := NewCache()
|
||||||
defer c.Close() // Stop the auto-cleanup goroutine
|
defer c.Close()
|
||||||
|
|
||||||
// Add some items with different expiration times
|
// Add some items with different expiration times
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
pastTime := now.Add(-1 * time.Hour) // Already expired
|
pastTime := now.Add(-1 * time.Hour) // Already expired
|
||||||
futureTime := now.Add(1 * time.Hour) // Not 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("expired", "expired-value", pastTime.Sub(now))
|
||||||
c.Set("valid", "valid-value", futureTime.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) {
|
func TestCache_SetMaxSize(t *testing.T) {
|
||||||
c := NewCache()
|
c := NewCache()
|
||||||
defer c.Close() // Stop the auto-cleanup goroutine
|
defer c.Close()
|
||||||
|
|
||||||
// Set a lower max size
|
// Set a lower max size
|
||||||
originalMaxSize := 10
|
originalMaxSize := 10
|
||||||
|
|||||||
+30
-2
@@ -727,6 +727,12 @@ type GracefulDegradation struct {
|
|||||||
config GracefulDegradationConfig
|
config GracefulDegradationConfig
|
||||||
// mutex protects shared state
|
// mutex protects shared state
|
||||||
mutex sync.RWMutex
|
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.
|
// GracefulDegradationConfig holds configuration for graceful degradation behavior.
|
||||||
@@ -762,6 +768,7 @@ func NewGracefulDegradation(config GracefulDegradationConfig, logger *Logger) *G
|
|||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gd.stopChan = make(chan struct{})
|
||||||
go gd.startHealthCheckRoutine()
|
go gd.startHealthCheckRoutine()
|
||||||
|
|
||||||
return gd
|
return gd
|
||||||
@@ -867,13 +874,13 @@ func (gd *GracefulDegradation) executeFallback(serviceName string) (interface{},
|
|||||||
|
|
||||||
// startHealthCheckRoutine starts the background health check routine
|
// startHealthCheckRoutine starts the background health check routine
|
||||||
func (gd *GracefulDegradation) startHealthCheckRoutine() {
|
func (gd *GracefulDegradation) startHealthCheckRoutine() {
|
||||||
healthCheckTask := NewBackgroundTask(
|
gd.healthCheckTask = NewBackgroundTask(
|
||||||
"graceful-degradation-health-check",
|
"graceful-degradation-health-check",
|
||||||
gd.config.HealthCheckInterval,
|
gd.config.HealthCheckInterval,
|
||||||
gd.performHealthChecks,
|
gd.performHealthChecks,
|
||||||
gd.BaseRecoveryMechanism.logger,
|
gd.BaseRecoveryMechanism.logger,
|
||||||
)
|
)
|
||||||
healthCheckTask.Start()
|
gd.healthCheckTask.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// performHealthChecks runs health checks for all registered services
|
// performHealthChecks runs health checks for all registered services
|
||||||
@@ -921,6 +928,27 @@ func (gd *GracefulDegradation) Reset() {
|
|||||||
gd.LogInfo("Graceful degradation state has been 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
|
// IsAvailable returns whether the mechanism is available for use
|
||||||
func (gd *GracefulDegradation) IsAvailable() bool {
|
func (gd *GracefulDegradation) IsAvailable() bool {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -13,14 +13,19 @@ type ProviderRegistry struct {
|
|||||||
typeMap map[ProviderType]OIDCProvider
|
typeMap map[ProviderType]OIDCProvider
|
||||||
providers []OIDCProvider
|
providers []OIDCProvider
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
// Bounded cache configuration to prevent memory leaks
|
||||||
|
maxCacheSize int
|
||||||
|
cacheCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProviderRegistry creates and initializes a new ProviderRegistry.
|
// NewProviderRegistry creates and initializes a new ProviderRegistry.
|
||||||
func NewProviderRegistry() *ProviderRegistry {
|
func NewProviderRegistry() *ProviderRegistry {
|
||||||
return &ProviderRegistry{
|
return &ProviderRegistry{
|
||||||
providers: make([]OIDCProvider, 0),
|
providers: make([]OIDCProvider, 0),
|
||||||
cache: make(map[string]OIDCProvider),
|
cache: make(map[string]OIDCProvider),
|
||||||
typeMap: make(map[ProviderType]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()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
r.cache = make(map[string]OIDCProvider)
|
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.
|
// 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)
|
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.cache[issuerURL] = detectedProvider
|
||||||
|
r.cacheCount++
|
||||||
|
|
||||||
return detectedProvider
|
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)
|
logger.Debugf("TraefikOidc.New: Final t.scopes initialized to: %v", t.scopes)
|
||||||
|
|
||||||
t.providerURL = config.ProviderURL
|
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
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func NewLazyCacheWithLogger(logger *Logger) *Cache {
|
|||||||
maxSize: DefaultMaxSize,
|
maxSize: DefaultMaxSize,
|
||||||
autoCleanupInterval: 10 * time.Minute,
|
autoCleanupInterval: 10 * time.Minute,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
return c
|
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.
|
// Close stops the automatic cleanup task and releases resources.
|
||||||
// This should be called when the cache is no longer needed to prevent resource leaks.
|
// This should be called when the cache is no longer needed to prevent resource leaks.
|
||||||
func (c *MetadataCache) Close() {
|
func (c *MetadataCache) Close() {
|
||||||
if c.cleanupTask != nil {
|
// First, close the stop channel and get cleanup task reference without holding lock
|
||||||
c.cleanupTask.Stop()
|
c.mutex.Lock()
|
||||||
c.cleanupTask = nil
|
|
||||||
|
// 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
|
package traefikoidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCleanup(t *testing.T) {
|
// MetadataCacheTestSuite provides comprehensive testing for MetadataCache
|
||||||
pm := &ProviderMetadata{}
|
type MetadataCacheTestSuite struct {
|
||||||
mc := &MetadataCache{
|
runner *TestSuiteRunner
|
||||||
metadata: pm,
|
factory *TestDataFactory
|
||||||
expiresAt: time.Now().Add(-1 * time.Hour),
|
edgeGen *EdgeCaseGenerator
|
||||||
}
|
perfTest *PerformanceTestHelper
|
||||||
mc.Cleanup()
|
mockWG *sync.WaitGroup
|
||||||
if mc.metadata != nil {
|
logger *Logger
|
||||||
t.Errorf("Expected metadata to be nil after cleanup")
|
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) {
|
// setup creates a new MetadataCache for testing
|
||||||
dummyData := &ProviderMetadata{}
|
func (suite *MetadataCacheTestSuite) setup() {
|
||||||
// Construct MetadataCache manually to avoid interference from auto cleanup.
|
suite.testCache = NewMetadataCacheWithLogger(suite.mockWG, suite.logger)
|
||||||
mc := &MetadataCache{
|
}
|
||||||
metadata: dummyData,
|
|
||||||
expiresAt: time.Now().Add(1 * time.Hour),
|
// cleanup cleans up test resources
|
||||||
autoCleanupInterval: 5 * time.Minute,
|
func (suite *MetadataCacheTestSuite) cleanup() {
|
||||||
logger: newNoOpLogger(),
|
if suite.testCache != nil {
|
||||||
|
suite.testCache.Close()
|
||||||
}
|
}
|
||||||
// Use NewLogger to create a logger that writes errors only.
|
suite.perfTest.Reset()
|
||||||
logger := NewLogger("error")
|
}
|
||||||
result, err := mc.GetMetadata("http://example.com", http.DefaultClient, logger)
|
|
||||||
if err != nil {
|
// createTestServer creates a mock HTTP server for testing
|
||||||
t.Errorf("Expected no error, got %v", err)
|
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 result != dummyData {
|
if delay > 0 {
|
||||||
t.Errorf("Expected cached metadata to be returned")
|
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) {
|
// createTimeoutClient creates an HTTP client with timeout
|
||||||
mc := &MetadataCache{
|
func (suite *MetadataCacheTestSuite) createTimeoutClient(timeout time.Duration) *http.Client {
|
||||||
autoCleanupInterval: 50 * time.Millisecond,
|
return &http.Client{
|
||||||
logger: newNoOpLogger(),
|
Timeout: timeout,
|
||||||
}
|
|
||||||
// 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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errorRoundTripper implements http.RoundTripper for error testing
|
||||||
type errorRoundTripper struct {
|
type errorRoundTripper struct {
|
||||||
err error
|
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
|
return nil, e.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetMetadata_FetchError(t *testing.T) {
|
// TestMetadataCache_BasicOperations tests basic cache operations
|
||||||
// Create an HTTP client that always returns an error.
|
func TestMetadataCache_BasicOperations(t *testing.T) {
|
||||||
errorClient := &http.Client{
|
suite := NewMetadataCacheTestSuite()
|
||||||
Transport: errorRoundTripper{err: fmt.Errorf("fake fetch error")},
|
|
||||||
|
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.
|
// Run basic cleanup tests
|
||||||
mc := &MetadataCache{
|
for _, test := range tests {
|
||||||
logger: newNoOpLogger(),
|
t.Run(test.Name, func(t *testing.T) {
|
||||||
}
|
if test.Setup != nil {
|
||||||
logger := NewLogger("error")
|
if err := test.Setup(t); err != nil {
|
||||||
metadata, err := mc.GetMetadata("http://example.com", errorClient, logger)
|
t.Fatalf("Setup failed: %v", err)
|
||||||
if err == nil {
|
}
|
||||||
t.Errorf("Expected error, got nil")
|
}
|
||||||
}
|
defer func() {
|
||||||
if metadata != nil {
|
if test.Teardown != nil {
|
||||||
t.Errorf("Expected nil metadata, got %v", metadata)
|
if err := test.Teardown(t); err != nil {
|
||||||
}
|
t.Errorf("Teardown failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Case 2: Cache has old metadata.
|
originalMetadata := suite.testCache.metadata
|
||||||
dummy := &ProviderMetadata{}
|
suite.testCache.Cleanup()
|
||||||
mc.metadata = dummy
|
|
||||||
mc.expiresAt = time.Now().Add(-1 * time.Minute)
|
switch test.Name {
|
||||||
logger2 := NewLogger("error")
|
case "CleanupExpiredMetadata":
|
||||||
metadata, err = mc.GetMetadata("http://example.com", errorClient, logger2)
|
if suite.testCache.metadata != nil {
|
||||||
if err != nil {
|
t.Error("Expected expired metadata to be nil after cleanup")
|
||||||
t.Errorf("Expected no error when cached metadata exists, got %v", err)
|
}
|
||||||
}
|
case "CleanupValidMetadata":
|
||||||
if metadata != dummy {
|
if suite.testCache.metadata != originalMetadata {
|
||||||
t.Errorf("Expected cached metadata to be returned")
|
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
|
poolCleaned := 0
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
if poolSession := sm.sessionPool.Get(); poolSession != nil {
|
if poolSession := sm.sessionPool.Get(); poolSession != nil {
|
||||||
|
|||||||
@@ -72,6 +72,18 @@ type TokenRetrievalResult struct {
|
|||||||
type ChunkManager struct {
|
type ChunkManager struct {
|
||||||
logger *Logger
|
logger *Logger
|
||||||
mutex *sync.RWMutex
|
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.
|
// NewChunkManager creates a new ChunkManager instance with proper initialization.
|
||||||
@@ -87,8 +99,12 @@ func NewChunkManager(logger *Logger) *ChunkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &ChunkManager{
|
return &ChunkManager{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
mutex: &sync.RWMutex{},
|
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
|
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