1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Deserialize, Serialize)]
4pub struct Server {
5 #[serde(rename = "@id")]
6 pub id: String,
7 #[serde(rename = "@url")]
8 pub url: String,
9 #[serde(rename = "@name")]
10 pub name: String,
11 #[serde(rename = "@sponsor")]
12 pub sponsor: String,
13 #[serde(rename = "@country")]
14 pub country: String,
15 #[serde(rename = "@lat")]
16 pub lat: f64,
17 #[serde(rename = "@lon")]
18 pub lon: f64,
19 #[serde(skip)]
20 pub distance: f64,
21}
22
23#[derive(Debug, Clone, Serialize)]
24pub struct TestResult {
25 pub server: ServerInfo,
26 pub ping: Option<f64>,
27 pub jitter: Option<f64>,
28 pub packet_loss: Option<f64>,
29 pub download: Option<f64>,
30 pub download_peak: Option<f64>,
31 pub upload: Option<f64>,
32 pub upload_peak: Option<f64>,
33 pub latency_download: Option<f64>,
34 pub latency_upload: Option<f64>,
35 pub download_samples: Option<Vec<f64>>,
36 pub upload_samples: Option<Vec<f64>>,
37 pub ping_samples: Option<Vec<f64>>,
38 pub timestamp: String,
39 pub client_ip: Option<String>,
40}
41
42impl TestResult {
43 #[allow(clippy::too_many_arguments)]
45 #[must_use]
46 pub fn from_test_runs(
47 server: ServerInfo,
48 ping: Option<f64>,
49 jitter: Option<f64>,
50 packet_loss: Option<f64>,
51 ping_samples: Vec<f64>,
52 dl: &crate::test_runner::TestRunResult,
53 ul: &crate::test_runner::TestRunResult,
54 client_ip: Option<String>,
55 ) -> Self {
56 fn opt_samples(v: &[f64]) -> Option<Vec<f64>> {
57 if v.is_empty() { None } else { Some(v.to_vec()) }
58 }
59 fn opt_positive(v: f64) -> Option<f64> {
60 if v > 0.0 { Some(v) } else { None }
61 }
62
63 Self {
64 server,
65 ping,
66 jitter,
67 packet_loss,
68 download: opt_positive(dl.avg_bps),
69 download_peak: opt_positive(dl.peak_bps),
70 upload: opt_positive(ul.avg_bps),
71 upload_peak: opt_positive(ul.peak_bps),
72 latency_download: dl.latency_under_load,
73 latency_upload: ul.latency_under_load,
74 download_samples: opt_samples(&dl.speed_samples),
75 upload_samples: opt_samples(&ul.speed_samples),
76 ping_samples: opt_samples(&ping_samples),
77 timestamp: chrono::Utc::now().to_rfc3339(),
78 client_ip,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Serialize)]
84pub struct ServerInfo {
85 pub id: String,
86 pub name: String,
87 pub sponsor: String,
88 pub country: String,
89 pub distance: f64,
90}
91
92#[derive(Debug, Clone, Serialize)]
93pub struct CsvOutput {
94 pub server_id: String,
95 pub sponsor: String,
96 pub server_name: String,
97 pub timestamp: String,
98 pub distance: f64,
99 pub ping: f64,
100 pub jitter: f64,
101 pub packet_loss: f64,
102 pub download: f64,
103 pub download_peak: f64,
104 pub upload: f64,
105 pub upload_peak: f64,
106 pub ip_address: String,
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn test_server_serialization() {
115 let server = Server {
116 id: "1234".to_string(),
117 url: "http://example.com".to_string(),
118 name: "Test Server".to_string(),
119 sponsor: "Test ISP".to_string(),
120 country: "US".to_string(),
121 lat: 40.7128,
122 lon: -74.0060,
123 distance: 100.5,
124 };
125
126 let json = serde_json::to_string(&server).unwrap();
127 assert!(json.contains("\"1234\""));
129 assert!(json.contains("\"Test Server\""));
130 }
131
132 #[test]
133 fn test_test_result_serialization() {
134 let result = TestResult {
135 server: ServerInfo {
136 id: "1234".to_string(),
137 name: "Test Server".to_string(),
138 sponsor: "Test ISP".to_string(),
139 country: "US".to_string(),
140 distance: 100.5,
141 },
142 ping: Some(15.234),
143 jitter: Some(1.2),
144 packet_loss: Some(0.0),
145 download: Some(150_000_000.0),
146 download_peak: Some(180_000_000.0),
147 upload: Some(50_000_000.0),
148 upload_peak: Some(60_000_000.0),
149 latency_download: Some(25.0),
150 latency_upload: Some(30.0),
151 download_samples: None,
152 upload_samples: None,
153 ping_samples: None,
154 timestamp: "2026-04-04T12:00:00Z".to_string(),
155 client_ip: Some("192.168.1.1".to_string()),
156 };
157
158 let json = serde_json::to_string(&result).unwrap();
159 assert!(json.contains("\"ping\":15.234"));
160 assert!(json.contains("\"jitter\":1.2"));
161 assert!(json.contains("\"download\":150000000.0"));
162 }
163
164 #[test]
165 fn test_csv_output_serialization() {
166 let csv = CsvOutput {
167 server_id: "1234".to_string(),
168 sponsor: "Test ISP".to_string(),
169 server_name: "Test Server".to_string(),
170 timestamp: "2026-04-04T12:00:00Z".to_string(),
171 distance: 100.5,
172 ping: 15.234,
173 jitter: 1.2,
174 packet_loss: 0.0,
175 download: 150_000_000.0,
176 download_peak: 180_000_000.0,
177 upload: 50_000_000.0,
178 upload_peak: 60_000_000.0,
179 ip_address: "192.168.1.1".to_string(),
180 };
181
182 let json = serde_json::to_string(&csv).unwrap();
183 assert!(json.contains("\"server_id\":\"1234\""));
184 assert!(json.contains("\"ping\":15.234"));
185 assert!(json.contains("\"jitter\":1.2"));
186 }
187
188 #[test]
189 fn test_server_clone() {
190 let server = Server {
191 id: "1234".to_string(),
192 url: "http://example.com".to_string(),
193 name: "Test".to_string(),
194 sponsor: "ISP".to_string(),
195 country: "US".to_string(),
196 lat: 40.0,
197 lon: -74.0,
198 distance: 0.0,
199 };
200
201 let cloned = server.clone();
202 assert_eq!(cloned.id, server.id);
203 assert_eq!(cloned.name, server.name);
204 }
205}