Skip to main content

netspeed_cli/
test_config.rs

1//! Centralized test configuration constants.
2//!
3//! This module consolidates all magic numbers used across download/upload tests,
4//! making it easy to tune test behavior and avoid inconsistent values.
5//!
6//! # Usage
7//!
8//! ```rust
9//! use netspeed_cli::test_config::TestConfig;
10//!
11//! let config = TestConfig::default();
12//! println!("rounds: {}, streams: {}, interval: {}ms",
13//!     config.download_rounds, config.stream_count, config.sample_interval_ms);
14//! ```
15
16/// Centralized test configuration for bandwidth measurement.
17///
18/// All timing, count, and sizing values used across the test pipeline
19/// are concentrated here so they can be tuned in one place.
20#[derive(Debug, Clone)]
21pub struct TestConfig {
22    /// Number of download rounds per stream (each round fetches a different test file).
23    pub download_rounds: usize,
24
25    /// Number of upload rounds per stream (each round uploads a chunk of test data).
26    pub upload_rounds: usize,
27
28    /// Number of concurrent streams in multi-stream mode.
29    pub stream_count: usize,
30
31    /// Throttle interval for speed sampling in milliseconds (20 Hz max).
32    pub sample_interval_ms: u64,
33
34    /// Number of ping attempts to measure latency, jitter, and packet loss.
35    pub ping_attempts: usize,
36
37    /// Payload size for each upload chunk in bytes.
38    pub upload_payload_bytes: usize,
39
40    /// Estimated total download bytes for progress bar initialization.
41    pub estimated_download_bytes: u64,
42
43    /// Estimated total upload bytes for progress bar initialization.
44    pub estimated_upload_bytes: u64,
45
46    /// How often to poll latency under load (milliseconds).
47    pub latency_poll_interval_ms: u64,
48
49    /// Default number of HTTP retry attempts for transient failures.
50    pub http_retry_attempts: usize,
51
52    /// Initial HTTP retry backoff in milliseconds.
53    pub http_retry_base_ms: u64,
54
55    /// Maximum HTTP retry backoff in milliseconds.
56    pub http_retry_max_ms: u64,
57}
58
59impl Default for TestConfig {
60    fn default() -> Self {
61        Self {
62            // Test rounds per stream
63            download_rounds: 4,
64            upload_rounds: 4,
65
66            // Concurrency
67            stream_count: 4,
68
69            // Sampling (20 Hz = 50ms interval)
70            sample_interval_ms: 50,
71
72            // Ping measurement
73            ping_attempts: 8,
74
75            // Upload payload (200 KB — matches speedtest.net standard)
76            upload_payload_bytes: 200_000,
77
78            // Progress bar estimates
79            estimated_download_bytes: 15_000_000, // 15 MB
80            estimated_upload_bytes: 4_000_000,    // 4 MB
81
82            // Latency under load polling
83            latency_poll_interval_ms: 100,
84
85            // HTTP retry configuration
86            http_retry_attempts: 3,
87            http_retry_base_ms: 100,
88            http_retry_max_ms: 5000,
89        }
90    }
91}
92
93impl TestConfig {
94    /// Get stream count based on single-connection mode.
95    #[must_use]
96    pub fn stream_count_for(single: bool) -> usize {
97        if single {
98            1
99        } else {
100            Self::default().stream_count
101        }
102    }
103
104    /// Calculate retry delay with exponential backoff.
105    /// Returns (delay_ms, should_retry).
106    #[must_use]
107    pub fn retry_delay(attempt: usize) -> (u64, bool) {
108        let config = Self::default();
109        if attempt >= config.http_retry_attempts {
110            return (0, false);
111        }
112        // Exponential backoff: 100ms, 200ms, 400ms, ... capped at max
113        let delay = config.http_retry_base_ms * 2u64.saturating_pow(attempt as u32);
114        let delay = delay.min(config.http_retry_max_ms);
115        (delay, true)
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_default_values() {
125        let config = TestConfig::default();
126        assert_eq!(config.download_rounds, 4);
127        assert_eq!(config.upload_rounds, 4);
128        assert_eq!(config.stream_count, 4);
129        assert_eq!(config.sample_interval_ms, 50);
130        assert_eq!(config.ping_attempts, 8);
131        assert_eq!(config.upload_payload_bytes, 200_000);
132        assert_eq!(config.estimated_download_bytes, 15_000_000);
133        assert_eq!(config.estimated_upload_bytes, 4_000_000);
134        assert_eq!(config.http_retry_attempts, 3);
135    }
136
137    #[test]
138    fn test_stream_count_for_single() {
139        assert_eq!(TestConfig::stream_count_for(true), 1);
140    }
141
142    #[test]
143    fn test_stream_count_for_multi() {
144        assert_eq!(TestConfig::stream_count_for(false), 4);
145    }
146
147    #[test]
148    fn test_retry_delay_first_attempt() {
149        let (delay, should_retry) = TestConfig::retry_delay(0);
150        assert!(should_retry);
151        assert_eq!(delay, 100); // 100ms base
152    }
153
154    #[test]
155    fn test_retry_delay_second_attempt() {
156        let (delay, should_retry) = TestConfig::retry_delay(1);
157        assert!(should_retry);
158        assert_eq!(delay, 200); // 100ms * 2^1
159    }
160
161    #[test]
162    fn test_retry_delay_third_attempt() {
163        let (delay, should_retry) = TestConfig::retry_delay(2);
164        assert!(should_retry);
165        assert_eq!(delay, 400); // 100ms * 2^2
166    }
167
168    #[test]
169    fn test_retry_delay_exhausted() {
170        let (_, should_retry) = TestConfig::retry_delay(3);
171        assert!(!should_retry);
172    }
173
174    #[test]
175    fn test_retry_delay_beyond_max_attempts() {
176        // Attempt 10 >= http_retry_attempts (3) returns should_retry=false with delay=0
177        let (delay, should_retry) = TestConfig::retry_delay(10);
178        assert!(!should_retry, "attempt 10 is beyond max retry attempts");
179        assert_eq!(delay, 0, "delay is 0 when retries exhausted");
180    }
181}