#![warn(missing_docs)]
#[cfg(test)]
#[allow(clippy::result_large_err)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use decrust_core::{
Backtrace, CircuitBreaker, CircuitBreakerConfig, CircuitBreakerObserver,
CircuitBreakerState, CircuitOperationType, CircuitTransitionEvent, DecrustError,
OptionalError,
};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{sync::Arc, time::Duration};
struct TestObserver {
state_changes: AtomicUsize,
operation_attempts: AtomicUsize,
operation_results: AtomicUsize,
resets: AtomicUsize,
}
impl TestObserver {
fn new() -> Self {
Self {
state_changes: AtomicUsize::new(0),
operation_attempts: AtomicUsize::new(0),
operation_results: AtomicUsize::new(0),
resets: AtomicUsize::new(0),
}
}
}
impl CircuitBreakerObserver for TestObserver {
fn on_state_change(&self, _name: &str, _event: &CircuitTransitionEvent) {
self.state_changes.fetch_add(1, Ordering::SeqCst);
}
fn on_operation_attempt(&self, _name: &str, _state: CircuitBreakerState) {
self.operation_attempts.fetch_add(1, Ordering::SeqCst);
}
fn on_operation_result(
&self,
_name: &str,
_op_type: CircuitOperationType,
_duration: Duration,
_error: Option<&DecrustError>,
) {
self.operation_results.fetch_add(1, Ordering::SeqCst);
}
fn on_reset(&self, _name: &str) {
self.resets.fetch_add(1, Ordering::SeqCst);
}
}
#[test]
fn test_circuit_breaker_initial_state() {
let config = CircuitBreakerConfig::default();
let cb = CircuitBreaker::new("test-circuit", config);
assert_eq!(cb.state(), CircuitBreakerState::Closed);
}
#[test]
fn test_circuit_breaker_trip() {
let config = CircuitBreakerConfig::default();
let cb = CircuitBreaker::new("test-circuit", config);
assert_eq!(cb.state(), CircuitBreakerState::Closed);
cb.trip();
assert_eq!(cb.state(), CircuitBreakerState::Open);
cb.reset();
assert_eq!(cb.state(), CircuitBreakerState::Closed);
}
#[test]
fn test_circuit_breaker_observer_notifications() {
let config = CircuitBreakerConfig::default();
let cb = CircuitBreaker::new("test-circuit", config);
let observer = Arc::new(TestObserver::new());
cb.add_observer(observer.clone());
cb.trip();
cb.reset();
assert_eq!(observer.state_changes.load(Ordering::SeqCst), 2); assert_eq!(observer.resets.load(Ordering::SeqCst), 1);
}
#[test]
fn test_circuit_breaker_execute_success() {
let config = CircuitBreakerConfig::default();
let cb = CircuitBreaker::new("test-circuit", config);
let result: Result<i32, DecrustError> = cb.execute(|| Ok(42));
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_circuit_breaker_default_state() {
let cb = CircuitBreaker::new("test", CircuitBreakerConfig::default());
assert_eq!(cb.state(), CircuitBreakerState::Closed);
}
#[test]
fn test_circuit_breaker_execute_error() {
let config = CircuitBreakerConfig::default();
let cb = CircuitBreaker::new("test-circuit", config);
let result: Result<i32, DecrustError> = cb.execute(|| {
Err(DecrustError::Internal {
message: "Test error".to_string(),
source: OptionalError::new(None),
component: None,
backtrace: Backtrace::generate(),
})
});
assert!(result.is_err());
}
#[test]
fn test_circuit_breaker_open_error_formatting() {
let mut config = CircuitBreakerConfig::default();
config.failure_threshold = 1; config.reset_timeout = Duration::from_millis(100);
let cb = CircuitBreaker::new("test-circuit", config);
let _result: Result<i32, DecrustError> = cb.execute(|| {
Err(DecrustError::Internal {
message: "Forced failure".to_string(),
source: OptionalError::new(None),
component: None,
backtrace: Backtrace::generate(),
})
});
cb.trip();
assert_eq!(cb.state(), CircuitBreakerState::Open);
let result: Result<i32, DecrustError> = cb.execute(|| Ok(42));
assert!(result.is_err());
let error = result.unwrap_err();
match &error {
DecrustError::CircuitBreakerOpen {
name,
retry_after,
failure_count,
last_error,
backtrace: _,
} => {
assert_eq!(name, "test-circuit");
assert!(retry_after.is_some());
assert!(failure_count.is_some() || failure_count.is_none()); assert!(last_error.is_some() || last_error.is_none());
let error_string = format!("{}", error);
assert!(error_string.contains("Circuit breaker 'test-circuit' is open"));
}
_ => panic!("Expected CircuitBreakerOpen error, got: {:?}", error),
}
}
#[test]
fn test_circuit_breaker_open_error_all_fields() {
let error = DecrustError::CircuitBreakerOpen {
name: "test-circuit".to_string(),
retry_after: Some(Duration::from_secs(30)),
failure_count: Some(5),
last_error: Some("Last error message".to_string()),
backtrace: Backtrace::generate(),
};
let error_string = format!("{}", error);
assert!(error_string.contains("Circuit breaker 'test-circuit' is open"));
let debug_string = format!("{:?}", error);
assert!(debug_string.contains("CircuitBreakerOpen"));
assert_eq!(
error.category(),
decrust_core::types::ErrorCategory::CircuitBreaker
);
}
#[test]
fn test_circuit_breaker_formatting_consistency() {
let internal_error = DecrustError::Internal {
message: "Internal circuit breaker error".to_string(),
source: OptionalError::new(None),
component: Some("circuit_breaker".to_string()),
backtrace: Backtrace::generate(),
};
let timeout_error = DecrustError::Timeout {
operation: "Circuit breaker operation".to_string(),
duration: Duration::from_secs(5),
backtrace: Backtrace::generate(),
};
let cb_open_error = DecrustError::CircuitBreakerOpen {
name: "test-circuit".to_string(),
retry_after: Some(Duration::from_secs(30)),
failure_count: Some(3),
last_error: Some("Previous error".to_string()),
backtrace: Backtrace::generate(),
};
let _ = format!("{}", internal_error);
let _ = format!("{}", timeout_error);
let _ = format!("{}", cb_open_error);
let _ = format!("{:?}", internal_error);
let _ = format!("{:?}", timeout_error);
let _ = format!("{:?}", cb_open_error);
assert_eq!(
internal_error.category(),
decrust_core::types::ErrorCategory::Internal
);
assert_eq!(
timeout_error.category(),
decrust_core::types::ErrorCategory::Timeout
);
assert_eq!(
cb_open_error.category(),
decrust_core::types::ErrorCategory::CircuitBreaker
);
}
}