Skip to main content

cli_speedtest/
lib.rs

1// src/lib.rs
2
3pub mod client;
4pub mod menu;
5pub mod models;
6pub mod theme;
7pub mod utils;
8
9use chrono::Utc;
10use models::{AppConfig, RunArgs, Server, SpeedTestResult};
11use reqwest::Client;
12use std::sync::Arc;
13use utils::WARMUP_SECS;
14
15const DEFAULT_SERVER_URL: &str = "https://speed.cloudflare.com";
16
17/// Core application logic — fully decoupled from clap so integration tests can
18/// call it directly with a mockito server URL via `RunArgs::server_url`.
19pub async fn run(
20    args: RunArgs,
21    config: Arc<AppConfig>,
22    client: Client,
23) -> anyhow::Result<SpeedTestResult> {
24    if args.duration_secs <= WARMUP_SECS as u64 {
25        anyhow::bail!(
26            "Duration must be greater than {} seconds (warm-up period). Got: {}s",
27            WARMUP_SECS,
28            args.duration_secs
29        );
30    }
31
32    if args.ping_count == 0 {
33        anyhow::bail!("--ping-count must be at least 1");
34    }
35
36    // Derive display name: if the URL is the default, label it "Cloudflare";
37    // otherwise show the URL itself so users know which custom server was used.
38    let server = Server {
39        name: if args.server_url == DEFAULT_SERVER_URL {
40            "Cloudflare".into()
41        } else {
42            args.server_url.clone()
43        },
44        base_url: args.server_url.clone(),
45    };
46
47    if !config.quiet {
48        println!("🔍 Using server: {}\n", server.name);
49    }
50
51    // --- Ping / Jitter / Packet Loss ---
52    let ping_stats = client::test_ping_stats(
53        &client,
54        &server.base_url,
55        args.ping_count,
56        Arc::clone(&config),
57    )
58    .await?;
59
60    // --- Download (skipped if --no-download) ---
61    let down_speed: Option<f64> = if args.no_download {
62        if !config.quiet {
63            println!("⬇️  Download: skipped\n");
64        }
65        None
66    } else {
67        let conns = args.connections.unwrap_or(8);
68        let speed = client::test_download(
69            &client,
70            &server.base_url,
71            args.duration_secs,
72            conns,
73            Arc::clone(&config),
74        )
75        .await?;
76        if !config.quiet {
77            println!("⬇️  Download Speed: {:.2} Mbps\n", speed);
78        }
79        Some(speed)
80    };
81
82    // --- Upload (skipped if --no-upload) ---
83    let up_speed: Option<f64> = if args.no_upload {
84        if !config.quiet {
85            println!("⬆️  Upload: skipped\n");
86        }
87        None
88    } else {
89        let conns = args.connections.unwrap_or(4);
90        let speed = client::test_upload(
91            &client,
92            &server.base_url,
93            args.duration_secs,
94            conns,
95            Arc::clone(&config),
96        )
97        .await?;
98        if !config.quiet {
99            println!("⬆️  Upload Speed: {:.2} Mbps\n", speed);
100        }
101        Some(speed)
102    };
103
104    // --- Summary box ---
105    if !config.quiet {
106        let term_cols = console::Term::stdout().size().1 as usize;
107        let box_width = term_cols.saturating_sub(4).clamp(44, 60);
108        let inner_width = box_width - 2;
109
110        println!("╔{}╗", "═".repeat(inner_width));
111        println!("║{:^width$}║", "📊 Test Summary", width = inner_width);
112        println!("╠{}╣", "═".repeat(inner_width));
113
114        // Server Row
115        let server_label = "  Server     : ";
116        let server_val_width = inner_width - server_label.len() - 1;
117        let truncated_server = theme::truncate_to(&server.name, server_val_width);
118        println!(
119            "║{}{} ║",
120            server_label,
121            theme::pad_to(&truncated_server, server_val_width),
122        );
123
124        println!("╠{}╣", "═".repeat(inner_width));
125
126        // Ping Stats Rows
127        let labels = [
128            (
129                "  Ping       : ",
130                theme::color_ping(ping_stats.avg_ms, &config),
131            ),
132            (
133                "  Jitter     : ",
134                theme::color_jitter(ping_stats.jitter_ms, &config),
135            ),
136            ("  Min Ping   : ", format!("{} ms", ping_stats.min_ms)),
137            ("  Max Ping   : ", format!("{} ms", ping_stats.max_ms)),
138            (
139                "  Packet Loss: ",
140                theme::color_loss(ping_stats.packet_loss_pct, &config),
141            ),
142        ];
143
144        for (label, val) in labels {
145            let val_width = inner_width - label.len() - 1;
146            println!("║{}{} ║", label, theme::pad_to(&val, val_width));
147        }
148
149        println!("╠{}╣", "═".repeat(inner_width));
150
151        // Download Row
152        match down_speed {
153            Some(s) => {
154                let label = "  Download   : ";
155                let speed_str = theme::color_speed(s, &config);
156                let rating = theme::speed_rating(s, &config);
157                let combined = format!("{}  {}", speed_str, rating);
158                let val_width = inner_width - label.len() - 1;
159                println!("║{}{} ║", label, theme::pad_to(&combined, val_width));
160            }
161            None => {
162                let label = "  Download   : ";
163                let val_width = inner_width - label.len() - 1;
164                println!("║{}{} ║", label, theme::pad_to("skipped", val_width));
165            }
166        }
167
168        // Upload Row
169        match up_speed {
170            Some(s) => {
171                let label = "  Upload     : ";
172                let speed_str = theme::color_speed(s, &config);
173                let rating = theme::speed_rating(s, &config);
174                let combined = format!("{}  {}", speed_str, rating);
175                let val_width = inner_width - label.len() - 1;
176                println!("║{}{} ║", label, theme::pad_to(&combined, val_width));
177            }
178            None => {
179                let label = "  Upload     : ";
180                let val_width = inner_width - label.len() - 1;
181                println!("║{}{} ║", label, theme::pad_to("skipped", val_width));
182            }
183        }
184
185        println!("╚{}╝", "═".repeat(inner_width));
186    }
187
188    Ok(SpeedTestResult {
189        timestamp: Utc::now().to_rfc3339(),
190        version: env!("CARGO_PKG_VERSION").to_string(),
191        server_name: server.name,
192        ping: ping_stats,
193        download_mbps: down_speed,
194        upload_mbps: up_speed,
195    })
196}