#[cfg(test)]
mod tests {
use super::super::error::{BittensorError, ErrorCategory, RetryConfig};
use super::super::retry::{CircuitBreaker, ExponentialBackoff, RetryNode};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
#[test]
fn test_error_categorization() {
let rpc_timeout = BittensorError::RpcTimeoutError {
message: "Connection timed out".to_string(),
timeout: Duration::from_secs(30),
};
assert_eq!(rpc_timeout.category(), ErrorCategory::Transient);
assert!(rpc_timeout.is_retryable());
let tx_timeout = BittensorError::TxTimeoutError {
message: "Transaction timed out".to_string(),
timeout: Duration::from_secs(60),
};
assert_eq!(tx_timeout.category(), ErrorCategory::Transient);
assert!(tx_timeout.is_retryable());
let network_error = BittensorError::NetworkConnectivityError {
message: "Network unavailable".to_string(),
};
assert_eq!(network_error.category(), ErrorCategory::Network);
assert!(network_error.is_retryable());
let rate_limit = BittensorError::RateLimitExceeded {
message: "Too many requests".to_string(),
};
assert_eq!(rate_limit.category(), ErrorCategory::RateLimit);
assert!(rate_limit.is_retryable());
let auth_error = BittensorError::SignatureError {
message: "Invalid signature".to_string(),
};
assert_eq!(auth_error.category(), ErrorCategory::Auth);
assert!(auth_error.is_retryable());
let config_error = BittensorError::InvalidHotkey {
hotkey: "invalid_hotkey".to_string(),
};
assert_eq!(config_error.category(), ErrorCategory::Config);
assert!(!config_error.is_retryable());
let permanent_error = BittensorError::NeuronNotFound {
uid: 123,
netuid: 1,
};
assert_eq!(permanent_error.category(), ErrorCategory::Permanent);
assert!(!permanent_error.is_retryable());
}
#[test]
fn test_retry_config_generation() {
let transient_error = BittensorError::RpcConnectionError {
message: "Connection failed".to_string(),
};
let config = transient_error.retry_config().unwrap();
assert_eq!(config.max_attempts, 5);
assert_eq!(config.initial_delay, Duration::from_millis(200));
assert_eq!(config.backoff_multiplier, 1.5);
assert!(config.jitter);
let rate_limit_error = BittensorError::RateLimitExceeded {
message: "Rate limited".to_string(),
};
let config = rate_limit_error.retry_config().unwrap();
assert_eq!(config.max_attempts, 3);
assert_eq!(config.initial_delay, Duration::from_secs(1));
assert_eq!(config.backoff_multiplier, 2.0);
assert!(!config.jitter);
let config_error = BittensorError::InvalidHotkey {
hotkey: "invalid".to_string(),
};
assert!(config_error.retry_config().is_none());
}
#[test]
fn test_legacy_error_categorization() {
let legacy_timeout = BittensorError::RpcError {
message: "Request timeout occurred".to_string(),
};
assert_eq!(legacy_timeout.category(), ErrorCategory::Transient);
let legacy_permanent = BittensorError::RpcError {
message: "Invalid method".to_string(),
};
assert_eq!(legacy_permanent.category(), ErrorCategory::Permanent);
let legacy_wallet_config = BittensorError::WalletError {
message: "File not found".to_string(),
};
assert_eq!(legacy_wallet_config.category(), ErrorCategory::Config);
let legacy_wallet_auth = BittensorError::WalletError {
message: "Authentication failed".to_string(),
};
assert_eq!(legacy_wallet_auth.category(), ErrorCategory::Auth);
}
#[test]
fn test_exponential_backoff_calculation() {
let config = RetryConfig {
max_attempts: 4,
initial_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(5),
backoff_multiplier: 2.0,
jitter: false,
};
let mut backoff = ExponentialBackoff::new(config);
assert_eq!(backoff.next_delay().unwrap(), Duration::from_millis(100));
assert_eq!(backoff.attempts(), 1);
assert_eq!(backoff.next_delay().unwrap(), Duration::from_millis(200));
assert_eq!(backoff.attempts(), 2);
assert_eq!(backoff.next_delay().unwrap(), Duration::from_millis(400));
assert_eq!(backoff.attempts(), 3);
assert_eq!(backoff.next_delay().unwrap(), Duration::from_millis(800));
assert_eq!(backoff.attempts(), 4);
assert!(backoff.next_delay().is_none());
}
#[test]
fn test_exponential_backoff_max_delay_cap() {
let config = RetryConfig {
max_attempts: 5,
initial_delay: Duration::from_millis(1000),
max_delay: Duration::from_millis(2000), backoff_multiplier: 3.0,
jitter: false,
};
let mut backoff = ExponentialBackoff::new(config);
assert_eq!(backoff.next_delay().unwrap(), Duration::from_millis(1000));
assert_eq!(backoff.next_delay().unwrap(), Duration::from_millis(2000)); assert_eq!(backoff.next_delay().unwrap(), Duration::from_millis(2000)); }
#[test]
fn test_exponential_backoff_reset() {
let config = RetryConfig::default();
let mut backoff = ExponentialBackoff::new(config);
backoff.next_delay();
backoff.next_delay();
assert_eq!(backoff.attempts(), 2);
backoff.reset();
assert_eq!(backoff.attempts(), 0);
}
#[tokio::test]
async fn test_retry_node_success_on_first_attempt() {
let operation = || async { Ok::<&str, BittensorError>("success") };
let node = RetryNode::new();
let result: Result<&str, BittensorError> = node.execute(operation).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "success");
}
#[tokio::test]
async fn test_retry_node_success_after_retries() {
let counter = Arc::new(AtomicU32::new(0));
let counter_clone = counter.clone();
let operation = move || {
let counter = counter_clone.clone();
async move {
let count = counter.fetch_add(1, Ordering::SeqCst);
if count < 2 {
Err(BittensorError::RpcConnectionError {
message: "Connection failed".to_string(),
})
} else {
Ok("success")
}
}
};
let node = RetryNode::new();
let result: Result<&str, BittensorError> = node.execute(operation).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "success");
assert_eq!(counter.load(Ordering::SeqCst), 3); }
#[tokio::test]
async fn test_retry_node_non_retryable_error() {
let operation = || async {
Err(BittensorError::InvalidHotkey {
hotkey: "invalid".to_string(),
})
};
let node = RetryNode::new();
let result: Result<&str, BittensorError> = node.execute(operation).await;
assert!(result.is_err());
match result.unwrap_err() {
BittensorError::InvalidHotkey { hotkey } => {
assert_eq!(hotkey, "invalid");
}
other => panic!("Expected InvalidHotkey, got {other:?}"),
}
}
#[tokio::test]
async fn test_retry_node_max_retries_exceeded() {
let operation = || async {
Err(BittensorError::RpcConnectionError {
message: "Always fails".to_string(),
})
};
let node = RetryNode::new();
let result: Result<&str, BittensorError> = node.execute(operation).await;
assert!(result.is_err());
match result.unwrap_err() {
BittensorError::MaxRetriesExceeded { attempts } => {
assert_eq!(attempts, 5); }
other => panic!("Expected MaxRetriesExceeded, got {other:?}"),
}
}
#[tokio::test]
async fn test_retry_node_with_timeout() {
let operation = || async {
sleep(Duration::from_millis(200)).await;
Err(BittensorError::RpcConnectionError {
message: "Slow operation".to_string(),
})
};
let node = RetryNode::new().with_timeout(Duration::from_millis(500));
let result: Result<&str, BittensorError> = node.execute(operation).await;
assert!(result.is_err());
match result.unwrap_err() {
BittensorError::BackoffTimeoutReached { duration } => {
assert!(duration >= Duration::from_millis(400)); }
other => panic!("Expected BackoffTimeoutReached, got {other:?}"),
}
}
#[tokio::test]
async fn test_retry_node_custom_config() {
let custom_config = RetryConfig {
max_attempts: 2,
initial_delay: Duration::from_millis(10),
max_delay: Duration::from_secs(1),
backoff_multiplier: 1.0,
jitter: false,
};
let counter = Arc::new(AtomicU32::new(0));
let counter_clone = counter.clone();
let operation = move || {
let counter = counter_clone.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
Err(BittensorError::RpcConnectionError {
message: "Always fails".to_string(),
})
}
};
let node = RetryNode::new();
let result: Result<&str, BittensorError> =
node.execute_with_config(operation, custom_config).await;
assert!(result.is_err());
assert_eq!(counter.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn test_circuit_breaker_normal_operation() {
let mut circuit_breaker = CircuitBreaker::new(3, Duration::from_millis(100));
let operation = || async { Ok::<&str, BittensorError>("success") };
let result = circuit_breaker.execute(operation).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "success");
}
#[tokio::test]
async fn test_circuit_breaker_opens_after_failures() {
let mut circuit_breaker = CircuitBreaker::new(2, Duration::from_millis(100));
let result: Result<&str, BittensorError> = circuit_breaker
.execute(|| async {
Err(BittensorError::RpcConnectionError {
message: "Connection failed".to_string(),
})
})
.await;
assert!(result.is_err());
let result: Result<&str, BittensorError> = circuit_breaker
.execute(|| async {
Err(BittensorError::RpcConnectionError {
message: "Connection failed".to_string(),
})
})
.await;
assert!(result.is_err());
let counter = Arc::new(AtomicU32::new(0));
let counter_clone = counter.clone();
let result = circuit_breaker
.execute(move || {
let counter = counter_clone.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
Ok("should not reach here")
}
})
.await;
assert!(result.is_err());
assert_eq!(counter.load(Ordering::SeqCst), 0); match result.unwrap_err() {
BittensorError::ServiceUnavailable { .. } => {}
other => panic!("Expected ServiceUnavailable, got {other:?}"),
}
}
#[tokio::test]
async fn test_circuit_breaker_recovery() {
let mut circuit_breaker = CircuitBreaker::new(1, Duration::from_millis(50));
let result: Result<&str, BittensorError> = circuit_breaker
.execute(|| async {
Err(BittensorError::RpcConnectionError {
message: "Connection failed".to_string(),
})
})
.await;
assert!(result.is_err());
let result = circuit_breaker
.execute(|| async { Ok::<&str, BittensorError>("success") })
.await;
assert!(result.is_err());
sleep(Duration::from_millis(60)).await;
let result = circuit_breaker
.execute(|| async { Ok::<&str, BittensorError>("success") })
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "success");
}
#[test]
fn test_error_display_formatting() {
let timeout_error = BittensorError::TxTimeoutError {
message: "Transaction timed out".to_string(),
timeout: Duration::from_secs(60),
};
let display = format!("{timeout_error}");
assert!(display.contains("Transaction timeout after 60s"));
assert!(display.contains("Transaction timed out"));
let insufficient_fees = BittensorError::InsufficientTxFees {
required: 1000,
available: 500,
};
let display = format!("{insufficient_fees}");
assert!(display.contains("required 1000"));
assert!(display.contains("available 500"));
let weight_error = BittensorError::WeightSettingFailed {
netuid: 1,
reason: "Invalid weights".to_string(),
};
let display = format!("{weight_error}");
assert!(display.contains("subnet 1"));
assert!(display.contains("Invalid weights"));
}
#[test]
fn test_error_debug_formatting() {
let error = BittensorError::NeuronNotFound {
uid: 123,
netuid: 1,
};
let debug = format!("{error:?}");
assert!(debug.contains("NeuronNotFound"));
assert!(debug.contains("uid: 123"));
assert!(debug.contains("netuid: 1"));
}
#[test]
fn test_retry_config_presets() {
let transient = RetryConfig::transient();
assert_eq!(transient.max_attempts, 5);
assert_eq!(transient.backoff_multiplier, 1.5);
assert!(transient.jitter);
let rate_limit = RetryConfig::rate_limit();
assert_eq!(rate_limit.max_attempts, 3);
assert_eq!(rate_limit.backoff_multiplier, 2.0);
assert!(!rate_limit.jitter);
let network = RetryConfig::network();
assert_eq!(network.max_attempts, 4);
assert!(network.jitter);
let auth = RetryConfig::auth();
assert_eq!(auth.max_attempts, 2);
assert_eq!(auth.backoff_multiplier, 1.0);
assert!(!auth.jitter);
}
#[test]
fn test_error_creation_helpers() {
let max_retries = BittensorError::max_retries_exceeded(5);
match max_retries {
BittensorError::MaxRetriesExceeded { attempts } => {
assert_eq!(attempts, 5);
}
_ => panic!("Expected MaxRetriesExceeded"),
}
let backoff_timeout = BittensorError::backoff_timeout(Duration::from_secs(30));
match backoff_timeout {
BittensorError::BackoffTimeoutReached { duration } => {
assert_eq!(duration, Duration::from_secs(30));
}
_ => panic!("Expected BackoffTimeoutReached"),
}
}
}