1#![allow(
2 clippy::cast_precision_loss,
3 clippy::cast_possible_truncation,
4 clippy::cast_sign_loss
5)]
6
7use crate::error::SpeedtestError;
8use crate::types::Server;
9use quick_xml::de::from_str;
10use reqwest::Client;
11use serde::Deserialize;
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15#[derive(Debug, Clone, Deserialize)]
18#[serde(rename = "settings")]
19struct ServerConfig {
20 #[serde(rename = "servers")]
21 servers_wrapper: ServersWrapper,
22}
23
24#[derive(Debug, Clone, Deserialize)]
26struct ServersWrapper {
27 #[serde(rename = "server", default)]
28 servers: Vec<Server>,
29}
30
31const SPEEDTEST_SERVERS_URL: &str = "https://www.speedtest.net/speedtest-servers-static.php";
32const SPEEDTEST_CONFIG_URL: &str = "https://www.speedtest.net/api/ios-config.php";
33
34pub fn calculate_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
44 const EARTH_RADIUS_KM: f64 = 6371.0;
45
46 let lat1_rad = lat1.to_radians();
47 let lat2_rad = lat2.to_radians();
48 let delta_lat = (lat2 - lat1).to_radians();
49 let delta_lon = (lon2 - lon1).to_radians();
50
51 let a = (delta_lat / 2.0).sin().powi(2)
52 + lat1_rad.cos() * lat2_rad.cos() * (delta_lon / 2.0).sin().powi(2);
53 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
54
55 EARTH_RADIUS_KM * c
56}
57
58#[derive(Debug, Clone, Deserialize)]
60struct ClientConfig {
61 #[serde(rename = "client")]
62 client: ClientInfo,
63}
64
65#[derive(Debug, Clone, Deserialize)]
66struct ClientInfo {
67 #[serde(rename = "@lat")]
68 lat: Option<f64>,
69 #[serde(rename = "@lon")]
70 lon: Option<f64>,
71}
72
73async fn fetch_client_location(client: &Client) -> Result<(f64, f64), SpeedtestError> {
75 let response = client
76 .get(SPEEDTEST_CONFIG_URL)
77 .send()
78 .await?
79 .text()
80 .await?;
81
82 let config: ClientConfig = from_str(&response)?;
83
84 match (config.client.lat, config.client.lon) {
85 (Some(lat), Some(lon)) => Ok((lat, lon)),
86 _ => Err(SpeedtestError::Context {
87 msg: "Could not parse client location from config".to_string(),
88 source: None,
89 }),
90 }
91}
92
93pub async fn fetch_servers(client: &Client) -> Result<Vec<Server>, SpeedtestError> {
100 let (client_lat, client_lon) = match fetch_client_location(client).await {
101 Ok(coords) => coords,
102 Err(ref e) => {
103 eprintln!(
104 "Warning: could not determine client location ({e}), using default (equator)"
105 );
106 (0.0, 0.0)
107 }
108 };
109
110 let response = client
111 .get(SPEEDTEST_SERVERS_URL)
112 .send()
113 .await?
114 .text()
115 .await?;
116
117 let server_config: ServerConfig = from_str(&response)?;
118
119 let mut servers = server_config.servers_wrapper.servers;
120 for server in &mut servers {
121 server.distance = calculate_distance(client_lat, client_lon, server.lat, server.lon);
122 }
123
124 servers.sort_by(|a, b| {
126 a.distance
127 .partial_cmp(&b.distance)
128 .unwrap_or(std::cmp::Ordering::Equal)
129 });
130
131 Ok(servers)
132}
133
134pub fn select_best_server(servers: &[Server]) -> Result<Server, SpeedtestError> {
140 if servers.is_empty() {
141 return Err(SpeedtestError::ServerNotFound(
142 "No servers available".to_string(),
143 ));
144 }
145
146 let best = servers
148 .iter()
149 .min_by(|a, b| {
150 a.distance
151 .partial_cmp(&b.distance)
152 .unwrap_or(std::cmp::Ordering::Equal)
153 })
154 .cloned()
155 .ok_or_else(|| SpeedtestError::ServerNotFound("No servers available".to_string()))?;
156
157 Ok(best)
158}
159
160pub async fn ping_test(
166 client: &Client,
167 server: &Server,
168) -> Result<(f64, f64, f64, Vec<f64>), SpeedtestError> {
169 const PING_ATTEMPTS: usize = 8;
170 let mut latencies = Vec::new();
171
172 for _ in 0..PING_ATTEMPTS {
174 let start = std::time::Instant::now();
175
176 let response = client
177 .get(format!("{}/latency.txt", server.url))
178 .send()
179 .await;
180
181 let elapsed = start.elapsed().as_secs_f64() * 1000.0; if let Ok(resp) = response {
183 if resp.status().is_success() {
184 latencies.push(elapsed);
185 }
186 }
187 }
188
189 if latencies.is_empty() {
191 return Err(SpeedtestError::Context {
192 msg: "All ping attempts failed".to_string(),
193 source: None,
194 });
195 }
196
197 let avg = latencies.iter().sum::<f64>() / latencies.len() as f64;
198
199 let jitter = if latencies.len() > 1 {
201 let mut jitter_sum = 0.0;
202 for i in 1..latencies.len() {
203 jitter_sum += (latencies[i] - latencies[i - 1]).abs();
204 }
205 jitter_sum / (latencies.len() - 1) as f64
206 } else {
207 0.0
208 };
209
210 let packet_loss = ((PING_ATTEMPTS - latencies.len()) as f64 / PING_ATTEMPTS as f64) * 100.0;
212
213 Ok((avg, jitter, packet_loss, latencies))
214}
215
216pub async fn measure_latency_under_load(
217 client: Client,
218 server_url: String,
219 samples: Arc<std::sync::Mutex<Vec<f64>>>,
220 stop: Arc<AtomicBool>,
221) {
222 while !stop.load(Ordering::Relaxed) {
223 let start = std::time::Instant::now();
224 let response = client.get(format!("{server_url}/latency.txt")).send().await;
225
226 if let Ok(resp) = response {
227 if resp.status().is_success() {
228 let elapsed = start.elapsed().as_secs_f64() * 1000.0;
229 if let Ok(mut lock) = samples.lock() {
230 lock.push(elapsed);
231 }
232 }
233 }
234
235 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_select_best_server() {
245 let servers = vec![
246 Server {
247 id: "1".to_string(),
248 url: "http://server1.com".to_string(),
249 name: "Far Server".to_string(),
250 sponsor: "ISP 1".to_string(),
251 country: "US".to_string(),
252 lat: 40.0,
253 lon: -74.0,
254 distance: 5000.0,
255 },
256 Server {
257 id: "2".to_string(),
258 url: "http://server2.com".to_string(),
259 name: "Close Server".to_string(),
260 sponsor: "ISP 2".to_string(),
261 country: "US".to_string(),
262 lat: 41.0,
263 lon: -73.0,
264 distance: 100.0,
265 },
266 ];
267
268 let best = select_best_server(&servers).unwrap();
269 assert_eq!(best.id, "2");
270 assert_eq!(best.distance, 100.0);
271 }
272
273 #[test]
274 fn test_select_best_server_empty() {
275 let servers: Vec<Server> = vec![];
276 let result = select_best_server(&servers);
277 assert!(result.is_err());
278 assert!(matches!(
279 result.unwrap_err(),
280 SpeedtestError::ServerNotFound(_)
281 ));
282 }
283
284 #[test]
285 fn test_select_best_server_single() {
286 let servers = vec![Server {
287 id: "1".to_string(),
288 url: "http://server1.com".to_string(),
289 name: "Only Server".to_string(),
290 sponsor: "ISP".to_string(),
291 country: "US".to_string(),
292 lat: 40.0,
293 lon: -74.0,
294 distance: 500.0,
295 }];
296
297 let best = select_best_server(&servers).unwrap();
298 assert_eq!(best.id, "1");
299 }
300
301 #[test]
302 fn test_server_distance_comparison() {
303 let servers = vec![
304 Server {
305 id: "1".to_string(),
306 url: "http://server1.com".to_string(),
307 name: "Server 1".to_string(),
308 sponsor: "ISP".to_string(),
309 country: "US".to_string(),
310 lat: 40.0,
311 lon: -74.0,
312 distance: 300.0,
313 },
314 Server {
315 id: "2".to_string(),
316 url: "http://server2.com".to_string(),
317 name: "Server 2".to_string(),
318 sponsor: "ISP".to_string(),
319 country: "US".to_string(),
320 lat: 41.0,
321 lon: -73.0,
322 distance: 200.0,
323 },
324 Server {
325 id: "3".to_string(),
326 url: "http://server3.com".to_string(),
327 name: "Server 3".to_string(),
328 sponsor: "ISP".to_string(),
329 country: "US".to_string(),
330 lat: 42.0,
331 lon: -72.0,
332 distance: 100.0,
333 },
334 ];
335
336 let best = select_best_server(&servers).unwrap();
337 assert_eq!(best.id, "3");
338 }
339
340 #[test]
341 fn test_server_with_equal_distances() {
342 let servers = vec![
343 Server {
344 id: "1".to_string(),
345 url: "http://server1.com".to_string(),
346 name: "Server 1".to_string(),
347 sponsor: "ISP".to_string(),
348 country: "US".to_string(),
349 lat: 40.0,
350 lon: -74.0,
351 distance: 100.0,
352 },
353 Server {
354 id: "2".to_string(),
355 url: "http://server2.com".to_string(),
356 name: "Server 2".to_string(),
357 sponsor: "ISP".to_string(),
358 country: "US".to_string(),
359 lat: 41.0,
360 lon: -73.0,
361 distance: 100.0,
362 },
363 ];
364
365 let best = select_best_server(&servers).unwrap();
366 assert!(best.id == "1" || best.id == "2");
368 }
369
370 #[test]
371 fn test_ping_test_average_calculation() {
372 let latencies = [10.0, 20.0, 15.0, 25.0];
373 let avg = latencies.iter().sum::<f64>() / latencies.len() as f64;
374 assert_eq!(avg, 17.5);
375 }
376
377 #[test]
378 fn test_ping_test_empty_handling() {
379 let latencies: Vec<f64> = vec![];
380 assert!(latencies.is_empty());
381 }
382
383 #[test]
384 fn test_calculate_distance_same_location() {
385 let dist = calculate_distance(40.7128, -74.0060, 40.7128, -74.0060);
386 assert!(dist < 0.01);
387 }
388
389 #[test]
390 fn test_calculate_distance_nyc_la() {
391 let dist = calculate_distance(40.7128, -74.0060, 34.0522, -118.2437);
392 assert!((dist - 3944.0).abs() < 200.0);
393 }
394
395 #[test]
396 fn test_calculate_distance_nyc_london() {
397 let dist = calculate_distance(40.7128, -74.0060, 51.5074, -0.1278);
398 assert!((dist - 5570.0).abs() < 300.0);
399 }
400
401 #[test]
402 fn test_client_config_deserialization() {
403 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
404<settings>
405 <client lat="40.7128" lon="-74.0060" ip="192.168.1.1" />
406</settings>"#;
407 let config: ClientConfig = from_str(xml).unwrap();
408 assert_eq!(config.client.lat, Some(40.7128));
409 assert_eq!(config.client.lon, Some(-74.0060));
410 }
411
412 #[test]
413 fn test_client_config_missing_coords() {
414 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
415<settings>
416 <client ip="192.168.1.1" />
417</settings>"#;
418 let config: ClientConfig = from_str(xml).unwrap();
419 assert!(config.client.lat.is_none());
420 assert!(config.client.lon.is_none());
421 }
422
423 #[test]
424 fn test_calculate_distance_sydney_tokyo() {
425 let dist = calculate_distance(-33.8688, 151.2093, 35.6762, 139.6503);
426 assert!((dist - 7823.0).abs() < 300.0);
427 }
428
429 #[test]
430 fn test_calculate_distance_opposite_sides() {
431 let dist = calculate_distance(40.7128, -74.0060, -33.8688, 151.2093);
433 assert!(dist > 15_000.0); }
435
436 #[test]
437 fn test_calculate_distance_equator() {
438 let dist = calculate_distance(0.0, 0.0, 0.0, 10.0);
440 assert!((dist - 1111.0).abs() < 100.0); }
442
443 #[test]
444 fn test_server_config_deserialization() {
445 let xml = r#"<?xml version="1.0"?>
446<settings>
447 <servers>
448 <server url="http://server1.com/speedtest/upload.php" name="Server 1" sponsor="ISP 1" country="US" id="1" lat="40.0" lon="-74.0" />
449 <server url="http://server2.com/speedtest/upload.php" name="Server 2" sponsor="ISP 2" country="CA" id="2" lat="43.0" lon="-79.0" />
450 </servers>
451</settings>"#;
452 let config: ServerConfig = from_str(xml).unwrap();
453 assert_eq!(config.servers_wrapper.servers.len(), 2);
454 assert_eq!(config.servers_wrapper.servers[0].id, "1");
455 assert_eq!(config.servers_wrapper.servers[1].country, "CA");
456 }
457
458 #[test]
459 fn test_server_distance_comparison_with_negative_coords() {
460 let servers = vec![
462 Server {
463 id: "1".to_string(),
464 url: "http://server1.com".to_string(),
465 name: "Southern".to_string(),
466 sponsor: "ISP".to_string(),
467 country: "AU".to_string(),
468 lat: -33.8688,
469 lon: 151.2093,
470 distance: 15_000.0,
471 },
472 Server {
473 id: "2".to_string(),
474 url: "http://server2.com".to_string(),
475 name: "Northern".to_string(),
476 sponsor: "ISP".to_string(),
477 country: "US".to_string(),
478 lat: 40.7128,
479 lon: -74.0060,
480 distance: 100.0,
481 },
482 ];
483
484 let best = select_best_server(&servers).unwrap();
485 assert_eq!(best.id, "2"); }
487
488 #[test]
489 fn test_servers_wrapper_empty_deserialization() {
490 let xml = r#"<?xml version="1.0"?>
491<settings>
492 <servers>
493 </servers>
494</settings>"#;
495 let config: ServerConfig = from_str(xml).unwrap();
496 assert!(config.servers_wrapper.servers.is_empty());
497 }
498}