1use crate::common;
10use crate::error::Error;
11use crate::grades;
12use crate::profiles::UserProfile;
13use crate::terminal;
14use crate::theme::{Colors, Theme};
15use crate::types::{CsvOutput, TestResult};
16use owo_colors::OwoColorize;
17
18#[derive(Debug, Clone, Copy, Default)]
20pub struct SkipState {
21 pub download: bool,
23 pub upload: bool,
25}
26
27fn section_header(title: &str, nc: bool, theme: Theme) -> String {
29 if nc {
30 format!(" {title}")
31 } else {
32 format!(" {}", Colors::header(title, theme))
33 }
34}
35
36#[derive(Debug)]
39pub enum OutputFormat {
40 Json,
41 Csv {
42 delimiter: char,
43 header: bool,
44 },
45 Simple {
46 theme: Theme,
47 },
48 Minimal {
49 theme: Theme,
50 },
51 Jsonl,
52 Compact {
53 dl_bytes: u64,
54 ul_bytes: u64,
55 dl_duration: f64,
56 ul_duration: f64,
57 elapsed: std::time::Duration,
58 profile: UserProfile,
59 theme: Theme,
60 },
61 Detailed {
62 dl_bytes: u64,
63 ul_bytes: u64,
64 dl_duration: f64,
65 ul_duration: f64,
66 skipped: SkipState,
67 elapsed: std::time::Duration,
68 profile: UserProfile,
69 minimal: bool,
70 theme: Theme,
71 },
72 Dashboard {
73 dl_mbps: f64,
74 dl_peak_mbps: f64,
75 dl_bytes: u64,
76 dl_duration: f64,
77 ul_mbps: f64,
78 ul_peak_mbps: f64,
79 ul_bytes: u64,
80 ul_duration: f64,
81 elapsed: std::time::Duration,
82 profile: UserProfile,
83 theme: Theme,
84 },
85}
86
87impl OutputFormat {
88 pub fn format(&self, result: &TestResult, bytes: bool) -> Result<(), Error> {
94 match self {
95 OutputFormat::Json => format_json(result),
96 OutputFormat::Jsonl => format_jsonl(result),
97 OutputFormat::Csv { delimiter, header } => format_csv(result, *delimiter, *header),
98 OutputFormat::Simple { theme } => format_simple(result, bytes, *theme),
99 OutputFormat::Minimal { theme } => format_minimal(result, bytes, *theme),
100 OutputFormat::Compact {
101 dl_bytes,
102 ul_bytes,
103 dl_duration,
104 ul_duration,
105 elapsed,
106 profile,
107 theme,
108 } => {
109 format_compact(
110 result,
111 bytes,
112 *dl_bytes,
113 *ul_bytes,
114 *dl_duration,
115 *ul_duration,
116 *elapsed,
117 *profile,
118 *theme,
119 );
120 Ok(())
121 }
122 OutputFormat::Detailed {
123 dl_bytes,
124 ul_bytes,
125 dl_duration,
126 ul_duration,
127 skipped,
128 elapsed,
129 profile,
130 minimal,
131 theme,
132 } => {
133 format_detailed(
134 result,
135 bytes,
136 *dl_bytes,
137 *ul_bytes,
138 *dl_duration,
139 *ul_duration,
140 *skipped,
141 *elapsed,
142 *profile,
143 *minimal,
144 *theme,
145 )?;
146 format_verbose_sections(result, *profile, *minimal, *theme);
147 Ok(())
148 }
149 OutputFormat::Dashboard {
150 dl_mbps,
151 dl_peak_mbps,
152 dl_bytes,
153 dl_duration,
154 ul_mbps,
155 ul_peak_mbps,
156 ul_bytes,
157 ul_duration,
158 elapsed,
159 profile,
160 theme,
161 } => {
162 dashboard::show(
163 result,
164 &dashboard::Summary {
165 dl_mbps: *dl_mbps,
166 dl_peak_mbps: *dl_peak_mbps,
167 dl_bytes: *dl_bytes,
168 dl_duration: *dl_duration,
169 ul_mbps: *ul_mbps,
170 ul_peak_mbps: *ul_peak_mbps,
171 ul_bytes: *ul_bytes,
172 ul_duration: *ul_duration,
173 elapsed: *elapsed,
174 profile: *profile,
175 theme: *theme,
176 },
177 )?;
178 Ok(())
179 }
180 }
181 }
182}
183
184pub trait Formatter: Send + Sync {
212 fn format(
218 &self,
219 result: &crate::types::TestResult,
220 use_bytes: bool,
221 ) -> Result<(), crate::error::Error>;
222
223 fn format_list(&self, servers: &[crate::types::Server]) -> Result<(), crate::error::Error>;
229}
230
231impl Formatter for OutputFormat {
233 fn format(
234 &self,
235 result: &crate::types::TestResult,
236 use_bytes: bool,
237 ) -> Result<(), crate::error::Error> {
238 self.format(result, use_bytes)
239 }
240
241 fn format_list(&self, servers: &[crate::types::Server]) -> Result<(), crate::error::Error> {
242 sections::format_list(servers).map_err(crate::error::Error::IoError)
243 }
244}
245
246pub mod dashboard;
247pub mod estimates;
248pub mod ratings;
249pub mod scenarios;
250pub mod sections;
251pub mod stability;
252
253pub use estimates::{format_targets, show};
255pub use ratings::{
256 BufferbloatGrade, bufferbloat_colorized, bufferbloat_grade, colorize_rating, connection_rating,
257 degradation_str, format_duration, format_overall_rating, format_speed_colored,
258 format_speed_plain, ping_rating, speed_rating_mbps,
259};
260pub use sections::{
261 build_elapsed_time, format_connection_info, format_download_section, format_elapsed_time,
262 format_footer, format_latency_section, format_list, format_test_summary, format_upload_section,
263};
264pub use stability::{compute_cv, compute_percentiles, format_stability_line};
265
266pub fn format_simple(result: &TestResult, bytes: bool, theme: Theme) -> Result<(), Error> {
273 let nc = terminal::no_color();
274 let mut parts = Vec::new();
275
276 if let Some(ping) = result.ping {
277 parts.push(if nc {
278 format!("Latency: {ping:.1} ms")
279 } else {
280 format!("Latency: {} ms", Colors::info(&format!("{ping:.1}"), theme))
281 });
282 }
283
284 if let Some(dl) = result.download {
285 let speed = if nc {
286 ratings::format_speed_plain(dl, bytes)
287 } else {
288 ratings::format_speed_colored(dl, bytes, theme)
289 };
290 parts.push(format!("Download: {speed}"));
291 }
292
293 if let Some(ul) = result.upload {
294 let speed = if nc {
295 ratings::format_speed_plain(ul, bytes)
296 } else {
297 ratings::format_speed_colored(ul, bytes, theme)
298 };
299 parts.push(format!("Upload: {speed}"));
300 }
301
302 eprintln!("{}", parts.join(" | "));
303 Ok(())
304}
305
306pub fn format_minimal(result: &TestResult, _bytes: bool, theme: Theme) -> Result<(), Error> {
313 let nc = terminal::no_color();
314 let profile = UserProfile::default();
315
316 let overall_grade = grades::grade_overall(
317 result.ping,
318 result.jitter,
319 result.download,
320 result.upload,
321 profile,
322 );
323 let grade_str = if nc {
324 format!("[{}]", overall_grade.as_str())
325 } else {
326 overall_grade.color_str(nc, theme)
327 };
328
329 let dl_str = result.download.map_or_else(
330 || "—↓".to_string(),
331 |d| {
332 let mbps = d / 1_000_000.0;
333 format!("{mbps:.1}↓")
334 },
335 );
336
337 let ul_str = result.upload.map_or_else(
338 || "—↑".to_string(),
339 |u| {
340 let mbps = u / 1_000_000.0;
341 format!("{mbps:.1}↑")
342 },
343 );
344
345 let lat_str = result
346 .ping
347 .map_or_else(|| "—ms".to_string(), |p| format!("{p:.0}ms"));
348
349 eprintln!("{grade_str} {dl_str} {ul_str} {lat_str}");
350 Ok(())
351}
352
353pub fn format_jsonl(result: &TestResult) -> Result<(), Error> {
359 println!("{}", serde_json::to_string(result)?);
360 Ok(())
361}
362
363pub fn format_compact(
366 result: &TestResult,
367 bytes: bool,
368 dl_bytes: u64,
369 ul_bytes: u64,
370 dl_duration: f64,
371 ul_duration: f64,
372 elapsed: std::time::Duration,
373 profile: UserProfile,
374 theme: Theme,
375) {
376 let nc = terminal::no_color();
377 let overall_grade = grades::grade_overall(
378 result.ping,
379 result.jitter,
380 result.download,
381 result.upload,
382 profile,
383 );
384
385 eprintln!();
386 eprintln!("{}", section_header("TEST RESULTS", nc, theme));
387 eprintln!("{}", ratings::format_overall_rating(result, nc, theme));
388 if !nc {
389 eprintln!(
390 " {} {}",
391 "Grade:".dimmed(),
392 overall_grade.color_str(nc, theme)
393 );
394 }
395 eprintln!();
396
397 sections::format_latency_section(result, nc, theme);
398 eprintln!();
399
400 sections::format_download_section(result, bytes, nc, false, theme);
401 eprintln!();
402
403 sections::format_upload_section(result, bytes, nc, false, theme);
404 eprintln!();
405
406 if let Some(ip) = &result.client_ip {
407 if nc {
408 eprintln!(" Server: {} · {}", result.server.sponsor, ip);
409 } else {
410 eprintln!(
411 " {} {} · {}",
412 "Server:".dimmed(),
413 Colors::bold(&result.server.sponsor, theme),
414 Colors::muted(ip, theme),
415 );
416 }
417 eprintln!();
418 }
419
420 if nc {
421 eprintln!(" SUMMARY");
422 } else {
423 eprintln!(" {}", Colors::header("SUMMARY", theme));
424 }
425 if dl_bytes > 0 {
426 eprintln!(
427 " {:>14}: {} in {:.1}s",
428 "Download".dimmed(),
429 common::format_data_size(dl_bytes),
430 dl_duration
431 );
432 }
433 if ul_bytes > 0 {
434 eprintln!(
435 " {:>14}: {} in {:.1}s",
436 "Upload".dimmed(),
437 common::format_data_size(ul_bytes),
438 ul_duration
439 );
440 }
441
442 eprintln!();
443 if nc {
444 eprintln!(" Total time: {:.1}s", elapsed.as_secs_f64());
445 } else {
446 eprintln!(
447 " {}: {}",
448 "Total time".dimmed(),
449 Colors::info(&format!("{:.1}s", elapsed.as_secs_f64()), theme),
450 );
451 }
452 sections::format_footer(&result.timestamp, nc, theme);
453}
454
455#[allow(clippy::too_many_arguments)]
462pub fn format_detailed(
463 result: &TestResult,
464 bytes: bool,
465 dl_bytes: u64,
466 ul_bytes: u64,
467 dl_duration: f64,
468 ul_duration: f64,
469 skipped: SkipState,
470 elapsed: std::time::Duration,
471 profile: UserProfile,
472 minimal: bool,
473 theme: Theme,
474) -> Result<(), Error> {
475 let nc = terminal::no_color() || minimal;
476 let overall_grade = grades::grade_overall(
477 result.ping,
478 result.jitter,
479 result.download,
480 result.upload,
481 profile,
482 );
483
484 if nc {
485 eprintln!("\n TEST RESULTS");
486 } else {
487 eprintln!("\n {}", Colors::header("TEST RESULTS", theme));
488 }
489 eprintln!("{}", ratings::format_overall_rating(result, nc, theme));
490 if !nc {
491 eprintln!(
492 " {} {}",
493 "Grade:".dimmed(),
494 overall_grade.color_str(nc, theme)
495 );
496 }
497 eprintln!();
498
499 sections::format_latency_section(result, nc, theme);
500 sections::format_download_section(result, bytes, nc, skipped.download, theme);
501 sections::format_upload_section(result, bytes, nc, skipped.upload, theme);
502 sections::format_connection_info(result, nc, theme);
503 sections::format_test_summary(dl_bytes, ul_bytes, dl_duration, ul_duration, nc);
504
505 eprintln!();
506 if nc {
507 eprintln!(" Total time: {:.1}s", elapsed.as_secs_f64());
508 } else {
509 eprintln!(
510 " {}: {}",
511 "Total time".dimmed(),
512 Colors::info(&format!("{:.1}s", elapsed.as_secs_f64()), theme),
513 );
514 }
515
516 sections::format_footer(&result.timestamp, nc, theme);
517
518 Ok(())
519}
520
521pub fn format_json(result: &TestResult) -> Result<(), Error> {
527 let is_tty = {
528 use std::io::IsTerminal;
529 std::io::stdout().is_terminal()
530 };
531 let output = if is_tty {
532 serde_json::to_string_pretty(result)?
533 } else {
534 serde_json::to_string(result)?
535 };
536 println!("{output}");
537 Ok(())
538}
539
540pub fn format_csv(result: &TestResult, delimiter: char, print_header: bool) -> Result<(), Error> {
546 let stdout = std::io::stdout();
547 let mut wtr = csv::WriterBuilder::new()
548 .delimiter(delimiter as u8)
549 .from_writer(stdout);
550 if print_header {
551 wtr.write_record([
552 "Server ID",
553 "Sponsor",
554 "Server Name",
555 "Timestamp",
556 "Distance",
557 "Ping",
558 "Jitter",
559 "Packet Loss",
560 "Download",
561 "Download Peak",
562 "Upload",
563 "Upload Peak",
564 "IP Address",
565 ])?;
566 }
567 let csv_output = CsvOutput {
568 server_id: result.server.id.clone(),
569 sponsor: result.server.sponsor.clone(),
570 server_name: result.server.name.clone(),
571 timestamp: result.timestamp.clone(),
572 distance: result.server.distance,
573 ping: result.ping.unwrap_or(0.0),
574 jitter: result.jitter.unwrap_or(0.0),
575 packet_loss: result.packet_loss.unwrap_or(0.0),
576 download: result.download.unwrap_or(0.0),
577 download_peak: result.download_peak.unwrap_or(0.0),
578 upload: result.upload.unwrap_or(0.0),
579 upload_peak: result.upload_peak.unwrap_or(0.0),
580 ip_address: result.client_ip.clone().unwrap_or_default(),
581 };
582 wtr.serialize(csv_output)?;
583 wtr.flush()?;
584 Ok(())
585}
586
587pub fn format_verbose_sections(
590 result: &TestResult,
591 profile: UserProfile,
592 minimal: bool,
593 theme: Theme,
594) {
595 let nc = terminal::no_color() || minimal;
596
597 if profile.show_estimates() {
598 let estimates = estimates::build(result.download, nc, theme);
599 if !estimates.is_empty() {
600 eprintln!("{estimates}");
601 }
602 }
603
604 if profile.show_stability() {
605 if let (Some(dl_s), Some(ul_s)) = (&result.download_samples, &result.upload_samples) {
606 let dl_cv = compute_cv(dl_s);
607 let ul_cv = compute_cv(ul_s);
608 let dl_grade = grades::grade_stability(dl_cv);
609 let ul_grade = grades::grade_stability(ul_cv);
610 let dl_stability = format_stability_line(dl_cv, nc, theme);
611 let ul_stability = format_stability_line(ul_cv, nc, theme);
612 eprintln!();
613 eprintln!("{}", section_header("STABILITY", nc, theme));
614 if nc {
615 eprintln!(" {:>14}: [{dl_stability}]", "Download");
616 eprintln!(" {:>14}: [{ul_stability}]", "Upload");
617 } else {
618 eprintln!(
619 " {:>14}: {} {dl_stability}",
620 "Download".dimmed(),
621 dl_grade.color_str(nc, theme)
622 );
623 eprintln!(
624 " {:>14}: {} {ul_stability}",
625 "Upload".dimmed(),
626 ul_grade.color_str(nc, theme)
627 );
628 }
629 }
630 }
631
632 if profile.show_percentiles() {
633 if let Some(ref samples) = result.ping_samples {
634 if let Some((p50, p95, p99)) = compute_percentiles(samples) {
635 eprintln!();
636 eprintln!("{}", section_header("LATENCY PERCENTILES", nc, theme));
637 let p50_str = format!("{p50:.1} ms");
638 let p95_str = format!("{p95:.1} ms");
639 let p99_str = format!("{p99:.1} ms");
640 if nc {
641 eprintln!(" P50: {p50_str} P95: {p95_str} P99: {p99_str}");
642 } else {
643 eprintln!(
644 " {}: {} {}: {} {}: {}",
645 "P50".dimmed(),
646 Colors::info(&p50_str, theme),
647 "P95".dimmed(),
648 Colors::warn(&p95_str, theme),
649 "P99".dimmed(),
650 Colors::bad(&p99_str, theme),
651 );
652 }
653 }
654 }
655 }
656
657 if profile.show_history() {
658 let dl_mbps = result.download.map_or(0.0, |d| d / 1_000_000.0);
659 let ul_mbps = result.upload.map_or(0.0, |u| u / 1_000_000.0);
660 if let Some(comparison) = crate::history::format_comparison(dl_mbps, ul_mbps, nc) {
661 eprintln!();
662 eprintln!(" {comparison}");
663 }
664 }
665}
666
667pub struct FormatterFactory;
675
676impl FormatterFactory {
677 pub fn create(format: Option<crate::config::Format>) -> Box<dyn Formatter> {
679 match format {
680 Some(crate::config::Format::Json) => Box::new(OutputFormat::Json),
681 Some(crate::config::Format::Jsonl) => Box::new(OutputFormat::Jsonl),
682 Some(crate::config::Format::Csv) => Box::new(OutputFormat::Csv {
683 delimiter: ',',
684 header: true,
685 }),
686 Some(crate::config::Format::Simple) => Box::new(OutputFormat::Simple {
687 theme: crate::theme::Theme::Dark,
688 }),
689 Some(crate::config::Format::Minimal) => Box::new(OutputFormat::Minimal {
690 theme: crate::theme::Theme::Dark,
691 }),
692 Some(crate::config::Format::Compact) => Box::new(OutputFormat::Compact {
693 dl_bytes: 0,
694 ul_bytes: 0,
695 dl_duration: 0.0,
696 ul_duration: 0.0,
697 elapsed: std::time::Duration::ZERO,
698 profile: crate::profiles::UserProfile::default(),
699 theme: crate::theme::Theme::Dark,
700 }),
701 Some(crate::config::Format::Detailed) => Box::new(OutputFormat::Detailed {
702 dl_bytes: 0,
703 ul_bytes: 0,
704 dl_duration: 0.0,
705 ul_duration: 0.0,
706 skipped: SkipState::default(),
707 elapsed: std::time::Duration::ZERO,
708 profile: crate::profiles::UserProfile::default(),
709 minimal: false,
710 theme: crate::theme::Theme::Dark,
711 }),
712 Some(crate::config::Format::Dashboard) => Box::new(OutputFormat::Dashboard {
713 dl_mbps: 0.0,
714 dl_peak_mbps: 0.0,
715 dl_bytes: 0,
716 dl_duration: 0.0,
717 ul_mbps: 0.0,
718 ul_peak_mbps: 0.0,
719 ul_bytes: 0,
720 ul_duration: 0.0,
721 elapsed: std::time::Duration::ZERO,
722 profile: crate::profiles::UserProfile::default(),
723 theme: crate::theme::Theme::Dark,
724 }),
725 None => Box::new(OutputFormat::Simple {
726 theme: crate::theme::Theme::Dark,
727 }),
728 }
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use crate::types::{PhaseResult, ServerInfo, TestPhases, TestResult};
736
737 fn make_test_result() -> TestResult {
738 TestResult {
739 status: "ok".to_string(),
740 version: env!("CARGO_PKG_VERSION").to_string(),
741 test_id: None,
742 server: ServerInfo {
743 id: "1".to_string(),
744 name: "Test".to_string(),
745 sponsor: "Test ISP".to_string(),
746 country: "US".to_string(),
747 distance: 10.0,
748 },
749 ping: Some(15.0),
750 jitter: Some(1.5),
751 packet_loss: Some(0.0),
752 download: Some(100_000_000.0),
753 download_peak: Some(120_000_000.0),
754 upload: Some(50_000_000.0),
755 upload_peak: Some(60_000_000.0),
756 latency_download: Some(20.0),
757 latency_upload: Some(18.0),
758 download_samples: Some(vec![95_000_000.0, 100_000_000.0, 105_000_000.0]),
759 upload_samples: Some(vec![48_000_000.0, 50_000_000.0, 52_000_000.0]),
760 ping_samples: Some(vec![12.0, 15.0, 18.0]),
761 timestamp: "2026-01-01T00:00:00Z".to_string(),
762 client_ip: Some("192.168.1.100".to_string()),
763 client_location: None,
764 download_cv: Some(0.05),
765 upload_cv: Some(0.04),
766 download_ci_95: Some((140.0, 160.0)),
767 upload_ci_95: Some((45.0, 55.0)),
768 overall_grade: None,
769 download_grade: None,
770 upload_grade: None,
771 connection_rating: None,
772 phases: TestPhases {
773 ping: PhaseResult::completed(),
774 download: PhaseResult::completed(),
775 upload: PhaseResult::completed(),
776 },
777 }
778 }
779
780 #[test]
781 fn test_format_simple_with_data() {
782 let result = make_test_result();
783 let _ = format_simple(&result, false, Theme::Dark);
785 }
786
787 #[test]
788 fn test_format_simple_no_ping() {
789 let mut result = make_test_result();
790 result.ping = None;
791 let _ = format_simple(&result, false, Theme::Dark);
792 }
793
794 #[test]
795 fn test_format_simple_no_download() {
796 let mut result = make_test_result();
797 result.download = None;
798 let _ = format_simple(&result, false, Theme::Dark);
799 }
800
801 #[test]
802 fn test_format_simple_no_upload() {
803 let mut result = make_test_result();
804 result.upload = None;
805 let _ = format_simple(&result, false, Theme::Dark);
806 }
807
808 #[test]
809 fn test_format_simple_bytes_mode() {
810 let result = make_test_result();
811 let _ = format_simple(&result, true, Theme::Dark);
812 }
813
814 #[test]
815 fn test_format_simple_light_theme() {
816 let result = make_test_result();
817 let _ = format_simple(&result, false, Theme::Light);
818 }
819
820 #[test]
821 fn test_format_minimal_basic() {
822 let result = make_test_result();
823 let _ = format_minimal(&result, false, Theme::Dark);
824 }
825
826 #[test]
827 fn test_format_minimal_no_download() {
828 let mut result = make_test_result();
829 result.download = None;
830 let _ = format_minimal(&result, false, Theme::Dark);
831 }
832
833 #[test]
834 fn test_format_minimal_no_upload() {
835 let mut result = make_test_result();
836 result.upload = None;
837 let _ = format_minimal(&result, false, Theme::Dark);
838 }
839
840 #[test]
841 fn test_format_minimal_no_ping() {
842 let mut result = make_test_result();
843 result.ping = None;
844 let _ = format_minimal(&result, false, Theme::Dark);
845 }
846
847 #[test]
848 fn test_format_jsonl_basic() {
849 let result = make_test_result();
850 let _ = format_jsonl(&result);
851 }
852
853 #[test]
854 fn test_format_compact_basic() {
855 let result = make_test_result();
856 format_compact(
857 &result,
858 false,
859 10_000_000,
860 5_000_000,
861 2.0,
862 1.0,
863 std::time::Duration::from_secs(5),
864 UserProfile::default(),
865 Theme::Dark,
866 );
867 }
868
869 #[test]
870 fn test_format_compact_with_client_ip() {
871 let result = make_test_result();
872 format_compact(
873 &result,
874 false,
875 10_000_000,
876 5_000_000,
877 2.0,
878 1.0,
879 std::time::Duration::from_secs(5),
880 UserProfile::default(),
881 Theme::Dark,
882 );
883 }
884
885 #[test]
886 fn test_format_compact_bytes_mode() {
887 let result = make_test_result();
888 format_compact(
889 &result,
890 true,
891 10_000_000,
892 5_000_000,
893 2.0,
894 1.0,
895 std::time::Duration::from_secs(5),
896 UserProfile::default(),
897 Theme::Dark,
898 );
899 }
900
901 #[test]
902 fn test_format_compact_nc_mode() {
903 let result = make_test_result();
904 format_compact(
906 &result,
907 false,
908 10_000_000,
909 5_000_000,
910 2.0,
911 1.0,
912 std::time::Duration::from_secs(5),
913 UserProfile::default(),
914 Theme::Monochrome,
915 );
916 }
917
918 #[test]
919 fn test_format_compact_gamer_profile() {
920 let result = make_test_result();
921 format_compact(
922 &result,
923 false,
924 10_000_000,
925 5_000_000,
926 2.0,
927 1.0,
928 std::time::Duration::from_secs(5),
929 UserProfile::Gamer,
930 Theme::Dark,
931 );
932 }
933
934 #[test]
935 fn test_format_detailed_basic() {
936 let result = make_test_result();
937 let _ = format_detailed(
938 &result,
939 false,
940 10_000_000,
941 5_000_000,
942 2.0,
943 1.0,
944 SkipState {
945 download: false,
946 upload: false,
947 },
948 std::time::Duration::from_secs(5),
949 UserProfile::default(),
950 false,
951 Theme::Dark,
952 );
953 }
954
955 #[test]
956 fn test_format_detailed_with_skipped() {
957 let result = make_test_result();
958 let _ = format_detailed(
959 &result,
960 false,
961 10_000_000,
962 5_000_000,
963 2.0,
964 1.0,
965 SkipState {
966 download: true,
967 upload: true,
968 },
969 std::time::Duration::from_secs(3),
970 UserProfile::default(),
971 false,
972 Theme::Dark,
973 );
974 }
975
976 #[test]
977 fn test_format_detailed_minimal_mode() {
978 let result = make_test_result();
979 let _ = format_detailed(
980 &result,
981 false,
982 10_000_000,
983 5_000_000,
984 2.0,
985 1.0,
986 SkipState {
987 download: false,
988 upload: false,
989 },
990 std::time::Duration::from_secs(5),
991 UserProfile::default(),
992 true,
993 Theme::Dark,
994 );
995 }
996
997 #[test]
998 fn test_format_detailed_bytes_mode() {
999 let result = make_test_result();
1000 let _ = format_detailed(
1001 &result,
1002 true,
1003 10_000_000,
1004 5_000_000,
1005 2.0,
1006 1.0,
1007 SkipState {
1008 download: false,
1009 upload: false,
1010 },
1011 std::time::Duration::from_secs(5),
1012 UserProfile::default(),
1013 false,
1014 Theme::Dark,
1015 );
1016 }
1017
1018 #[test]
1019 fn test_format_json_basic() {
1020 let result = make_test_result();
1021 let _ = format_json(&result);
1022 }
1023
1024 #[test]
1025 fn test_format_csv_basic() {
1026 let result = make_test_result();
1027 let _ = format_csv(&result, ',', true);
1028 }
1029
1030 #[test]
1031 fn test_format_csv_no_header() {
1032 let result = make_test_result();
1033 let _ = format_csv(&result, ';', false);
1034 }
1035
1036 #[test]
1037 fn test_format_csv_tab_delimiter() {
1038 let result = make_test_result();
1039 let _ = format_csv(&result, '\t', true);
1040 }
1041
1042 #[test]
1043 fn test_format_csv_with_missing_values() {
1044 let mut result = make_test_result();
1045 result.ping = None;
1046 result.jitter = None;
1047 result.packet_loss = None;
1048 let _ = format_csv(&result, ',', true);
1049 }
1050
1051 #[test]
1052 fn test_skip_state_default() {
1053 let skip = SkipState::default();
1054 assert!(!skip.download);
1055 assert!(!skip.upload);
1056 }
1057
1058 #[test]
1059 fn test_skip_state_custom() {
1060 let skip = SkipState {
1061 download: true,
1062 upload: false,
1063 };
1064 assert!(skip.download);
1065 assert!(!skip.upload);
1066 }
1067
1068 #[test]
1069 fn test_output_format_json() {
1070 let fmt = OutputFormat::Json;
1071 let result = make_test_result();
1072 assert!(fmt.format(&result, false).is_ok());
1073 }
1074
1075 #[test]
1076 fn test_output_format_jsonl() {
1077 let fmt = OutputFormat::Jsonl;
1078 let result = make_test_result();
1079 assert!(fmt.format(&result, false).is_ok());
1080 }
1081
1082 #[test]
1083 fn test_output_format_csv() {
1084 let fmt = OutputFormat::Csv {
1085 delimiter: ',',
1086 header: true,
1087 };
1088 let result = make_test_result();
1089 assert!(fmt.format(&result, false).is_ok());
1090 }
1091
1092 #[test]
1093 fn test_output_format_simple() {
1094 let fmt = OutputFormat::Simple { theme: Theme::Dark };
1095 let result = make_test_result();
1096 assert!(fmt.format(&result, false).is_ok());
1097 }
1098
1099 #[test]
1100 fn test_output_format_minimal() {
1101 let fmt = OutputFormat::Minimal { theme: Theme::Dark };
1102 let result = make_test_result();
1103 assert!(fmt.format(&result, false).is_ok());
1104 }
1105
1106 #[test]
1107 fn test_output_format_detailed() {
1108 let fmt = OutputFormat::Detailed {
1109 dl_bytes: 10_000_000,
1110 ul_bytes: 5_000_000,
1111 dl_duration: 2.0,
1112 ul_duration: 1.0,
1113 skipped: SkipState::default(),
1114 elapsed: std::time::Duration::from_secs(5),
1115 profile: UserProfile::default(),
1116 minimal: false,
1117 theme: Theme::Dark,
1118 };
1119 let result = make_test_result();
1120 assert!(fmt.format(&result, false).is_ok());
1121 }
1122
1123 #[test]
1124 fn test_output_format_compact() {
1125 let fmt = OutputFormat::Compact {
1126 dl_bytes: 10_000_000,
1127 ul_bytes: 5_000_000,
1128 dl_duration: 2.0,
1129 ul_duration: 1.0,
1130 elapsed: std::time::Duration::from_secs(5),
1131 profile: UserProfile::default(),
1132 theme: Theme::Dark,
1133 };
1134 let result = make_test_result();
1135 assert!(fmt.format(&result, false).is_ok());
1136 }
1137
1138 #[test]
1139 fn test_output_format_dashboard() {
1140 let fmt = OutputFormat::Dashboard {
1141 dl_mbps: 100.0,
1142 dl_peak_mbps: 120.0,
1143 dl_bytes: 10_000_000,
1144 dl_duration: 2.0,
1145 ul_mbps: 50.0,
1146 ul_peak_mbps: 60.0,
1147 ul_bytes: 5_000_000,
1148 ul_duration: 1.0,
1149 elapsed: std::time::Duration::from_secs(5),
1150 profile: UserProfile::default(),
1151 theme: Theme::Dark,
1152 };
1153 let result = make_test_result();
1154 assert!(fmt.format(&result, false).is_ok());
1155 }
1156
1157 #[test]
1158 fn test_section_header_nc_mode() {
1159 let header = section_header("Test Header", true, Theme::Dark);
1160 assert!(header.contains("Test Header"));
1161 assert!(!header.contains("\x1b")); }
1163
1164 #[test]
1165 fn test_section_header_colored() {
1166 let header = section_header("Test Header", false, Theme::Dark);
1167 assert!(header.contains("Test Header"));
1168 }
1169
1170 #[test]
1171 fn test_format_verbose_sections_power_user() {
1172 let result = make_test_result();
1173 format_verbose_sections(&result, UserProfile::PowerUser, false, Theme::Dark);
1175 }
1176
1177 #[test]
1178 fn test_format_verbose_sections_casual() {
1179 let result = make_test_result();
1180 format_verbose_sections(&result, UserProfile::Casual, false, Theme::Dark);
1182 }
1183
1184 #[test]
1185 fn test_format_verbose_sections_gamer() {
1186 let result = make_test_result();
1187 format_verbose_sections(&result, UserProfile::Gamer, false, Theme::Dark);
1189 }
1190
1191 #[test]
1192 fn test_format_verbose_sections_remote_worker() {
1193 let result = make_test_result();
1194 format_verbose_sections(&result, UserProfile::RemoteWorker, false, Theme::Dark);
1196 }
1197
1198 #[test]
1199 fn test_format_verbose_sections_minimal() {
1200 let result = make_test_result();
1201 format_verbose_sections(&result, UserProfile::default(), true, Theme::Dark);
1202 }
1203
1204 #[test]
1205 fn test_format_verbose_sections_integration() {
1206 format_verbose_sections(
1208 &make_test_result(),
1209 UserProfile::default(),
1210 false,
1211 Theme::Dark,
1212 );
1213 }
1214
1215 #[test]
1216 fn test_format_verbose_sections_empty() {
1217 let mut result = make_test_result();
1219 result.ping = None;
1220 result.jitter = None;
1221 result.download = None;
1222 result.upload = None;
1223 result.download_samples = None;
1224 result.upload_samples = None;
1225 result.ping_samples = None;
1226 format_verbose_sections(&result, UserProfile::default(), false, Theme::Dark);
1227 }
1228
1229 #[test]
1230 fn test_format_data_kb() {
1231 assert_eq!(crate::common::format_data_size(5120), "5.0 KB");
1232 }
1233
1234 #[test]
1235 fn test_format_data_mb() {
1236 assert_eq!(crate::common::format_data_size(5_242_880), "5.0 MB");
1237 }
1238
1239 #[test]
1240 fn test_format_data_gb() {
1241 assert_eq!(crate::common::format_data_size(1_073_741_824), "1.00 GB");
1242 }
1243}