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