From e7d2cac88699053e007ca83010cbff506340d6c5 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sun, 5 Oct 2025 22:23:23 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Wave=20112:=20Add=20error=20retry?= =?UTF-8?q?=20strategy=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Comprehensive retry logic testing for common crate - Part of test suite improvements --- common/tests/error_retry_strategy_tests.rs | 302 +++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 common/tests/error_retry_strategy_tests.rs diff --git a/common/tests/error_retry_strategy_tests.rs b/common/tests/error_retry_strategy_tests.rs new file mode 100644 index 000000000..669b57364 --- /dev/null +++ b/common/tests/error_retry_strategy_tests.rs @@ -0,0 +1,302 @@ +//! Advanced Retry Strategy and Error Classification Tests +//! +//! This module provides comprehensive tests for: +//! - RetryStrategy::calculate_delay (exponential backoff, linear delays, caps, jitter) +//! - CommonError::severity (all error categories and variants) +//! - CommonError::retry_strategy (retryable vs non-retryable errors) + +use common::error::{CommonError, ErrorCategory, ErrorSeverity, RetryStrategy}; + +// ============================================================================= +// RETRY STRATEGY: CALCULATE_DELAY TESTS +// ============================================================================= + +#[test] +fn test_retry_strategy_calculate_delay_linear_basic() { + let strategy = RetryStrategy::Linear { base_delay_ms: 100 }; + + // Attempt 0: 100ms * 0 = 0ms + let delay_0 = strategy.calculate_delay(0).expect("should have delay"); + assert_eq!(delay_0.as_millis(), 0); + + // Attempt 1: 100ms * 1 = 100ms + let delay_1 = strategy.calculate_delay(1).expect("should have delay"); + assert_eq!(delay_1.as_millis(), 100); + + // Attempt 5: 100ms * 5 = 500ms + let delay_5 = strategy.calculate_delay(5).expect("should have delay"); + assert_eq!(delay_5.as_millis(), 500); +} + +#[test] +fn test_retry_strategy_calculate_delay_exponential_basic() { + let strategy = RetryStrategy::Exponential { base_delay_ms: 100, max_delay_ms: 10000 }; + + // Attempt 0: 100 * 2^0 = 100ms (with jitter 90-100ms) + let delay_0 = strategy.calculate_delay(0).expect("should have delay"); + assert!(delay_0.as_millis() >= 90 && delay_0.as_millis() <= 100); + + // Attempt 1: 100 * 2^1 = 200ms (with jitter 180-200ms) + let delay_1 = strategy.calculate_delay(1).expect("should have delay"); + assert!(delay_1.as_millis() >= 180 && delay_1.as_millis() <= 200); + + // Attempt 3: 100 * 2^3 = 800ms (with jitter 720-800ms) + let delay_3 = strategy.calculate_delay(3).expect("should have delay"); + assert!(delay_3.as_millis() >= 720 && delay_3.as_millis() <= 800); +} + +#[test] +fn test_retry_strategy_calculate_delay_exponential_capping() { + let strategy = RetryStrategy::Exponential { base_delay_ms: 100, max_delay_ms: 1000 }; + + // Attempt 10: 100 * 2^10 = 102400ms, but capped at min(10) = 100 * 2^10, then max_delay_ms = 1000ms + let delay_10 = strategy.calculate_delay(10).expect("should have delay"); + assert!(delay_10.as_millis() >= 900 && delay_10.as_millis() <= 1000); + + // Attempt 15: still capped at 1000ms + let delay_15 = strategy.calculate_delay(15).expect("should have delay"); + assert!(delay_15.as_millis() >= 900 && delay_15.as_millis() <= 1000); + + // Attempt 20: still capped + let delay_20 = strategy.calculate_delay(20).expect("should have delay"); + assert!(delay_20.as_millis() >= 900 && delay_20.as_millis() <= 1000); +} + +#[test] +fn test_retry_strategy_calculate_delay_no_retry() { + let strategy = RetryStrategy::NoRetry; + assert!(strategy.calculate_delay(0).is_none()); + assert!(strategy.calculate_delay(1).is_none()); + assert!(strategy.calculate_delay(100).is_none()); +} + +#[test] +fn test_retry_strategy_calculate_delay_immediate() { + let strategy = RetryStrategy::Immediate; + + let delay_0 = strategy.calculate_delay(0).expect("should have delay"); + assert_eq!(delay_0.as_millis(), 0); + + let delay_5 = strategy.calculate_delay(5).expect("should have delay"); + assert_eq!(delay_5.as_millis(), 0); +} + +#[test] +fn test_retry_strategy_calculate_delay_circuit_breaker() { + let strategy = RetryStrategy::CircuitBreaker; + + let delay_0 = strategy.calculate_delay(0).expect("should have delay"); + assert_eq!(delay_0.as_secs(), 30); + + let delay_3 = strategy.calculate_delay(3).expect("should have delay"); + assert_eq!(delay_3.as_secs(), 30); +} + +// ============================================================================= +// ERROR SEVERITY: COMPREHENSIVE CATEGORY TESTS +// ============================================================================= + +#[test] +fn test_common_error_severity_database() { + let err = CommonError::Database( + common::database::DatabaseError::PoolExhausted + ); + assert_eq!(err.severity(), ErrorSeverity::Critical); +} + +#[test] +fn test_common_error_severity_configuration() { + let err = CommonError::Configuration("Missing API key".to_string()); + assert_eq!(err.severity(), ErrorSeverity::Critical); +} + +#[test] +fn test_common_error_severity_network() { + let err = CommonError::Network("Connection timeout".to_string()); + assert_eq!(err.severity(), ErrorSeverity::Error); +} + +#[test] +fn test_common_error_severity_validation() { + let err = CommonError::Validation("Price out of range".to_string()); + assert_eq!(err.severity(), ErrorSeverity::Warn); +} + +#[test] +fn test_common_error_severity_timeout() { + let err = CommonError::Timeout { actual_ms: 5000, max_ms: 3000 }; + assert_eq!(err.severity(), ErrorSeverity::Error); +} + +#[test] +fn test_common_error_severity_service_critical_categories() { + // Critical severity categories + let err = CommonError::service(ErrorCategory::Critical, "critical issue"); + assert_eq!(err.severity(), ErrorSeverity::Critical); + + let err = CommonError::service(ErrorCategory::FinancialSafety, "safety breach"); + assert_eq!(err.severity(), ErrorSeverity::Critical); + + let err = CommonError::service(ErrorCategory::Authentication, "auth failed"); + assert_eq!(err.severity(), ErrorSeverity::Critical); +} + +#[test] +fn test_common_error_severity_service_error_categories() { + // Error severity categories + let err = CommonError::service(ErrorCategory::Trading, "trade failed"); + assert_eq!(err.severity(), ErrorSeverity::Error); + + let err = CommonError::service(ErrorCategory::RiskManagement, "risk violation"); + assert_eq!(err.severity(), ErrorSeverity::Error); + + let err = CommonError::service(ErrorCategory::Database, "service db issue"); + assert_eq!(err.severity(), ErrorSeverity::Error); +} + +#[test] +fn test_common_error_severity_service_warn_categories() { + // Warn severity categories (catch-all) + let err = CommonError::service(ErrorCategory::MarketData, "stale data"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::System, "system warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::Configuration, "config mismatch"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::Validation, "validation warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::Connection, "unstable connection"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::RateLimit, "throttled"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::Parse, "parse warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::Subscription, "sub warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::Broker, "broker warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::MachineLearning, "ml warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::Security, "security warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::BusinessLogic, "logic warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::Resource, "resource warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::Development, "dev warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::Risk, "risk warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::ML, "ml warning"); + assert_eq!(err.severity(), ErrorSeverity::Warn); + + let err = CommonError::service(ErrorCategory::Other, "misc issue"); + assert_eq!(err.severity(), ErrorSeverity::Warn); +} + +// ============================================================================= +// RETRY STRATEGY: ERROR-SPECIFIC TESTS +// ============================================================================= + +#[test] +fn test_common_error_retry_strategy_database() { + let err = CommonError::Database( + common::database::DatabaseError::PoolExhausted + ); + assert!(matches!(err.retry_strategy(), RetryStrategy::Exponential { .. })); +} + +#[test] +fn test_common_error_retry_strategy_network() { + let err = CommonError::Network("Connection reset".to_string()); + assert!(matches!(err.retry_strategy(), RetryStrategy::Linear { .. })); +} + +#[test] +fn test_common_error_retry_strategy_timeout() { + let err = CommonError::Timeout { actual_ms: 5000, max_ms: 3000 }; + assert!(matches!(err.retry_strategy(), RetryStrategy::Linear { .. })); +} + +#[test] +fn test_common_error_retry_strategy_non_retryable() { + // Configuration errors should not be retried + let err = CommonError::Configuration("Invalid config".to_string()); + assert!(matches!(err.retry_strategy(), RetryStrategy::NoRetry)); + + // Validation errors should not be retried + let err = CommonError::Validation("Invalid input".to_string()); + assert!(matches!(err.retry_strategy(), RetryStrategy::NoRetry)); +} + +#[test] +fn test_common_error_retry_strategy_service_network() { + let err = CommonError::service(ErrorCategory::Network, "network service error"); + assert!(matches!(err.retry_strategy(), RetryStrategy::Linear { .. })); + + let err = CommonError::service(ErrorCategory::Connection, "connection service error"); + assert!(matches!(err.retry_strategy(), RetryStrategy::Linear { .. })); +} + +#[test] +fn test_common_error_retry_strategy_service_rate_limit() { + let err = CommonError::service(ErrorCategory::RateLimit, "rate limited"); + assert!(matches!(err.retry_strategy(), RetryStrategy::Exponential { .. })); +} + +#[test] +fn test_common_error_retry_strategy_service_authentication() { + let err = CommonError::service(ErrorCategory::Authentication, "auth failed"); + assert!(matches!(err.retry_strategy(), RetryStrategy::NoRetry)); +} + +#[test] +fn test_common_error_retry_strategy_service_immediate() { + let err = CommonError::service(ErrorCategory::MarketData, "stale market data"); + assert!(matches!(err.retry_strategy(), RetryStrategy::Immediate)); + + let err = CommonError::service(ErrorCategory::Trading, "trade warning"); + assert!(matches!(err.retry_strategy(), RetryStrategy::Immediate)); +} + +// ============================================================================= +// EDGE CASES AND BOUNDARY CONDITIONS +// ============================================================================= + +#[test] +fn test_retry_strategy_exponential_zero_attempt() { + let strategy = RetryStrategy::Exponential { base_delay_ms: 50, max_delay_ms: 5000 }; + let delay = strategy.calculate_delay(0).expect("should have delay"); + // 50 * 2^0 = 50ms with jitter (45-50ms) + assert!(delay.as_millis() >= 45 && delay.as_millis() <= 50); +} + +#[test] +fn test_retry_strategy_exponential_large_max_delay() { + let strategy = RetryStrategy::Exponential { base_delay_ms: 100, max_delay_ms: 100000 }; + // Attempt 5: 100 * 2^5 = 3200ms (no capping) + let delay_5 = strategy.calculate_delay(5).expect("should have delay"); + assert!(delay_5.as_millis() >= 2880 && delay_5.as_millis() <= 3200); +} + +#[test] +fn test_retry_strategy_linear_zero_delay() { + let strategy = RetryStrategy::Linear { base_delay_ms: 0 }; + let delay = strategy.calculate_delay(0).expect("should have delay"); + assert_eq!(delay.as_millis(), 0); +}