1use thiserror::Error;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum ErrorCategory {
6 Network,
8 Config,
10 Parse,
12 Output,
14 Internal,
16}
17
18#[derive(Debug, Error)]
24pub enum Error {
25 #[error("Network error: {0}")]
27 NetworkError(#[from] reqwest::Error),
28
29 #[error("Failed to fetch server list: {0}")]
31 ServerListFetch(#[source] reqwest::Error),
32
33 #[error("Download test failed: {0}")]
35 DownloadTest(#[source] reqwest::Error),
36
37 #[error("Upload test failed: {0}")]
39 UploadTest(#[source] reqwest::Error),
40
41 #[error("Download test failed: {0}")]
43 DownloadFailure(String),
44
45 #[error("Upload test failed: {0}")]
47 UploadFailure(String),
48
49 #[error("Failed to discover client IP: {0}")]
51 IpDiscovery(#[source] reqwest::Error),
52
53 #[error("XML parse error: {0}")]
55 ParseXml(#[from] quick_xml::Error),
56
57 #[error("JSON parse error: {0}")]
59 ParseJson(#[from] serde_json::Error),
60
61 #[error("XML deserialization error: {0}")]
63 DeserializeXml(#[from] quick_xml::de::DeError),
64
65 #[error("CSV error: {0}")]
67 Csv(#[from] csv::Error),
68
69 #[error("Server not found: {0}")]
71 ServerNotFound(String),
72
73 #[error("I/O error: {0}")]
75 IoError(#[from] std::io::Error),
76
77 #[error("{msg}")]
79 Context {
80 msg: String,
81 source: Option<Box<dyn std::error::Error + Send + Sync>>,
82 },
83}
84
85impl Error {
86 #[must_use]
88 pub fn context(msg: impl Into<String>) -> Self {
89 Self::Context {
90 msg: msg.into(),
91 source: None,
92 }
93 }
94
95 #[must_use]
97 pub fn with_source(
98 msg: impl Into<String>,
99 source: impl std::error::Error + Send + Sync + 'static,
100 ) -> Self {
101 Self::Context {
102 msg: msg.into(),
103 source: Some(Box::new(source)),
104 }
105 }
106
107 #[must_use]
109 pub fn category(&self) -> ErrorCategory {
110 match self {
111 Error::NetworkError(_) => ErrorCategory::Network,
112 Error::ServerListFetch(_) => ErrorCategory::Network,
113 Error::DownloadTest(_) => ErrorCategory::Network,
114 Error::DownloadFailure(_) => ErrorCategory::Network,
115 Error::UploadTest(_) => ErrorCategory::Network,
116 Error::UploadFailure(_) => ErrorCategory::Network,
117 Error::IpDiscovery(_) => ErrorCategory::Network,
118 Error::ParseJson(_) => ErrorCategory::Parse,
119 Error::ParseXml(_) => ErrorCategory::Parse,
120 Error::DeserializeXml(_) => ErrorCategory::Parse,
121 Error::Csv(_) => ErrorCategory::Output,
122 Error::ServerNotFound(_) => ErrorCategory::Config,
123 Error::IoError(_) => ErrorCategory::Output,
124 Error::Context { .. } => ErrorCategory::Internal,
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use std::error::Error as _;
133
134 #[test]
135 fn test_network_error_display() {
136 let err = Error::context("connection failed");
138 assert_eq!(format!("{err}"), "connection failed");
139 }
140
141 #[test]
142 fn test_json_error_display() {
143 let invalid_json = "{invalid}";
144 let result: Result<serde_json::Value, _> = serde_json::from_str(invalid_json);
145 assert!(result.is_err());
146 let err = Error::from(result.unwrap_err());
147 assert!(format!("{err}").contains("JSON parse error"));
148 }
149
150 #[test]
151 fn test_server_not_found_display() {
152 let err = Error::ServerNotFound("no servers".to_string());
153 assert_eq!(format!("{err}"), "Server not found: no servers");
154 }
155
156 #[test]
157 fn test_io_error_display() {
158 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
159 let speedtest_err = Error::from(io_err);
160 assert!(format!("{speedtest_err}").contains("I/O error"));
161 }
162
163 #[test]
164 fn test_context_error_display() {
165 let err = Error::context("custom error");
166 assert_eq!(format!("{err}"), "custom error");
167 }
168
169 #[test]
170 fn test_context_with_source() {
171 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
172 let err = Error::with_source("Failed to read config", io_err);
173 assert_eq!(format!("{err}"), "Failed to read config");
174 assert!(err.source().is_some());
175 }
176
177 #[test]
178 fn test_from_io_error() {
179 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
180 let speedtest_err: Error = io_err.into();
181 assert!(matches!(speedtest_err, Error::IoError(_)));
182 assert!(format!("{speedtest_err}").contains("I/O error"));
183 }
184
185 #[test]
186 fn test_error_trait_implementation() {
187 let err = Error::context("test error");
188 let _: &dyn std::error::Error = &err;
190 }
191
192 #[test]
193 fn test_debug_trait() {
194 let err = Error::context("debug test");
195 let debug_str = format!("{err:?}");
196 assert!(debug_str.contains("Context"));
197 assert!(debug_str.contains("debug test"));
198 }
199
200 #[test]
201 fn test_from_serde_json_error() {
202 let invalid_json = "{invalid}";
203 let result: Result<serde_json::Value, _> = serde_json::from_str(invalid_json);
204 assert!(result.is_err());
205 let err: Error = result.unwrap_err().into();
206 assert!(matches!(err, Error::ParseJson(_)));
207 }
208
209 #[test]
210 fn test_from_quick_xml_de_error() {
211 let invalid_xml = "<unclosed>";
212 let result: Result<serde_json::Value, _> = quick_xml::de::from_str(invalid_xml);
213 assert!(result.is_err());
214 let err: Error = result.unwrap_err().into();
215 assert!(matches!(err, Error::DeserializeXml(_)));
216 }
217
218 #[test]
219 fn test_from_csv_error_direct() {
220 let data = b"a,b\n1,2,3";
221 let mut reader = csv::ReaderBuilder::new()
222 .has_headers(true)
223 .flexible(false)
224 .from_reader(&data[..]);
225 for result in reader.records() {
226 if let Err(e) = result {
227 let err: Error = e.into();
228 assert!(matches!(err, Error::Csv(_)));
229 return;
230 }
231 }
232 panic!("Expected CSV parse error");
233 }
234
235 #[test]
236 fn test_error_source_chain() {
237 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
238 let err = Error::with_source("Failed to load history", io_err);
239
240 assert!(matches!(err, Error::Context { .. }));
242 let source = err.source();
243 assert!(source.is_some());
244
245 let source = source.unwrap();
247 assert!(source.is::<std::io::Error>());
248 }
249
250 #[test]
251 fn test_context_without_source() {
252 let err = Error::context("standalone error");
253 assert!(matches!(err, Error::Context { source: None, .. }));
254 assert!(err.source().is_none());
255 }
256}