use std::fmt;
use std::time::Duration;
#[derive(Debug)]
pub enum ScatterProxyError {
CircuitOpen { host: String },
MaxAttemptsExhausted {
host: String,
attempts: usize,
last_error: String,
},
Timeout { host: String, elapsed: Duration },
PoolFull { capacity: usize },
Init(String),
}
impl fmt::Display for ScatterProxyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScatterProxyError::CircuitOpen { host } => {
write!(f, "circuit breaker open for host '{host}'")
}
ScatterProxyError::MaxAttemptsExhausted {
host,
attempts,
last_error,
} => {
write!(
f,
"max attempts exhausted for host '{host}' after {attempts} attempt(s): {last_error}"
)
}
ScatterProxyError::Timeout { host, elapsed } => {
write!(
f,
"task timeout for host '{host}' after {:.1}s",
elapsed.as_secs_f64()
)
}
ScatterProxyError::PoolFull { capacity } => {
write!(f, "task pool is full (capacity: {capacity})")
}
ScatterProxyError::Init(reason) => {
write!(f, "initialization error: {reason}")
}
}
}
}
impl std::error::Error for ScatterProxyError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_circuit_open() {
let err = ScatterProxyError::CircuitOpen {
host: "example.com".into(),
};
let msg = err.to_string();
assert!(msg.contains("circuit breaker open"));
assert!(msg.contains("example.com"));
}
#[test]
fn display_max_attempts_exhausted() {
let err = ScatterProxyError::MaxAttemptsExhausted {
host: "api.example.com".into(),
attempts: 5,
last_error: "connection refused".into(),
};
let msg = err.to_string();
assert!(msg.contains("api.example.com"));
assert!(msg.contains("5 attempt(s)"));
assert!(msg.contains("connection refused"));
}
#[test]
fn display_timeout() {
let err = ScatterProxyError::Timeout {
host: "slow.example.com".into(),
elapsed: Duration::from_millis(8500),
};
let msg = err.to_string();
assert!(msg.contains("slow.example.com"));
assert!(msg.contains("8.5s"));
}
#[test]
fn display_pool_full() {
let err = ScatterProxyError::PoolFull { capacity: 1000 };
let msg = err.to_string();
assert!(msg.contains("task pool is full"));
assert!(msg.contains("1000"));
}
#[test]
fn display_init() {
let err = ScatterProxyError::Init("failed to fetch proxy list".into());
let msg = err.to_string();
assert!(msg.contains("initialization error"));
assert!(msg.contains("failed to fetch proxy list"));
}
#[test]
fn error_trait_is_implemented() {
let err: Box<dyn std::error::Error> = Box::new(ScatterProxyError::Init("test".into()));
assert!(err.source().is_none());
}
#[test]
fn debug_format_includes_variant_name() {
let err = ScatterProxyError::CircuitOpen { host: "h".into() };
let dbg = format!("{err:?}");
assert!(dbg.contains("CircuitOpen"));
}
#[test]
fn timeout_sub_second_formatting() {
let err = ScatterProxyError::Timeout {
host: "h".into(),
elapsed: Duration::from_millis(200),
};
assert!(err.to_string().contains("0.2s"));
}
#[test]
fn timeout_exact_seconds_formatting() {
let err = ScatterProxyError::Timeout {
host: "h".into(),
elapsed: Duration::from_secs(60),
};
assert!(err.to_string().contains("60.0s"));
}
#[test]
fn max_attempts_with_one_attempt() {
let err = ScatterProxyError::MaxAttemptsExhausted {
host: "h".into(),
attempts: 1,
last_error: "err".into(),
};
assert!(err.to_string().contains("1 attempt(s)"));
}
}