1use std::fmt;
2use std::time::Duration;
3
4#[derive(Debug)]
6pub enum ScatterProxyError {
7 CircuitOpen { host: String },
9 MaxAttemptsExhausted {
11 host: String,
12 attempts: usize,
13 last_error: String,
14 },
15 Timeout { host: String, elapsed: Duration },
17 PoolFull { capacity: usize },
19 Init(String),
21}
22
23impl fmt::Display for ScatterProxyError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 ScatterProxyError::CircuitOpen { host } => {
27 write!(f, "circuit breaker open for host '{host}'")
28 }
29 ScatterProxyError::MaxAttemptsExhausted {
30 host,
31 attempts,
32 last_error,
33 } => {
34 write!(
35 f,
36 "max attempts exhausted for host '{host}' after {attempts} attempt(s): {last_error}"
37 )
38 }
39 ScatterProxyError::Timeout { host, elapsed } => {
40 write!(
41 f,
42 "task timeout for host '{host}' after {:.1}s",
43 elapsed.as_secs_f64()
44 )
45 }
46 ScatterProxyError::PoolFull { capacity } => {
47 write!(f, "task pool is full (capacity: {capacity})")
48 }
49 ScatterProxyError::Init(reason) => {
50 write!(f, "initialization error: {reason}")
51 }
52 }
53 }
54}
55
56impl std::error::Error for ScatterProxyError {}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn display_circuit_open() {
64 let err = ScatterProxyError::CircuitOpen {
65 host: "example.com".into(),
66 };
67 let msg = err.to_string();
68 assert!(msg.contains("circuit breaker open"));
69 assert!(msg.contains("example.com"));
70 }
71
72 #[test]
73 fn display_max_attempts_exhausted() {
74 let err = ScatterProxyError::MaxAttemptsExhausted {
75 host: "api.example.com".into(),
76 attempts: 5,
77 last_error: "connection refused".into(),
78 };
79 let msg = err.to_string();
80 assert!(msg.contains("api.example.com"));
81 assert!(msg.contains("5 attempt(s)"));
82 assert!(msg.contains("connection refused"));
83 }
84
85 #[test]
86 fn display_timeout() {
87 let err = ScatterProxyError::Timeout {
88 host: "slow.example.com".into(),
89 elapsed: Duration::from_millis(8500),
90 };
91 let msg = err.to_string();
92 assert!(msg.contains("slow.example.com"));
93 assert!(msg.contains("8.5s"));
94 }
95
96 #[test]
97 fn display_pool_full() {
98 let err = ScatterProxyError::PoolFull { capacity: 1000 };
99 let msg = err.to_string();
100 assert!(msg.contains("task pool is full"));
101 assert!(msg.contains("1000"));
102 }
103
104 #[test]
105 fn display_init() {
106 let err = ScatterProxyError::Init("failed to fetch proxy list".into());
107 let msg = err.to_string();
108 assert!(msg.contains("initialization error"));
109 assert!(msg.contains("failed to fetch proxy list"));
110 }
111
112 #[test]
113 fn error_trait_is_implemented() {
114 let err: Box<dyn std::error::Error> = Box::new(ScatterProxyError::Init("test".into()));
115 assert!(err.source().is_none());
117 }
118
119 #[test]
120 fn debug_format_includes_variant_name() {
121 let err = ScatterProxyError::CircuitOpen { host: "h".into() };
122 let dbg = format!("{err:?}");
123 assert!(dbg.contains("CircuitOpen"));
124 }
125
126 #[test]
127 fn timeout_sub_second_formatting() {
128 let err = ScatterProxyError::Timeout {
129 host: "h".into(),
130 elapsed: Duration::from_millis(200),
131 };
132 assert!(err.to_string().contains("0.2s"));
133 }
134
135 #[test]
136 fn timeout_exact_seconds_formatting() {
137 let err = ScatterProxyError::Timeout {
138 host: "h".into(),
139 elapsed: Duration::from_secs(60),
140 };
141 assert!(err.to_string().contains("60.0s"));
142 }
143
144 #[test]
145 fn max_attempts_with_one_attempt() {
146 let err = ScatterProxyError::MaxAttemptsExhausted {
147 host: "h".into(),
148 attempts: 1,
149 last_error: "err".into(),
150 };
151 assert!(err.to_string().contains("1 attempt(s)"));
152 }
153}