1#![allow(
2 clippy::cast_precision_loss,
3 clippy::cast_possible_truncation,
4 clippy::cast_sign_loss
5)]
6
7use crate::endpoints::ServerEndpoints;
8use crate::error::Error;
9use crate::test_config::TestConfig;
10use crate::types::Server;
11use quick_xml::de::from_str;
12use reqwest::Client;
13use serde::Deserialize;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicBool, Ordering};
16
17#[derive(Debug, Clone, Deserialize)]
20#[serde(rename = "settings")]
21struct ServerConfig {
22 #[serde(rename = "servers")]
23 servers_wrapper: ServersWrapper,
24}
25
26#[derive(Debug, Clone, Deserialize)]
28struct ServersWrapper {
29 #[serde(rename = "server", default)]
30 servers: Vec<Server>,
31}
32
33const SPEEDTEST_SERVERS_URL: &str = "https://www.speedtest.net/speedtest-servers-static.php";
34const SPEEDTEST_CONFIG_URL: &str = "https://www.speedtest.net/api/ios-config.php";
35
36#[must_use]
46pub fn calculate_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
47 const EARTH_RADIUS_KM: f64 = 6371.0;
48
49 let lat1_rad = lat1.to_radians();
50 let lat2_rad = lat2.to_radians();
51 let delta_lat = (lat2 - lat1).to_radians();
52 let delta_lon = (lon2 - lon1).to_radians();
53
54 let a = (delta_lat / 2.0).sin().powi(2)
55 + lat1_rad.cos() * lat2_rad.cos() * (delta_lon / 2.0).sin().powi(2);
56 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
57
58 EARTH_RADIUS_KM * c
59}
60
61#[derive(Debug, Clone, Deserialize, Default)]
66struct ClientConfig {
67 #[serde(rename = "client", default)]
68 client: ClientInfo,
69}
70
71#[derive(Debug, Clone, Deserialize, Default)]
73struct ClientInfo {
74 #[serde(rename = "@lat", default)]
75 lat: Option<f64>,
76 #[serde(rename = "@lon", default)]
77 lon: Option<f64>,
78 #[serde(rename = "@city", default)]
79 city: Option<String>,
80 #[serde(rename = "@country", default)]
81 country: Option<String>,
82}
83
84pub async fn fetch_client_location(client: &Client) -> Result<crate::types::ClientLocation, Error> {
94 let response = client
95 .get(SPEEDTEST_CONFIG_URL)
96 .send()
97 .await?
98 .text()
99 .await?;
100
101 let config: ClientConfig = from_str(&response)?;
102
103 match (config.client.lat, config.client.lon) {
104 (Some(lat), Some(lon)) => Ok(crate::types::ClientLocation {
105 lat,
106 lon,
107 city: config.client.city,
108 country: config.client.country,
109 }),
110 _ => Err(Error::Context {
111 msg: "Could not parse client location from config".to_string(),
112 source: None,
113 }),
114 }
115}
116
117pub async fn fetch(
127 client: &Client,
128) -> Result<(Vec<Server>, Option<crate::types::ClientLocation>), Error> {
129 let client_location = match fetch_client_location(client).await {
130 Ok(loc) => Some(loc),
131 Err(ref e) => {
132 eprintln!(
133 "Warning: could not determine client location ({e}), using default (equator)"
134 );
135 None
136 }
137 };
138
139 let (client_lat, client_lon) = client_location
141 .as_ref()
142 .map_or((0.0, 0.0), |loc| (loc.lat, loc.lon));
143
144 let response = client
145 .get(SPEEDTEST_SERVERS_URL)
146 .send()
147 .await?
148 .text()
149 .await?;
150
151 let server_config: ServerConfig = from_str(&response)?;
152
153 let mut servers = server_config.servers_wrapper.servers;
154 for server in &mut servers {
155 server.distance = calculate_distance(client_lat, client_lon, server.lat, server.lon);
156 }
157
158 servers.sort_by(|a, b| {
160 a.distance
161 .partial_cmp(&b.distance)
162 .unwrap_or(std::cmp::Ordering::Equal)
163 });
164
165 Ok((servers, client_location))
166}
167
168pub fn select_best_server(servers: &[Server]) -> Result<Server, Error> {
174 if servers.is_empty() {
175 return Err(Error::ServerNotFound("No servers available".to_string()));
176 }
177
178 let best = servers
180 .iter()
181 .min_by(|a, b| {
182 a.distance
183 .partial_cmp(&b.distance)
184 .unwrap_or(std::cmp::Ordering::Equal)
185 })
186 .cloned()
187 .ok_or_else(|| Error::ServerNotFound("No servers available".to_string()))?;
188
189 Ok(best)
190}
191
192pub async fn ping_test(
198 client: &Client,
199 server: &Server,
200) -> Result<(f64, f64, f64, Vec<f64>), Error> {
201 let config = TestConfig::default();
202 let ping_attempts = config.ping_attempts;
203 let mut latencies = Vec::new();
204
205 for _ in 0..ping_attempts {
207 let start = std::time::Instant::now();
208
209 let response = client
210 .get(ServerEndpoints::from_server_url(&server.url).latency())
211 .send()
212 .await;
213
214 let elapsed = start.elapsed().as_secs_f64() * 1000.0; if let Ok(resp) = response {
216 if resp.status().is_success() {
217 latencies.push(elapsed);
218 }
219 }
220 }
221
222 if latencies.is_empty() {
224 return Err(Error::Context {
225 msg: "All ping attempts failed".to_string(),
226 source: None,
227 });
228 }
229
230 let avg = latencies.iter().sum::<f64>() / latencies.len() as f64;
232
233 let jitter = if latencies.len() > 1 {
235 let mut jitter_sum = 0.0;
236 for i in 1..latencies.len() {
237 jitter_sum += (latencies[i] - latencies[i - 1]).abs();
238 }
239 jitter_sum / (latencies.len() - 1) as f64
241 } else {
242 0.0
243 };
244
245 let packet_loss = ((ping_attempts - latencies.len()) as f64 / ping_attempts as f64) * 100.0;
248
249 Ok((avg, jitter, packet_loss, latencies))
250}
251
252pub async fn measure_latency_under_load(
253 client: Client,
254 server_url: String,
255 samples: Arc<std::sync::Mutex<Vec<f64>>>,
256 stop: Arc<AtomicBool>,
257) {
258 let config = TestConfig::default();
259 let poll_interval = config.latency_poll_interval_ms;
260
261 while !stop.load(Ordering::Relaxed) {
262 let start = std::time::Instant::now();
263 let response = client
264 .get(ServerEndpoints::from_server_url(&server_url).latency())
265 .send()
266 .await;
267
268 if let Ok(resp) = response {
269 if resp.status().is_success() {
270 let elapsed = start.elapsed().as_secs_f64() * 1000.0;
271 if let Ok(mut lock) = samples.lock() {
272 lock.push(elapsed);
273 }
274 }
275 }
276
277 tokio::time::sleep(std::time::Duration::from_millis(poll_interval)).await;
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_select_best_server() {
287 let servers = vec![
288 Server {
289 id: "1".to_string(),
290 url: "http://server1.com".to_string(),
291 name: "Far Server".to_string(),
292 sponsor: "ISP 1".to_string(),
293 country: "US".to_string(),
294 lat: 40.0,
295 lon: -74.0,
296 distance: 5000.0,
297 },
298 Server {
299 id: "2".to_string(),
300 url: "http://server2.com".to_string(),
301 name: "Close Server".to_string(),
302 sponsor: "ISP 2".to_string(),
303 country: "US".to_string(),
304 lat: 41.0,
305 lon: -73.0,
306 distance: 100.0,
307 },
308 ];
309
310 let best = select_best_server(&servers).unwrap();
311 assert_eq!(best.id, "2");
312 assert!((best.distance - 100.0).abs() < f64::EPSILON);
313 }
314
315 #[test]
316 fn test_select_best_server_empty() {
317 let servers: Vec<Server> = vec![];
318 let result = select_best_server(&servers);
319 assert!(result.is_err());
320 assert!(matches!(result.unwrap_err(), Error::ServerNotFound(_)));
321 }
322
323 #[test]
324 fn test_select_best_server_single() {
325 let servers = vec![Server {
326 id: "1".to_string(),
327 url: "http://server1.com".to_string(),
328 name: "Only Server".to_string(),
329 sponsor: "ISP".to_string(),
330 country: "US".to_string(),
331 lat: 40.0,
332 lon: -74.0,
333 distance: 500.0,
334 }];
335
336 let best = select_best_server(&servers).unwrap();
337 assert_eq!(best.id, "1");
338 }
339
340 #[test]
341 fn test_server_distance_comparison() {
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: 300.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: 200.0,
362 },
363 Server {
364 id: "3".to_string(),
365 url: "http://server3.com".to_string(),
366 name: "Server 3".to_string(),
367 sponsor: "ISP".to_string(),
368 country: "US".to_string(),
369 lat: 42.0,
370 lon: -72.0,
371 distance: 100.0,
372 },
373 ];
374
375 let best = select_best_server(&servers).unwrap();
376 assert_eq!(best.id, "3");
377 }
378
379 #[test]
380 fn test_server_with_equal_distances() {
381 let servers = vec![
382 Server {
383 id: "1".to_string(),
384 url: "http://server1.com".to_string(),
385 name: "Server 1".to_string(),
386 sponsor: "ISP".to_string(),
387 country: "US".to_string(),
388 lat: 40.0,
389 lon: -74.0,
390 distance: 100.0,
391 },
392 Server {
393 id: "2".to_string(),
394 url: "http://server2.com".to_string(),
395 name: "Server 2".to_string(),
396 sponsor: "ISP".to_string(),
397 country: "US".to_string(),
398 lat: 41.0,
399 lon: -73.0,
400 distance: 100.0,
401 },
402 ];
403
404 let best = select_best_server(&servers).unwrap();
405 assert!(best.id == "1" || best.id == "2");
407 }
408
409 #[test]
410 fn test_ping_test_average_calculation() {
411 let latencies = [10.0, 20.0, 15.0, 25.0];
412 let avg = latencies.iter().sum::<f64>() / latencies.len() as f64;
413 assert!((avg - 17.5).abs() < f64::EPSILON);
414 }
415
416 #[test]
417 fn test_ping_test_empty_handling() {
418 let latencies: Vec<f64> = vec![];
419 assert!(latencies.is_empty());
420 }
421
422 #[test]
423 fn test_calculate_distance_same_location() {
424 let dist = calculate_distance(40.7128, -74.0060, 40.7128, -74.0060);
425 assert!(dist < 0.01);
426 }
427
428 #[test]
429 fn test_calculate_distance_nyc_la() {
430 let dist = calculate_distance(40.7128, -74.0060, 34.0522, -118.2437);
431 assert!((dist - 3944.0).abs() < 200.0);
432 }
433
434 #[test]
435 fn test_calculate_distance_nyc_london() {
436 let dist = calculate_distance(40.7128, -74.0060, 51.5074, -0.1278);
437 assert!((dist - 5570.0).abs() < 300.0);
438 }
439
440 #[test]
441 fn test_client_config_deserialization() {
442 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
443<settings>
444 <client lat="40.7128" lon="-74.0060" ip="192.168.1.1" />
445</settings>"#;
446 let config: ClientConfig = from_str(xml).unwrap();
447 assert_eq!(config.client.lat, Some(40.7128));
448 assert_eq!(config.client.lon, Some(-74.0060));
449 }
450
451 #[test]
452 fn test_client_config_missing_coords() {
453 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
454<settings>
455 <client ip="192.168.1.1" />
456</settings>"#;
457 let config: ClientConfig = from_str(xml).unwrap();
458 assert!(config.client.lat.is_none());
459 assert!(config.client.lon.is_none());
460 }
461
462 #[test]
463 fn test_calculate_distance_sydney_tokyo() {
464 let dist = calculate_distance(-33.8688, 151.2093, 35.6762, 139.6503);
465 assert!((dist - 7823.0).abs() < 300.0);
466 }
467
468 #[test]
469 fn test_calculate_distance_opposite_sides() {
470 let dist = calculate_distance(40.7128, -74.0060, -33.8688, 151.2093);
472 assert!(dist > 15_000.0); }
474
475 #[test]
476 fn test_calculate_distance_equator() {
477 let dist = calculate_distance(0.0, 0.0, 0.0, 10.0);
479 assert!((dist - 1111.0).abs() < 100.0); }
481
482 #[test]
483 fn test_server_config_deserialization() {
484 let xml = r#"<?xml version="1.0"?>
485<settings>
486 <servers>
487 <server url="http://server1.com/speedtest/upload.php" name="Server 1" sponsor="ISP 1" country="US" id="1" lat="40.0" lon="-74.0" />
488 <server url="http://server2.com/speedtest/upload.php" name="Server 2" sponsor="ISP 2" country="CA" id="2" lat="43.0" lon="-79.0" />
489 </servers>
490</settings>"#;
491 let config: ServerConfig = from_str(xml).unwrap();
492 assert_eq!(config.servers_wrapper.servers.len(), 2);
493 assert_eq!(config.servers_wrapper.servers[0].id, "1");
494 assert_eq!(config.servers_wrapper.servers[1].country, "CA");
495 }
496
497 #[test]
498 fn test_server_distance_comparison_with_negative_coords() {
499 let servers = vec![
501 Server {
502 id: "1".to_string(),
503 url: "http://server1.com".to_string(),
504 name: "Southern".to_string(),
505 sponsor: "ISP".to_string(),
506 country: "AU".to_string(),
507 lat: -33.8688,
508 lon: 151.2093,
509 distance: 15_000.0,
510 },
511 Server {
512 id: "2".to_string(),
513 url: "http://server2.com".to_string(),
514 name: "Northern".to_string(),
515 sponsor: "ISP".to_string(),
516 country: "US".to_string(),
517 lat: 40.7128,
518 lon: -74.0060,
519 distance: 100.0,
520 },
521 ];
522
523 let best = select_best_server(&servers).unwrap();
524 assert_eq!(best.id, "2"); }
526
527 #[test]
528 fn test_servers_wrapper_empty_deserialization() {
529 let xml = r#"<?xml version="1.0"?>
530<settings>
531 <servers>
532 </servers>
533</settings>"#;
534 let config: ServerConfig = from_str(xml).unwrap();
535 assert!(config.servers_wrapper.servers.is_empty());
536 }
537}