Files
mev-beta/internal/recovery/retry_handler_test.go
Krypto Kajun 850223a953 fix(multicall): resolve critical multicall parsing corruption issues
- Added comprehensive bounds checking to prevent buffer overruns in multicall parsing
- Implemented graduated validation system (Strict/Moderate/Permissive) to reduce false positives
- Added LRU caching system for address validation with 10-minute TTL
- Enhanced ABI decoder with missing Universal Router and Arbitrum-specific DEX signatures
- Fixed duplicate function declarations and import conflicts across multiple files
- Added error recovery mechanisms with multiple fallback strategies
- Updated tests to handle new validation behavior for suspicious addresses
- Fixed parser test expectations for improved validation system
- Applied gofmt formatting fixes to ensure code style compliance
- Fixed mutex copying issues in monitoring package by introducing MetricsSnapshot
- Resolved critical security vulnerabilities in heuristic address extraction
- Progress: Updated TODO audit from 10% to 35% complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 00:12:55 -05:00

363 lines
10 KiB
Go

package recovery
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/fraktal/mev-beta/internal/logger"
)
func TestRetryHandler_ExecuteWithRetry_Success(t *testing.T) {
log := logger.New("debug", "text", "")
handler := NewRetryHandler(log)
attempts := 0
operation := func(ctx context.Context, attempt int) error {
attempts++
if attempts == 2 {
return nil // Success on second attempt
}
return errors.New("temporary failure")
}
result := handler.ExecuteWithRetry(context.Background(), "test_operation", operation)
assert.True(t, result.Success)
assert.Equal(t, 2, result.Attempts)
assert.Nil(t, result.LastError)
assert.Equal(t, 2, attempts)
}
func TestRetryHandler_ExecuteWithRetry_MaxAttemptsReached(t *testing.T) {
log := logger.New("debug", "text", "")
handler := NewRetryHandler(log)
attempts := 0
operation := func(ctx context.Context, attempt int) error {
attempts++
return errors.New("persistent failure")
}
result := handler.ExecuteWithRetry(context.Background(), "test_operation", operation)
assert.False(t, result.Success)
assert.Equal(t, 3, result.Attempts) // Default max attempts
assert.NotNil(t, result.LastError)
assert.Equal(t, "persistent failure", result.LastError.Error())
assert.Equal(t, 3, attempts)
}
func TestRetryHandler_ExecuteWithRetry_ContextCanceled(t *testing.T) {
log := logger.New("debug", "text", "")
handler := NewRetryHandler(log)
ctx, cancel := context.WithCancel(context.Background())
attempts := 0
operation := func(ctx context.Context, attempt int) error {
attempts++
if attempts == 2 {
cancel() // Cancel context on second attempt
}
return errors.New("failure")
}
result := handler.ExecuteWithRetry(ctx, "test_operation", operation)
assert.False(t, result.Success)
assert.LessOrEqual(t, result.Attempts, 3)
assert.NotNil(t, result.LastError)
}
func TestRetryHandler_ExecuteWithRetry_CustomConfig(t *testing.T) {
log := logger.New("debug", "text", "")
handler := NewRetryHandler(log)
// Set custom configuration
customConfig := RetryConfig{
MaxAttempts: 5,
InitialDelay: 10 * time.Millisecond,
MaxDelay: 100 * time.Millisecond,
BackoffFactor: 2.0,
JitterEnabled: false,
TimeoutPerAttempt: 1 * time.Second,
}
handler.SetConfig("custom_operation", customConfig)
attempts := 0
operation := func(ctx context.Context, attempt int) error {
attempts++
return errors.New("persistent failure")
}
start := time.Now()
result := handler.ExecuteWithRetry(context.Background(), "custom_operation", operation)
duration := time.Since(start)
assert.False(t, result.Success)
assert.Equal(t, 5, result.Attempts) // Custom max attempts
assert.Equal(t, 5, attempts)
// Should have taken some time due to delays (at least 150ms for delays)
expectedMinDuration := 10*time.Millisecond + 20*time.Millisecond + 40*time.Millisecond + 80*time.Millisecond
assert.GreaterOrEqual(t, duration, expectedMinDuration)
}
func TestRetryHandler_ExecuteWithRetry_Disabled(t *testing.T) {
log := logger.New("debug", "text", "")
handler := NewRetryHandler(log)
handler.Disable()
attempts := 0
operation := func(ctx context.Context, attempt int) error {
attempts++
return errors.New("failure")
}
result := handler.ExecuteWithRetry(context.Background(), "test_operation", operation)
assert.False(t, result.Success)
assert.Equal(t, 1, result.Attempts) // Only one attempt when disabled
assert.Equal(t, 1, attempts)
}
func TestRetryHandler_CalculateDelay(t *testing.T) {
log := logger.New("debug", "text", "")
handler := NewRetryHandler(log)
config := RetryConfig{
InitialDelay: 100 * time.Millisecond,
MaxDelay: 1 * time.Second,
BackoffFactor: 2.0,
JitterEnabled: false,
}
tests := []struct {
attempt int
expectedMin time.Duration
expectedMax time.Duration
}{
{1, 100 * time.Millisecond, 100 * time.Millisecond},
{2, 200 * time.Millisecond, 200 * time.Millisecond},
{3, 400 * time.Millisecond, 400 * time.Millisecond},
{4, 800 * time.Millisecond, 800 * time.Millisecond},
{5, 1 * time.Second, 1 * time.Second}, // Should be capped at MaxDelay
}
for _, tt := range tests {
t.Run(fmt.Sprintf("attempt_%d", tt.attempt), func(t *testing.T) {
delay := handler.calculateDelay(config, tt.attempt)
assert.GreaterOrEqual(t, delay, tt.expectedMin)
assert.LessOrEqual(t, delay, tt.expectedMax)
})
}
}
func TestRetryHandler_CalculateDelay_WithJitter(t *testing.T) {
log := logger.New("debug", "text", "")
handler := NewRetryHandler(log)
config := RetryConfig{
InitialDelay: 100 * time.Millisecond,
MaxDelay: 1 * time.Second,
BackoffFactor: 2.0,
JitterEnabled: true,
}
// Test jitter variation
delays := make([]time.Duration, 10)
for i := 0; i < 10; i++ {
delays[i] = handler.calculateDelay(config, 2) // 200ms base
}
// Should have some variation due to jitter
allSame := true
for i := 1; i < len(delays); i++ {
if delays[i] != delays[0] {
allSame = false
break
}
}
assert.False(t, allSame, "Jitter should cause variation in delays")
// All delays should be reasonable (within 10% of base)
baseDelay := 200 * time.Millisecond
for _, delay := range delays {
assert.GreaterOrEqual(t, delay, baseDelay*9/10) // 10% below
assert.LessOrEqual(t, delay, baseDelay*11/10) // 10% above
}
}
func TestRetryHandler_GetStats(t *testing.T) {
log := logger.New("debug", "text", "")
handler := NewRetryHandler(log)
// Execute some operations
successOp := func(ctx context.Context, attempt int) error {
return nil
}
failOp := func(ctx context.Context, attempt int) error {
return errors.New("failure")
}
handler.ExecuteWithRetry(context.Background(), "test_success", successOp)
handler.ExecuteWithRetry(context.Background(), "test_success", successOp)
handler.ExecuteWithRetry(context.Background(), "test_fail", failOp)
stats := handler.GetStats()
// Check success stats
successStats := stats["test_success"]
require.NotNil(t, successStats)
assert.Equal(t, 2, successStats.TotalAttempts)
assert.Equal(t, 2, successStats.SuccessfulRetries)
assert.Equal(t, 0, successStats.FailedRetries)
// Check failure stats
failStats := stats["test_fail"]
require.NotNil(t, failStats)
assert.Equal(t, 3, failStats.TotalAttempts) // Default max attempts
assert.Equal(t, 0, failStats.SuccessfulRetries)
assert.Equal(t, 1, failStats.FailedRetries)
}
func TestRetryHandler_GetHealthSummary(t *testing.T) {
log := logger.New("debug", "text", "")
handler := NewRetryHandler(log)
// Execute some operations to generate stats
successOp := func(ctx context.Context, attempt int) error {
return nil
}
partialFailOp := func(ctx context.Context, attempt int) error {
if attempt < 2 {
return errors.New("temporary failure")
}
return nil
}
// 2 immediate successes
handler.ExecuteWithRetry(context.Background(), "immediate_success", successOp)
handler.ExecuteWithRetry(context.Background(), "immediate_success", successOp)
// 1 success after retry
handler.ExecuteWithRetry(context.Background(), "retry_success", partialFailOp)
summary := handler.GetHealthSummary()
assert.True(t, summary["enabled"].(bool))
assert.Equal(t, 2, summary["total_operations"].(int))
assert.Equal(t, 2, summary["healthy_operations"].(int))
assert.Equal(t, 0, summary["unhealthy_operations"].(int))
// Check operation details
details := summary["operation_details"].(map[string]interface{})
immediateDetails := details["immediate_success"].(map[string]interface{})
assert.Equal(t, 1.0, immediateDetails["success_rate"].(float64))
assert.Equal(t, 1.0, immediateDetails["average_attempts"].(float64))
assert.True(t, immediateDetails["is_healthy"].(bool))
retryDetails := details["retry_success"].(map[string]interface{})
assert.Equal(t, 1.0, retryDetails["success_rate"].(float64))
assert.Equal(t, 2.0, retryDetails["average_attempts"].(float64))
assert.True(t, retryDetails["is_healthy"].(bool)) // Still healthy despite retries
}
func TestRetryHandler_ConcurrentExecution(t *testing.T) {
log := logger.New("debug", "text", "")
handler := NewRetryHandler(log)
const numGoroutines = 50
const operationsPerGoroutine = 20
done := make(chan bool, numGoroutines)
successCount := make(chan int, numGoroutines)
operation := func(ctx context.Context, attempt int) error {
// 80% success rate
if attempt <= 1 && time.Now().UnixNano()%5 != 0 {
return nil
}
if attempt == 2 {
return nil // Always succeed on second attempt
}
return errors.New("failure")
}
// Launch concurrent retry operations
for i := 0; i < numGoroutines; i++ {
go func(id int) {
defer func() { done <- true }()
successes := 0
for j := 0; j < operationsPerGoroutine; j++ {
result := handler.ExecuteWithRetry(context.Background(),
fmt.Sprintf("concurrent_op_%d", id), operation)
if result.Success {
successes++
}
}
successCount <- successes
}(i)
}
// Collect results
totalSuccesses := 0
for i := 0; i < numGoroutines; i++ {
select {
case <-done:
totalSuccesses += <-successCount
case <-time.After(30 * time.Second):
t.Fatal("Concurrent retry test timed out")
}
}
totalOperations := numGoroutines * operationsPerGoroutine
successRate := float64(totalSuccesses) / float64(totalOperations)
t.Logf("Concurrent execution: %d/%d operations succeeded (%.2f%%)",
totalSuccesses, totalOperations, successRate*100)
// Should have high success rate due to retries
assert.GreaterOrEqual(t, successRate, 0.8, "Success rate should be at least 80%")
// Verify stats are consistent
stats := handler.GetStats()
assert.NotEmpty(t, stats, "Should have recorded stats")
}
func TestRetryHandler_EdgeCases(t *testing.T) {
log := logger.New("debug", "text", "")
handler := NewRetryHandler(log)
t.Run("nil operation", func(t *testing.T) {
assert.Panics(t, func() {
handler.ExecuteWithRetry(context.Background(), "nil_op", nil)
})
})
t.Run("empty operation type", func(t *testing.T) {
operation := func(ctx context.Context, attempt int) error {
return nil
}
result := handler.ExecuteWithRetry(context.Background(), "", operation)
assert.True(t, result.Success)
})
t.Run("very long operation type", func(t *testing.T) {
longName := string(make([]byte, 1000))
operation := func(ctx context.Context, attempt int) error {
return nil
}
result := handler.ExecuteWithRetry(context.Background(), longName, operation)
assert.True(t, result.Success)
})
}