Skip to main content

netspeed_cli/
upload.rs

1//! Multi-stream upload bandwidth measurement.
2//!
3//! This module handles uploading test data to speedtest.net servers
4//! to measure upload bandwidth. It supports:
5//! - Multi-stream concurrent uploads (4 streams by default, 1 with `--single`)
6//! - Progressive payload sizing for accurate measurement
7//! - Real-time progress tracking with speed calculation
8//! - Peak speed detection through periodic sampling
9
10use crate::bandwidth_loop::BandwidthLoopState;
11use crate::common;
12use crate::error::SpeedtestError;
13use crate::progress::{SpeedProgress, no_color};
14use crate::types::Server;
15use owo_colors::OwoColorize;
16use reqwest::Client;
17use std::sync::Arc;
18
19/// Build upload URL
20#[must_use]
21pub fn build_upload_url(server_url: &str) -> String {
22    format!("{server_url}/upload")
23}
24
25fn generate_upload_data(size: usize) -> Vec<u8> {
26    let mut data = vec![0u8; size];
27    for (i, byte) in data.iter_mut().enumerate() {
28        *byte = (i % 256) as u8;
29    }
30    data
31}
32
33/// Number of upload rounds per stream (each round uploads a chunk of test data).
34const UPLOAD_TEST_ROUNDS: usize = 4;
35
36/// Estimated total bytes for progress bar initialization.
37const ESTIMATED_UPLOAD_BYTES: u64 = 4_000_000; // 4 MB estimate
38
39/// Run upload bandwidth test against the given server.
40///
41/// Returns `(avg_speed_bps, peak_speed_bps, total_bytes_uploaded, speed_samples)`.
42///
43/// # Errors
44///
45/// Returns [`SpeedtestError::NetworkError`] if all upload streams fail.
46pub async fn upload_test(
47    client: &Client,
48    server: &Server,
49    single: bool,
50    progress: Arc<SpeedProgress>,
51) -> Result<(f64, f64, u64, Vec<f64>), SpeedtestError> {
52    let concurrent_uploads = common::determine_stream_count(single);
53    let state = Arc::new(BandwidthLoopState::new(ESTIMATED_UPLOAD_BYTES, progress));
54    let upload_data = generate_upload_data(200_000); // 200KB chunks
55
56    let mut handles = Vec::new();
57
58    for _ in 0..concurrent_uploads {
59        let client = client.clone();
60        let server_url = server.url.clone();
61        let data = upload_data.clone();
62        let state = Arc::clone(&state);
63
64        let handle = tokio::spawn(async move {
65            let mut uploaded_bytes = 0u64;
66
67            for _ in 0..UPLOAD_TEST_ROUNDS {
68                let upload_url = build_upload_url(&server_url);
69
70                if let Ok(response) = client.post(&upload_url).body(data.clone()).send().await {
71                    if response.status().is_success() {
72                        let chunk = data.len() as u64;
73                        uploaded_bytes += chunk;
74                        state.record_bytes(chunk);
75                    }
76                }
77            }
78
79            uploaded_bytes
80        });
81
82        handles.push(handle);
83    }
84
85    // Collect results — log any task panics so failures aren't silently swallowed.
86    // Bytes are already counted via atomic counters, so we don't need the return values.
87    for (i, handle) in handles.into_iter().enumerate() {
88        if let Err(e) = handle.await {
89            let msg = format!("Warning: upload task {i} failed: {e}");
90            if no_color() {
91                eprintln!("\n{msg}");
92            } else {
93                eprintln!("\n{}", msg.yellow().bold());
94            }
95        }
96    }
97
98    let final_result = state.finish();
99    Ok((
100        final_result.avg_bps,
101        final_result.peak_bps,
102        final_result.total_bytes,
103        final_result.speed_samples,
104    ))
105}
106
107#[cfg(test)]
108mod tests {
109    use crate::common;
110
111    use super::*;
112
113    #[test]
114    fn test_upload_bandwidth_calculation() {
115        let result = common::calculate_bandwidth(1_000_000, 2.0);
116        assert_eq!(result, 4_000_000.0);
117    }
118
119    #[test]
120    fn test_upload_bandwidth_zero_elapsed() {
121        let result = common::calculate_bandwidth(1_000_000, 0.0);
122        assert_eq!(result, 0.0);
123    }
124
125    #[test]
126    fn test_upload_concurrent_count_single() {
127        assert_eq!(common::determine_stream_count(true), 1);
128    }
129
130    #[test]
131    fn test_upload_concurrent_count_multiple() {
132        assert_eq!(common::determine_stream_count(false), 4);
133    }
134
135    #[test]
136    fn test_upload_url_generation() {
137        let url = build_upload_url("http://server.example.com");
138        assert!(url.ends_with("/upload"));
139    }
140
141    #[test]
142    fn test_upload_url_generation_full_path() {
143        let url = build_upload_url("http://server.example.com/speedtest");
144        assert_eq!(url, "http://server.example.com/speedtest/upload");
145    }
146
147    #[test]
148    fn test_generate_upload_data_size() {
149        let data = generate_upload_data(1000);
150        assert_eq!(data.len(), 1000);
151    }
152
153    #[test]
154    fn test_generate_upload_data_pattern() {
155        let data = generate_upload_data(300);
156        for (i, &byte) in data.iter().enumerate() {
157            assert_eq!(byte, (i % 256) as u8);
158        }
159    }
160
161    #[test]
162    fn test_generate_upload_data_wraps_at_256() {
163        let data = generate_upload_data(512);
164        assert_eq!(data[0], 0u8);
165        assert_eq!(data[255], 255u8);
166        assert_eq!(data[256], 0u8);
167        assert_eq!(data[511], 255u8);
168    }
169
170    #[test]
171    fn test_generate_upload_data_empty() {
172        let data = generate_upload_data(0);
173        assert!(data.is_empty());
174    }
175
176    #[test]
177    fn test_upload_data_size_constant() {
178        // Verify the upload data size used in upload_test (200KB)
179        let data = generate_upload_data(200_000);
180        assert_eq!(data.len(), 200_000);
181    }
182}