Skip to main content

netspeed_cli/
output_strategy.rs

1//! Output format resolution — extracts format selection from Config into
2//! an `OutputFormat` strategy (Strategy pattern, OCP-compliant).
3//!
4//! Add new variants in `OutputFormat` (formatter/mod.rs) and extend the
5//! `resolve` function below — no caller changes needed.
6
7use crate::config::{Config, Format};
8use crate::formatter::{OutputFormat, SkipState};
9use crate::profiles::UserProfile;
10use crate::task_runner::TestRunResult;
11
12/// Resolve the active output format from config and test results.
13#[must_use]
14pub fn resolve_output_format(
15    config: &Config,
16    dl_result: &TestRunResult,
17    ul_result: &TestRunResult,
18    elapsed: std::time::Duration,
19) -> OutputFormat {
20    let profile = config
21        .profile()
22        .and_then(UserProfile::from_name)
23        .unwrap_or_default();
24    let theme = config.theme();
25
26    // --format flag takes precedence over legacy --json/--csv/--simple booleans
27    match config.format() {
28        Some(Format::Json) => OutputFormat::Json,
29        Some(Format::Jsonl) => OutputFormat::Jsonl,
30        Some(Format::Csv) => OutputFormat::Csv {
31            delimiter: config.csv_delimiter(),
32            header: config.csv_header(),
33        },
34        Some(Format::Minimal) => OutputFormat::Minimal { theme },
35        Some(Format::Simple) => OutputFormat::Simple { theme },
36        Some(Format::Compact) => OutputFormat::Compact {
37            dl_bytes: dl_result.total_bytes,
38            ul_bytes: ul_result.total_bytes,
39            dl_duration: dl_result.duration_secs,
40            ul_duration: ul_result.duration_secs,
41            elapsed,
42            profile,
43            theme,
44        },
45        Some(Format::Dashboard) => OutputFormat::Dashboard {
46            dl_mbps: dl_result.avg_bps / 1_000_000.0,
47            dl_peak_mbps: dl_result.peak_bps / 1_000_000.0,
48            dl_bytes: dl_result.total_bytes,
49            dl_duration: dl_result.duration_secs,
50            ul_mbps: ul_result.avg_bps / 1_000_000.0,
51            ul_peak_mbps: ul_result.peak_bps / 1_000_000.0,
52            ul_bytes: ul_result.total_bytes,
53            ul_duration: ul_result.duration_secs,
54            elapsed,
55            profile,
56            theme,
57        },
58        Some(Format::Detailed) => OutputFormat::Detailed {
59            dl_bytes: dl_result.total_bytes,
60            ul_bytes: ul_result.total_bytes,
61            dl_duration: dl_result.duration_secs,
62            ul_duration: ul_result.duration_secs,
63            skipped: SkipState {
64                download: config.no_download(),
65                upload: config.no_upload(),
66            },
67            elapsed,
68            profile,
69            minimal: config.minimal(),
70            theme,
71        },
72        None => {
73            // Default to detailed output
74            OutputFormat::Detailed {
75                dl_bytes: dl_result.total_bytes,
76                ul_bytes: ul_result.total_bytes,
77                dl_duration: dl_result.duration_secs,
78                ul_duration: ul_result.duration_secs,
79                skipped: SkipState {
80                    download: config.no_download(),
81                    upload: config.no_upload(),
82                },
83                elapsed,
84                profile,
85                minimal: config.minimal(),
86                theme,
87            }
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::config::{ConfigSource, OutputSource, TestSource};
96    use crate::task_runner::TestRunResult;
97
98    /// Helper to create a minimal download TestRunResult for testing
99    fn make_test_run(dl_bps: f64, dl_peak: f64, dl_bytes: u64, dl_dur: f64) -> TestRunResult {
100        TestRunResult {
101            avg_bps: dl_bps,
102            peak_bps: dl_peak,
103            total_bytes: dl_bytes,
104            duration_secs: dl_dur,
105            speed_samples: vec![dl_bps],
106            latency_under_load: None,
107        }
108    }
109
110    fn make_upload_run(bps: f64, peak: f64, bytes: u64, dur: f64) -> TestRunResult {
111        TestRunResult {
112            avg_bps: bps,
113            peak_bps: peak,
114            total_bytes: bytes,
115            duration_secs: dur,
116            speed_samples: vec![bps],
117            latency_under_load: None,
118        }
119    }
120
121    fn make_config(format: Option<Format>) -> Config {
122        let source = ConfigSource {
123            output: OutputSource {
124                format,
125                theme: String::from(if cfg!(windows) { "monochrome" } else { "dark" }),
126                ..Default::default()
127            },
128            ..Default::default()
129        };
130        Config::from_source(&source)
131    }
132
133    #[test]
134    fn test_resolve_output_format_json() {
135        let config = make_config(Some(Format::Json));
136        let dl = make_test_run(100_000_000.0, 120_000_000.0, 10_000_000, 2.0);
137        let ul = make_upload_run(50_000_000.0, 60_000_000.0, 5_000_000, 1.0);
138
139        let result = resolve_output_format(&config, &dl, &ul, std::time::Duration::from_secs(5));
140        assert!(matches!(result, OutputFormat::Json));
141    }
142
143    #[test]
144    fn test_resolve_output_format_jsonl() {
145        let config = make_config(Some(Format::Jsonl));
146        let dl = make_test_run(100_000_000.0, 120_000_000.0, 10_000_000, 2.0);
147        let ul = make_upload_run(50_000_000.0, 60_000_000.0, 5_000_000, 1.0);
148
149        let result = resolve_output_format(&config, &dl, &ul, std::time::Duration::from_secs(5));
150        assert!(matches!(result, OutputFormat::Jsonl));
151    }
152
153    #[test]
154    fn test_resolve_output_format_csv() {
155        let source = ConfigSource {
156            output: OutputSource {
157                format: Some(Format::Csv),
158                csv_delimiter: ';',
159                csv_header: Some(true),
160                ..Default::default()
161            },
162            ..Default::default()
163        };
164        let config = Config::from_source(&source);
165        let dl = make_test_run(100_000_000.0, 120_000_000.0, 10_000_000, 2.0);
166        let ul = make_upload_run(50_000_000.0, 60_000_000.0, 5_000_000, 1.0);
167
168        let result = resolve_output_format(&config, &dl, &ul, std::time::Duration::from_secs(5));
169        match result {
170            OutputFormat::Csv { delimiter, header } => {
171                assert_eq!(delimiter, ';');
172                assert!(header);
173            }
174            _ => panic!("Expected Csv format"),
175        }
176    }
177
178    #[test]
179    fn test_resolve_output_format_minimal() {
180        let config = make_config(Some(Format::Minimal));
181        let dl = make_test_run(100_000_000.0, 120_000_000.0, 10_000_000, 2.0);
182        let ul = make_upload_run(50_000_000.0, 60_000_000.0, 5_000_000, 1.0);
183
184        let result = resolve_output_format(&config, &dl, &ul, std::time::Duration::from_secs(5));
185        match result {
186            OutputFormat::Minimal { theme } => {
187                assert_eq!(
188                    theme,
189                    if cfg!(windows) {
190                        crate::theme::Theme::Monochrome
191                    } else {
192                        crate::theme::Theme::Dark
193                    }
194                );
195            }
196            _ => panic!("Expected Minimal format"),
197        }
198    }
199
200    #[test]
201    fn test_resolve_output_format_simple() {
202        let config = make_config(Some(Format::Simple));
203        let dl = make_test_run(100_000_000.0, 120_000_000.0, 10_000_000, 2.0);
204        let ul = make_upload_run(50_000_000.0, 60_000_000.0, 5_000_000, 1.0);
205
206        let result = resolve_output_format(&config, &dl, &ul, std::time::Duration::from_secs(5));
207        match result {
208            OutputFormat::Simple { theme } => {
209                assert_eq!(
210                    theme,
211                    if cfg!(windows) {
212                        crate::theme::Theme::Monochrome
213                    } else {
214                        crate::theme::Theme::Dark
215                    }
216                );
217            }
218            _ => panic!("Expected Simple format"),
219        }
220    }
221
222    #[test]
223    fn test_resolve_output_format_compact() {
224        let config = make_config(Some(Format::Compact));
225        let dl = make_test_run(100_000_000.0, 120_000_000.0, 10_000_000, 2.0);
226        let ul = make_upload_run(50_000_000.0, 60_000_000.0, 5_000_000, 1.0);
227
228        let result = resolve_output_format(&config, &dl, &ul, std::time::Duration::from_secs(5));
229        match result {
230            OutputFormat::Compact {
231                dl_bytes,
232                ul_bytes,
233                elapsed,
234                profile,
235                ..
236            } => {
237                assert_eq!(dl_bytes, 10_000_000);
238                assert_eq!(ul_bytes, 5_000_000);
239                assert_eq!(elapsed.as_secs(), 5);
240                assert_eq!(profile, UserProfile::PowerUser); // default
241            }
242            _ => panic!("Expected Compact format"),
243        }
244    }
245
246    #[test]
247    fn test_resolve_output_format_dashboard() {
248        let config = make_config(Some(Format::Dashboard));
249        let dl = make_test_run(100_000_000.0, 120_000_000.0, 10_000_000, 2.0);
250        let ul = make_upload_run(50_000_000.0, 60_000_000.0, 5_000_000, 1.0);
251
252        let result = resolve_output_format(&config, &dl, &ul, std::time::Duration::from_secs(10));
253        match result {
254            OutputFormat::Dashboard {
255                dl_mbps,
256                dl_peak_mbps,
257                ul_mbps,
258                ul_peak_mbps,
259                elapsed,
260                ..
261            } => {
262                assert!((dl_mbps - 100.0).abs() < 0.01);
263                assert!((dl_peak_mbps - 120.0).abs() < 0.01);
264                assert!((ul_mbps - 50.0).abs() < 0.01);
265                assert!((ul_peak_mbps - 60.0).abs() < 0.01);
266                assert_eq!(elapsed.as_secs(), 10);
267            }
268            _ => panic!("Expected Dashboard format"),
269        }
270    }
271
272    #[test]
273    fn test_resolve_output_format_detailed() {
274        let config = make_config(Some(Format::Detailed));
275        let dl = make_test_run(100_000_000.0, 120_000_000.0, 10_000_000, 2.0);
276        let ul = make_upload_run(50_000_000.0, 60_000_000.0, 5_000_000, 1.0);
277
278        let result = resolve_output_format(&config, &dl, &ul, std::time::Duration::from_secs(8));
279        match result {
280            OutputFormat::Detailed {
281                dl_bytes,
282                ul_bytes,
283                elapsed,
284                skipped,
285                minimal,
286                ..
287            } => {
288                assert_eq!(dl_bytes, 10_000_000);
289                assert_eq!(ul_bytes, 5_000_000);
290                assert_eq!(elapsed.as_secs(), 8);
291                assert!(!skipped.download);
292                assert!(!skipped.upload);
293                assert!(!minimal);
294            }
295            _ => panic!("Expected Detailed format"),
296        }
297    }
298
299    #[test]
300    fn test_resolve_output_format_detailed_with_skipped() {
301        let source = ConfigSource {
302            test: TestSource {
303                no_download: Some(true),
304                no_upload: Some(true),
305                ..Default::default()
306            },
307            output: OutputSource {
308                format: Some(Format::Detailed),
309                minimal: Some(true),
310                theme: String::from("light"),
311                ..Default::default()
312            },
313            ..Default::default()
314        };
315        let config = Config::from_source(&source);
316        let dl = make_test_run(100_000_000.0, 120_000_000.0, 10_000_000, 2.0);
317        let ul = make_upload_run(50_000_000.0, 60_000_000.0, 5_000_000, 1.0);
318
319        let result = resolve_output_format(&config, &dl, &ul, std::time::Duration::from_secs(3));
320        match result {
321            OutputFormat::Detailed {
322                skipped,
323                minimal,
324                theme,
325                ..
326            } => {
327                assert!(skipped.download);
328                assert!(skipped.upload);
329                assert!(minimal);
330                assert_eq!(theme, crate::theme::Theme::Light);
331            }
332            _ => panic!("Expected Detailed format"),
333        }
334    }
335
336    #[test]
337    fn test_resolve_output_format_default_none() {
338        // When format is None, default to Detailed
339        let source = ConfigSource {
340            output: OutputSource {
341                format: None,
342                ..Default::default()
343            },
344            ..Default::default()
345        };
346        let config = Config::from_source(&source);
347        let dl = make_test_run(100_000_000.0, 120_000_000.0, 10_000_000, 2.0);
348        let ul = make_upload_run(50_000_000.0, 60_000_000.0, 5_000_000, 1.0);
349
350        let result = resolve_output_format(&config, &dl, &ul, std::time::Duration::from_secs(5));
351        assert!(matches!(result, OutputFormat::Detailed { .. }));
352    }
353
354    #[test]
355    fn test_resolve_output_format_with_profile() {
356        let source = ConfigSource {
357            output: OutputSource {
358                format: Some(Format::Compact),
359                profile: Some(String::from("gamer")),
360                ..Default::default()
361            },
362            ..Default::default()
363        };
364        let config = Config::from_source(&source);
365        let dl = make_test_run(100_000_000.0, 120_000_000.0, 10_000_000, 2.0);
366        let ul = make_upload_run(50_000_000.0, 60_000_000.0, 5_000_000, 1.0);
367
368        let result = resolve_output_format(&config, &dl, &ul, std::time::Duration::from_secs(5));
369        match result {
370            OutputFormat::Compact { profile, .. } => {
371                assert_eq!(profile, UserProfile::Gamer);
372            }
373            _ => panic!("Expected Compact format"),
374        }
375    }
376}