speedtest-tui 0.1.1

A terminal-based network speed test tool with real-time gauges and graphs
use anyhow::Result;
use std::time::Instant;

use super::PingResult;
use crate::utils::calculate_jitter;

const PING_SAMPLES: usize = 5;

pub async fn single_ping(url: &str) -> Result<f64> {
    use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, ORIGIN, REFERER, USER_AGENT};

    let mut headers = HeaderMap::new();
    headers.insert(USER_AGENT, HeaderValue::from_static("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"));
    headers.insert(ACCEPT, HeaderValue::from_static("*/*"));

    if url.contains("cloudflare") {
        headers.insert(
            ORIGIN,
            HeaderValue::from_static("https://speed.cloudflare.com"),
        );
        headers.insert(
            REFERER,
            HeaderValue::from_static("https://speed.cloudflare.com/"),
        );
    }

    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(10))
        .default_headers(headers)
        .build()?;

    let start = Instant::now();
    let _ = client.head(url).send().await?;
    Ok(start.elapsed().as_secs_f64() * 1000.0)
}

pub async fn measure_ping(url: &str) -> Result<PingResult> {
    let mut samples = Vec::with_capacity(PING_SAMPLES);
    let mut failures = 0;

    for _ in 0..PING_SAMPLES {
        match single_ping(url).await {
            Ok(ms) => samples.push(ms),
            Err(_) => failures += 1,
        }
        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    }

    if samples.is_empty() {
        anyhow::bail!("All ping attempts failed");
    }

    let min_ms = samples.iter().cloned().fold(f64::INFINITY, f64::min);
    let max_ms = samples.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    let avg_ms = samples.iter().sum::<f64>() / samples.len() as f64;
    let jitter_ms = calculate_jitter(&samples);
    let packet_loss = (failures as f64 / PING_SAMPLES as f64) * 100.0;

    Ok(PingResult {
        latency_ms: avg_ms,
        jitter_ms,
        min_ms,
        max_ms,
        packet_loss_percent: packet_loss,
        samples,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_ping_google() {
        let result = measure_ping("https://www.google.com").await;
        assert!(result.is_ok());
    }
}