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::run_concurrent_streams;
11use crate::endpoints::ServerEndpoints;
12use crate::error::Error;
13use crate::progress::Tracker;
14use crate::test_config::TestConfig;
15use crate::types::Server;
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    ServerEndpoints::from_server_url(server_url)
23        .upload()
24        .to_string()
25}
26
27/// Deterministic upload payload: byte\[i\] = i % 256.
28/// Initialized once via `LazyLock` — Bytes-backed for zero-copy sharing.
29static UPLOAD_PAYLOAD: std::sync::LazyLock<bytes::Bytes> = std::sync::LazyLock::new(|| {
30    let mut data = vec![0u8; 200_000];
31    for (i, byte) in data.iter_mut().enumerate() {
32        *byte = (i % 256) as u8;
33    }
34    bytes::Bytes::from(data)
35});
36
37/// Generate upload data of the given size (used by tests).
38#[cfg(test)]
39fn generate_upload_data(size: usize) -> Vec<u8> {
40    let mut data = vec![0u8; size];
41    for (i, byte) in data.iter_mut().enumerate() {
42        *byte = (i % 256) as u8;
43    }
44    data
45}
46
47/// Run upload bandwidth test against the given server.
48///
49/// Returns `(avg_speed_bps, peak_speed_bps, total_bytes_uploaded, speed_samples)`.
50///
51/// # Errors
52///
53/// Returns [`Error::NetworkError`] if all upload streams fail.
54pub async fn run(
55    client: &Client,
56    server: &Server,
57    single: bool,
58    progress: Arc<Tracker>,
59) -> Result<(f64, f64, u64, Vec<f64>), Error> {
60    let config = TestConfig::default();
61    let stream_count = TestConfig::stream_count_for(single);
62    let upload_data: bytes::Bytes = (*UPLOAD_PAYLOAD).clone();
63
64    let result = run_concurrent_streams(
65        config.estimated_upload_bytes,
66        stream_count,
67        progress,
68        "upload",
69        |_, state, sample_interval| {
70            let client = client.clone();
71            let server_url = Arc::new(server.url.clone());
72            let data = Arc::new(upload_data.clone());
73            tokio::spawn(async move {
74                for _ in 0..config.upload_rounds {
75                    let upload_url = build_upload_url(&server_url);
76
77                    let response = client
78                        .post(&upload_url)
79                        .body((*data).clone())
80                        .send()
81                        .await
82                        .map_err(Error::UploadTest)?;
83
84                    if !response.status().is_success() {
85                        return Err(Error::UploadFailure(format!(
86                            "server returned {} for {upload_url}",
87                            response.status()
88                        )));
89                    }
90
91                    let chunk = u64::try_from(data.len()).unwrap_or(u64::MAX);
92                    state.record_bytes(chunk, sample_interval);
93                }
94                Ok(())
95            })
96        },
97    )
98    .await?;
99
100    Ok((
101        result.avg_bps,
102        result.peak_bps,
103        result.total_bytes,
104        result.speed_samples,
105    ))
106}
107
108#[cfg(test)]
109mod tests {
110    use crate::common;
111    use crate::test_config::TestConfig;
112
113    use super::*;
114
115    #[test]
116    fn test_upload_bandwidth_calculation() {
117        let result = common::calculate_bandwidth(1_000_000, 2.0);
118        assert!((result - 4_000_000.0).abs() < f64::EPSILON);
119    }
120
121    #[test]
122    fn test_upload_bandwidth_zero_elapsed() {
123        let result = common::calculate_bandwidth(1_000_000, 0.0);
124        assert!(result.abs() < f64::EPSILON);
125    }
126
127    #[test]
128    fn test_upload_concurrent_count_single() {
129        assert_eq!(TestConfig::stream_count_for(true), 1);
130    }
131
132    #[test]
133    fn test_upload_concurrent_count_multiple() {
134        assert_eq!(TestConfig::stream_count_for(false), 4);
135    }
136
137    #[test]
138    fn test_upload_url_generation() {
139        let url = build_upload_url("http://server.example.com");
140        assert!(url.ends_with("/upload.php"));
141    }
142
143    #[test]
144    fn test_upload_url_generation_full_path() {
145        let url = build_upload_url("http://server.example.com/speedtest/upload.php");
146        assert_eq!(url, "http://server.example.com/speedtest/upload.php");
147    }
148
149    #[test]
150    fn test_generate_upload_data_size() {
151        let data = generate_upload_data(1000);
152        assert_eq!(data.len(), 1000);
153    }
154
155    #[test]
156    fn test_generate_upload_data_pattern() {
157        let data = generate_upload_data(300);
158        for (i, &byte) in data.iter().enumerate() {
159            assert_eq!(byte, (i % 256) as u8);
160        }
161    }
162
163    #[test]
164    fn test_generate_upload_data_wraps_at_256() {
165        let data = generate_upload_data(512);
166        assert_eq!(data[0], 0u8);
167        assert_eq!(data[255], 255u8);
168        assert_eq!(data[256], 0u8);
169        assert_eq!(data[511], 255u8);
170    }
171
172    #[test]
173    fn test_generate_upload_data_empty() {
174        let data = generate_upload_data(0);
175        assert!(data.is_empty());
176    }
177
178    #[test]
179    fn test_upload_data_size_constant() {
180        // Verify the upload data size used in run (200KB)
181        let data = generate_upload_data(200_000);
182        assert_eq!(data.len(), 200_000);
183    }
184
185    #[test]
186    fn test_upload_payload_lazy_init() {
187        // Verify the LazyLock payload matches the expected pattern
188        assert_eq!(UPLOAD_PAYLOAD.len(), 200_000);
189        assert_eq!(UPLOAD_PAYLOAD[0], 0u8);
190        assert_eq!(UPLOAD_PAYLOAD[255], 255u8);
191        assert_eq!(UPLOAD_PAYLOAD[256], 0u8);
192    }
193}