1use thiserror::Error;
2
3#[derive(Debug, Error)]
9pub enum SpeedtestError {
10 #[error("Network error: {0}")]
12 NetworkError(#[from] reqwest::Error),
13
14 #[error("XML parse error: {0}")]
16 ParseXml(#[from] quick_xml::Error),
17
18 #[error("JSON parse error: {0}")]
20 ParseJson(#[from] serde_json::Error),
21
22 #[error("XML deserialization error: {0}")]
24 DeserializeXml(#[from] quick_xml::de::DeError),
25
26 #[error("CSV error: {0}")]
28 Csv(#[from] csv::Error),
29
30 #[error("Server not found: {0}")]
32 ServerNotFound(String),
33
34 #[error("I/O error: {0}")]
36 IoError(#[from] std::io::Error),
37
38 #[error("{msg}")]
40 Context {
41 msg: String,
42 source: Option<Box<dyn std::error::Error + Send + Sync>>,
43 },
44}
45
46impl SpeedtestError {
47 #[must_use]
49 pub fn context(msg: impl Into<String>) -> Self {
50 Self::Context {
51 msg: msg.into(),
52 source: None,
53 }
54 }
55
56 #[must_use]
58 pub fn with_source(
59 msg: impl Into<String>,
60 source: impl std::error::Error + Send + Sync + 'static,
61 ) -> Self {
62 Self::Context {
63 msg: msg.into(),
64 source: Some(Box::new(source)),
65 }
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use std::error::Error;
73
74 #[test]
75 fn test_network_error_display() {
76 let err = SpeedtestError::context("connection failed");
78 assert_eq!(format!("{err}"), "connection failed");
79 }
80
81 #[test]
82 fn test_json_error_display() {
83 let invalid_json = "{invalid}";
84 let result: Result<serde_json::Value, _> = serde_json::from_str(invalid_json);
85 assert!(result.is_err());
86 let err = SpeedtestError::from(result.unwrap_err());
87 assert!(format!("{err}").contains("JSON parse error"));
88 }
89
90 #[test]
91 fn test_server_not_found_display() {
92 let err = SpeedtestError::ServerNotFound("no servers".to_string());
93 assert_eq!(format!("{err}"), "Server not found: no servers");
94 }
95
96 #[test]
97 fn test_io_error_display() {
98 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
99 let speedtest_err = SpeedtestError::from(io_err);
100 assert!(format!("{speedtest_err}").contains("I/O error"));
101 }
102
103 #[test]
104 fn test_context_error_display() {
105 let err = SpeedtestError::context("custom error");
106 assert_eq!(format!("{err}"), "custom error");
107 }
108
109 #[test]
110 fn test_context_with_source() {
111 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
112 let err = SpeedtestError::with_source("Failed to read config", io_err);
113 assert_eq!(format!("{err}"), "Failed to read config");
114 assert!(err.source().is_some());
115 }
116
117 #[test]
118 fn test_from_io_error() {
119 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
120 let speedtest_err: SpeedtestError = io_err.into();
121 assert!(matches!(speedtest_err, SpeedtestError::IoError(_)));
122 assert!(format!("{speedtest_err}").contains("I/O error"));
123 }
124
125 #[test]
126 fn test_error_trait_implementation() {
127 let err = SpeedtestError::context("test error");
128 let _: &dyn std::error::Error = &err;
130 }
131
132 #[test]
133 fn test_debug_trait() {
134 let err = SpeedtestError::context("debug test");
135 let debug_str = format!("{err:?}");
136 assert!(debug_str.contains("Context"));
137 assert!(debug_str.contains("debug test"));
138 }
139
140 #[test]
141 fn test_from_serde_json_error() {
142 let invalid_json = "{invalid}";
143 let result: Result<serde_json::Value, _> = serde_json::from_str(invalid_json);
144 assert!(result.is_err());
145 let err: SpeedtestError = result.unwrap_err().into();
146 assert!(matches!(err, SpeedtestError::ParseJson(_)));
147 }
148
149 #[test]
150 fn test_from_quick_xml_de_error() {
151 let invalid_xml = "<unclosed>";
152 let result: Result<serde_json::Value, _> = quick_xml::de::from_str(invalid_xml);
153 assert!(result.is_err());
154 let err: SpeedtestError = result.unwrap_err().into();
155 assert!(matches!(err, SpeedtestError::DeserializeXml(_)));
156 }
157
158 #[test]
159 fn test_from_csv_error_direct() {
160 let data = b"a,b\n1,2,3";
161 let mut reader = csv::ReaderBuilder::new()
162 .has_headers(true)
163 .flexible(false)
164 .from_reader(&data[..]);
165 for result in reader.records() {
166 if let Err(e) = result {
167 let err: SpeedtestError = e.into();
168 assert!(matches!(err, SpeedtestError::Csv(_)));
169 return;
170 }
171 }
172 panic!("Expected CSV parse error");
173 }
174
175 #[test]
176 fn test_error_source_chain() {
177 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
178 let err = SpeedtestError::with_source("Failed to load history", io_err);
179
180 assert!(matches!(err, SpeedtestError::Context { .. }));
182 let source = err.source();
183 assert!(source.is_some());
184
185 let source = source.unwrap();
187 assert!(source.is::<std::io::Error>());
188 }
189
190 #[test]
191 fn test_context_without_source() {
192 let err = SpeedtestError::context("standalone error");
193 assert!(matches!(err, SpeedtestError::Context { source: None, .. }));
194 assert!(err.source().is_none());
195 }
196}