1use crate::error::Error;
6
7pub trait ResultSink {
9 fn write_report(&self, report: &crate::domain::reporting::Report) -> Result<(), Error>;
10}
11
12use crate::types::TestResult;
13
14pub trait SaveResult: Send + Sync {
17 fn save(&self, result: &TestResult) -> Result<(), Error>;
18}
19
20pub trait LoadHistory: Send + Sync {
24 fn load_recent(&self, limit: usize) -> Result<Vec<TestResult>, Error>;
25 fn clear(&self) -> Result<(), Error>;
26}
27
28pub trait HistoryStorage: Send + Sync {
31 fn save(&self, result: &TestResult) -> Result<(), Error>;
32 fn load_history(&self, limit: usize) -> Result<Vec<TestResult>, Error>;
33 fn clear_history(&self) -> Result<(), Error>;
34}
35
36impl<T: SaveResult + LoadHistory> HistoryStorage for T {
37 fn save(&self, result: &TestResult) -> Result<(), Error> {
38 SaveResult::save(self, result)
39 }
40
41 fn load_history(&self, limit: usize) -> Result<Vec<TestResult>, Error> {
42 LoadHistory::load_recent(self, limit)
43 }
44
45 fn clear_history(&self) -> Result<(), Error> {
46 LoadHistory::clear(self)
47 }
48}
49
50pub struct FileStorage {
52 _path: std::path::PathBuf,
54}
55
56impl FileStorage {
57 pub fn new() -> Self {
58 Self {
59 _path: std::path::PathBuf::new(),
60 }
61 }
62
63 pub fn with_path(path: std::path::PathBuf) -> Self {
64 Self { _path: path }
65 }
66}
67
68impl Default for FileStorage {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl SaveResult for FileStorage {
75 fn save(&self, result: &TestResult) -> Result<(), Error> {
76 crate::history::save_result(result)
77 }
78}
79
80impl LoadHistory for FileStorage {
81 fn load_recent(&self, limit: usize) -> Result<Vec<TestResult>, Error> {
82 let entries = crate::history::load()?;
83 let converted: Vec<TestResult> = entries
84 .into_iter()
85 .rev()
86 .take(limit)
87 .map(|e| TestResult {
88 timestamp: e.timestamp,
89 server: crate::types::ServerInfo {
90 id: "0".to_string(),
91 name: e.server_name,
92 sponsor: e.sponsor,
93 country: "".to_string(),
94 distance: 0.0,
95 },
96 ping: e.ping,
97 jitter: e.jitter,
98 packet_loss: e.packet_loss,
99 download: e.download,
100 download_peak: e.download_peak,
101 upload: e.upload,
102 upload_peak: e.upload_peak,
103 latency_download: e.latency_download,
104 latency_upload: e.latency_upload,
105 client_ip: e.client_ip,
106 ..TestResult::default()
107 })
108 .collect();
109 Ok(converted)
110 }
111
112 fn clear(&self) -> Result<(), Error> {
113 Ok(())
114 }
115}
116
117impl ResultSink for FileStorage {
118 fn write_report(&self, report: &crate::domain::reporting::Report) -> Result<(), Error> {
119 crate::history::save_report(report)
121 }
122}
123
124pub struct MockStorage {
126 results: std::sync::Mutex<Vec<TestResult>>,
127}
128
129impl MockStorage {
130 pub fn new() -> Self {
131 Self {
132 results: std::sync::Mutex::new(Vec::new()),
133 }
134 }
135
136 pub fn with_results(results: Vec<TestResult>) -> Self {
137 Self {
138 results: std::sync::Mutex::new(results),
139 }
140 }
141}
142
143impl Default for MockStorage {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149impl SaveResult for MockStorage {
150 fn save(&self, result: &TestResult) -> Result<(), Error> {
151 let mut guard = self
152 .results
153 .lock()
154 .map_err(|e| Error::context(format!("mock storage lock poisoned: {e}")))?;
155 guard.push(result.clone());
156 Ok(())
157 }
158}
159
160impl LoadHistory for MockStorage {
161 fn load_recent(&self, limit: usize) -> Result<Vec<TestResult>, Error> {
162 let guard = self
163 .results
164 .lock()
165 .map_err(|e| Error::context(format!("mock storage lock poisoned: {e}")))?;
166 Ok(guard.iter().rev().take(limit).cloned().collect())
167 }
168
169 fn clear(&self) -> Result<(), Error> {
170 let mut guard = self
171 .results
172 .lock()
173 .map_err(|e| Error::context(format!("mock storage lock poisoned: {e}")))?;
174 guard.clear();
175 Ok(())
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn make_test_result(id: &str) -> TestResult {
184 TestResult {
185 status: "ok".to_string(),
186 version: "0.0.0".to_string(),
187 test_id: Some(id.to_string()),
188 server: crate::types::ServerInfo {
189 id: id.to_string(),
190 name: "Test".to_string(),
191 sponsor: "ISP".to_string(),
192 country: "US".to_string(),
193 distance: 100.0,
194 },
195 ping: Some(10.0),
196 jitter: Some(1.0),
197 packet_loss: Some(0.0),
198 download: Some(100_000_000.0),
199 download_peak: Some(120_000_000.0),
200 upload: Some(50_000_000.0),
201 upload_peak: Some(60_000_000.0),
202 download_cv: None,
203 upload_cv: None,
204 download_ci_95: None,
205 upload_ci_95: None,
206 latency_download: None,
207 latency_upload: None,
208 download_samples: None,
209 upload_samples: None,
210 ping_samples: None,
211 timestamp: "2026-01-01T00:00:00Z".to_string(),
212 client_ip: Some("1.2.3.4".to_string()),
213 client_location: None,
214 overall_grade: None,
215 download_grade: None,
216 upload_grade: None,
217 connection_rating: None,
218 phases: crate::types::TestPhases {
219 ping: crate::types::PhaseResult::completed(),
220 download: crate::types::PhaseResult::completed(),
221 upload: crate::types::PhaseResult::completed(),
222 },
223 }
224 }
225
226 #[test]
227 fn test_mock_storage_save_load_round_trip() {
228 let storage = MockStorage::new();
229 let result = make_test_result("abc");
230
231 <dyn SaveResult>::save(&storage, &result).unwrap();
232
233 let loaded = <dyn LoadHistory>::load_recent(&storage, 10).unwrap();
234 assert_eq!(loaded.len(), 1);
235 assert_eq!(loaded[0].test_id, Some("abc".to_string()));
236 }
237
238 #[test]
239 fn test_mock_storage_with_results() {
240 let r1 = make_test_result("first");
241 let r2 = make_test_result("second");
242 let storage = MockStorage::with_results(vec![r1, r2]);
243
244 let loaded = <dyn LoadHistory>::load_recent(&storage, 10).unwrap();
245 assert_eq!(loaded.len(), 2);
246 }
247
248 #[test]
249 fn test_mock_storage_load_recent_limit() {
250 let storage = MockStorage::with_results(vec![
251 make_test_result("a"),
252 make_test_result("b"),
253 make_test_result("c"),
254 ]);
255
256 let loaded = <dyn LoadHistory>::load_recent(&storage, 2).unwrap();
257 assert_eq!(loaded.len(), 2);
258 assert_eq!(loaded[0].test_id, Some("c".to_string()));
259 assert_eq!(loaded[1].test_id, Some("b".to_string()));
260 }
261
262 #[test]
263 fn test_mock_storage_clear() {
264 let storage = MockStorage::with_results(vec![make_test_result("x")]);
265 assert_eq!(
266 <dyn LoadHistory>::load_recent(&storage, 10).unwrap().len(),
267 1
268 );
269
270 <dyn LoadHistory>::clear(&storage).unwrap();
271 assert!(
272 <dyn LoadHistory>::load_recent(&storage, 10)
273 .unwrap()
274 .is_empty()
275 );
276 }
277
278 #[test]
279 fn test_mock_storage_empty_load() {
280 let storage = MockStorage::new();
281 let loaded = <dyn LoadHistory>::load_recent(&storage, 10).unwrap();
282 assert!(loaded.is_empty());
283 }
284
285 #[test]
286 #[serial_test::serial]
287 fn test_history_storage_for_file_storage() {
288 let storage = crate::storage::FileStorage::new();
289 let result = make_test_result("hist");
290 if <dyn SaveResult>::save(&storage, &result).is_err() {
291 return;
292 }
293 if let Ok(loaded) = <dyn HistoryStorage>::load_history(&storage, 1) {
294 assert_eq!(loaded.len(), 1);
295 let _ = <dyn HistoryStorage>::clear_history(&storage);
296 }
297 }
298}