1use crate::terminal::no_emoji;
4use crate::theme::{Colors, Theme};
5use crate::types::TestResult;
6use owo_colors::OwoColorize;
7
8#[must_use]
9pub fn ping_rating(ping_ms: f64) -> &'static str {
10 if ping_ms < 10.0 {
11 "Excellent"
12 } else if ping_ms < 30.0 {
13 "Good"
14 } else if ping_ms < 60.0 {
15 "Fair"
16 } else if ping_ms < 100.0 {
17 "Poor"
18 } else {
19 "Bad"
20 }
21}
22
23#[must_use]
24pub fn speed_rating_mbps(mbps: f64) -> &'static str {
25 if mbps >= 500.0 {
26 "Excellent"
27 } else if mbps >= 200.0 {
28 "Great"
29 } else if mbps >= 100.0 {
30 "Good"
31 } else if mbps >= 50.0 {
32 "Fair"
33 } else if mbps >= 25.0 {
34 "Moderate"
35 } else if mbps >= 10.0 {
36 "Slow"
37 } else {
38 "Very Slow"
39 }
40}
41
42#[must_use]
43pub fn colorize_rating(rating: &str, nc: bool, theme: Theme) -> String {
44 if nc || no_emoji() {
45 format!("[{rating}]")
46 } else {
47 let (icon, colored) = match rating {
48 "Excellent" => ("⚡ ", Colors::good(rating, theme)),
49 "Great" => ("🔵 ", Colors::info(rating, theme)),
50 "Good" => ("🟢 ", Colors::good(rating, theme)),
51 "Fair" => ("🟡 ", Colors::warn(rating, theme)),
52 "Moderate" => ("🟠 ", Colors::warn(rating, theme)),
53 "Poor" => ("🔴 ", Colors::bad(rating, theme)),
54 "Slow" => ("🟤 ", Colors::bad(rating, theme)),
55 "Very Slow" => ("⚠️ ", Colors::bad(rating, theme)),
56 _ => ("", rating.to_string()),
57 };
58 format!("{icon}{colored}")
59 }
60}
61
62struct SpeedComponents {
64 value: f64,
65 unit: &'static str,
66}
67
68fn speed_components(bps: f64, bytes: bool) -> SpeedComponents {
70 let divider = if bytes { 8.0 } else { 1.0 };
71 let unit = if bytes { "MB/s" } else { "Mb/s" };
72 let value = bps / divider / 1_000_000.0;
73 SpeedComponents { value, unit }
74}
75
76#[must_use]
77pub fn format_speed_colored(bps: f64, bytes: bool, theme: Theme) -> String {
78 let SpeedComponents { value, unit } = speed_components(bps, bytes);
79 let mbps = bps / 1_000_000.0;
80 let rating = speed_rating_mbps(mbps);
81 let text = format!("{value:.2} {unit}");
82 match rating {
83 "Excellent" | "Great" | "Good" => Colors::good(&text, theme),
84 "Fair" | "Moderate" => Colors::warn(&text, theme),
85 "Poor" | "Slow" | "Very Slow" => Colors::bad(&text, theme),
86 _ => text,
87 }
88}
89
90#[must_use]
91pub fn format_speed_plain(bps: f64, bytes: bool) -> String {
92 let SpeedComponents { value, unit } = speed_components(bps, bytes);
93 format!("{value:.2} {unit}")
94}
95
96#[must_use]
97pub fn format_duration(secs: f64) -> String {
98 if secs < 60.0 {
99 format!("{secs:.1}s")
100 } else {
101 let mins = (secs / 60.0).clamp(0.0, u64::MAX as f64) as u64;
103 let secs = secs % 60.0;
104 format!("{mins}m {secs:.0}s")
105 }
106}
107
108#[must_use]
109pub fn connection_rating(result: &TestResult) -> &'static str {
110 fn score_lower(value: f64, thresholds: [f64; 5]) -> f64 {
112 if value < thresholds[0] {
113 100.0
114 } else if value < thresholds[1] {
115 80.0
116 } else if value < thresholds[2] {
117 60.0
118 } else if value < thresholds[3] {
119 40.0
120 } else {
121 20.0
122 }
123 }
124
125 fn score_higher(mbps: f64, thresholds: [f64; 6]) -> f64 {
127 if mbps >= thresholds[0] {
128 100.0
129 } else if mbps >= thresholds[1] {
130 85.0
131 } else if mbps >= thresholds[2] {
132 70.0
133 } else if mbps >= thresholds[3] {
134 55.0
135 } else if mbps >= thresholds[4] {
136 40.0
137 } else if mbps >= thresholds[5] {
138 25.0
139 } else {
140 10.0
141 }
142 }
143
144 let mut score = 0.0;
145 let mut factors = 0.0;
146
147 if let Some(ping) = result.ping {
149 score += score_lower(ping, [10.0, 30.0, 60.0, 100.0, f64::MAX]);
150 factors += 1.0;
151 }
152
153 if let Some(jitter) = result.jitter {
155 score += score_lower(jitter, [2.0, 5.0, 10.0, 20.0, f64::MAX]);
156 factors += 1.0;
157 }
158
159 if let Some(dl) = result.download {
161 score += score_higher(dl / 1_000_000.0, [500.0, 200.0, 100.0, 50.0, 25.0, 10.0]);
162 factors += 1.0;
163 }
164
165 if let Some(ul) = result.upload {
167 score += score_higher(ul / 1_000_000.0, [500.0, 200.0, 100.0, 50.0, 25.0, 10.0]);
168 factors += 1.0;
169 }
170
171 if factors == 0.0 {
172 return "Unknown";
173 }
174
175 let avg = score / factors;
176 if avg >= 90.0 {
177 "Excellent"
178 } else if avg >= 75.0 {
179 "Great"
180 } else if avg >= 55.0 {
181 "Good"
182 } else if avg >= 40.0 {
183 "Fair"
184 } else if avg >= 25.0 {
185 "Moderate"
186 } else {
187 "Poor"
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum BufferbloatGrade {
194 A,
195 B,
196 C,
197 D,
198 F,
199}
200
201impl BufferbloatGrade {
202 #[must_use]
203 pub fn as_str(&self) -> &'static str {
204 match self {
205 Self::A => "A",
206 Self::B => "B",
207 Self::C => "C",
208 Self::D => "D",
209 Self::F => "F",
210 }
211 }
212}
213
214#[must_use]
216pub fn bufferbloat_grade(load_latency: f64, idle_latency: f64) -> (BufferbloatGrade, f64) {
217 let added = if idle_latency > 0.0 {
218 load_latency - idle_latency
219 } else {
220 load_latency
221 };
222 let grade = if added < 5.0 {
223 BufferbloatGrade::A
224 } else if added < 20.0 {
225 BufferbloatGrade::B
226 } else if added < 50.0 {
227 BufferbloatGrade::C
228 } else if added < 100.0 {
229 BufferbloatGrade::D
230 } else {
231 BufferbloatGrade::F
232 };
233 (grade, added.max(0.0))
234}
235
236#[must_use]
237pub fn bufferbloat_colorized(
238 grade: BufferbloatGrade,
239 added_ms: f64,
240 nc: bool,
241 theme: Theme,
242) -> String {
243 if nc {
244 format!("{} ({added_ms:.0}ms)", grade.as_str())
245 } else {
246 let text = format!("{} ({added_ms:.0}ms added)", grade.as_str());
247 match grade {
248 BufferbloatGrade::A | BufferbloatGrade::B => Colors::good(&text, theme),
249 BufferbloatGrade::C | BufferbloatGrade::D => Colors::warn(&text, theme),
250 BufferbloatGrade::F => Colors::bad(&text, theme),
251 }
252 }
253}
254
255#[must_use]
256pub fn format_overall_rating(result: &TestResult, nc: bool, theme: Theme) -> String {
257 let rating = connection_rating(result);
258 if nc || no_emoji() {
259 format!(" Overall: {rating}")
260 } else {
261 let (icon, colored) = match rating {
262 "Excellent" => ("⚡ ", Colors::good(rating, theme)),
263 "Great" => ("🔵 ", Colors::info(rating, theme)),
264 "Good" => ("🟢 ", Colors::good(rating, theme)),
265 "Fair" => ("🟡 ", Colors::warn(rating, theme)),
266 "Moderate" => ("🟠 ", Colors::warn(rating, theme)),
267 "Poor" => ("🔴 ", Colors::bad(rating, theme)),
268 _ => ("", rating.to_string()),
269 };
270 format!(" {} {icon}{colored}", "Overall:".dimmed())
271 }
272}
273
274#[must_use]
275pub fn degradation_str(lat_load: f64, idle_ping: Option<f64>, nc: bool, theme: Theme) -> String {
276 let Some(idle) = idle_ping else {
277 return String::new();
278 };
279 if idle <= 0.0 {
280 return String::new();
281 }
282 let pct = ((lat_load / idle) - 1.0) * 100.0;
283 let text = format!(
284 "+{pct:.0}% ({})",
285 if pct < 25.0 {
286 "minimal"
287 } else if pct < 50.0 {
288 "moderate"
289 } else {
290 "significant"
291 }
292 );
293 if nc {
294 format!(" [{text:>8}]")
295 } else {
296 let colored = if pct < 25.0 {
297 Colors::good(&text, theme)
298 } else if pct < 50.0 {
299 Colors::warn(&text, theme)
300 } else {
301 Colors::bad(&text, theme)
302 };
303 format!(" {colored}")
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use crate::types::{PhaseResult, ServerInfo, TestPhases, TestResult};
311
312 fn make_test_result() -> TestResult {
313 TestResult {
314 status: "ok".to_string(),
315 version: env!("CARGO_PKG_VERSION").to_string(),
316 test_id: None,
317 server: ServerInfo {
318 id: "1".to_string(),
319 name: "Test".to_string(),
320 sponsor: "Test ISP".to_string(),
321 country: "US".to_string(),
322 distance: 10.0,
323 },
324 ping: Some(15.0),
325 jitter: Some(1.5),
326 packet_loss: Some(0.0),
327 download: Some(100_000_000.0),
328 download_peak: Some(120_000_000.0),
329 upload: Some(50_000_000.0),
330 upload_peak: Some(60_000_000.0),
331 latency_download: Some(20.0),
332 latency_upload: Some(18.0),
333 download_samples: None,
334 upload_samples: None,
335 ping_samples: None,
336 timestamp: "2026-01-01T00:00:00Z".to_string(),
337 client_ip: None,
338 client_location: None,
339 download_cv: None,
340 upload_cv: None,
341 download_ci_95: None,
342 upload_ci_95: None,
343 overall_grade: None,
344 download_grade: None,
345 upload_grade: None,
346 connection_rating: None,
347 phases: TestPhases {
348 ping: PhaseResult::completed(),
349 download: PhaseResult::completed(),
350 upload: PhaseResult::completed(),
351 },
352 }
353 }
354
355 #[test]
358 fn test_ping_rating_excellent() {
359 assert_eq!(ping_rating(5.0), "Excellent");
360 assert_eq!(ping_rating(9.9), "Excellent");
361 assert_eq!(ping_rating(0.0), "Excellent");
362 }
363
364 #[test]
365 fn test_ping_rating_good() {
366 assert_eq!(ping_rating(10.0), "Good");
367 assert_eq!(ping_rating(29.9), "Good");
368 assert_eq!(ping_rating(15.0), "Good");
369 }
370
371 #[test]
372 fn test_ping_rating_fair() {
373 assert_eq!(ping_rating(30.0), "Fair");
374 assert_eq!(ping_rating(59.9), "Fair");
375 assert_eq!(ping_rating(45.0), "Fair");
376 }
377
378 #[test]
379 fn test_ping_rating_poor() {
380 assert_eq!(ping_rating(60.0), "Poor");
381 assert_eq!(ping_rating(99.9), "Poor");
382 assert_eq!(ping_rating(80.0), "Poor");
383 }
384
385 #[test]
386 fn test_ping_rating_bad() {
387 assert_eq!(ping_rating(100.0), "Bad");
388 assert_eq!(ping_rating(500.0), "Bad");
389 assert_eq!(ping_rating(10000.0), "Bad");
390 }
391
392 #[test]
395 fn test_speed_rating_excellent() {
396 assert_eq!(speed_rating_mbps(500.0), "Excellent");
397 assert_eq!(speed_rating_mbps(1000.0), "Excellent");
398 }
399
400 #[test]
401 fn test_speed_rating_great() {
402 assert_eq!(speed_rating_mbps(200.0), "Great");
403 assert_eq!(speed_rating_mbps(499.9), "Great");
404 assert_eq!(speed_rating_mbps(300.0), "Great");
405 }
406
407 #[test]
408 fn test_speed_rating_good() {
409 assert_eq!(speed_rating_mbps(100.0), "Good");
410 assert_eq!(speed_rating_mbps(199.9), "Good");
411 assert_eq!(speed_rating_mbps(150.0), "Good");
412 }
413
414 #[test]
415 fn test_speed_rating_fair() {
416 assert_eq!(speed_rating_mbps(50.0), "Fair");
417 assert_eq!(speed_rating_mbps(99.9), "Fair");
418 }
419
420 #[test]
421 fn test_speed_rating_moderate() {
422 assert_eq!(speed_rating_mbps(25.0), "Moderate");
423 assert_eq!(speed_rating_mbps(49.9), "Moderate");
424 assert_eq!(speed_rating_mbps(30.0), "Moderate");
425 }
426
427 #[test]
428 fn test_speed_rating_slow() {
429 assert_eq!(speed_rating_mbps(10.0), "Slow");
430 assert_eq!(speed_rating_mbps(24.9), "Slow");
431 assert_eq!(speed_rating_mbps(15.0), "Slow");
432 }
433
434 #[test]
435 fn test_speed_rating_very_slow() {
436 assert_eq!(speed_rating_mbps(0.0), "Very Slow");
437 assert_eq!(speed_rating_mbps(9.9), "Very Slow");
438 assert_eq!(speed_rating_mbps(1.0), "Very Slow");
439 }
440
441 #[test]
444 fn test_colorize_rating_excellent() {
445 let result = colorize_rating("Excellent", true, Theme::Dark);
446 assert!(result.contains("[Excellent]"));
447 }
448
449 #[test]
450 fn test_colorize_rating_great() {
451 let result = colorize_rating("Great", true, Theme::Dark);
452 assert!(result.contains("[Great]"));
453 }
454
455 #[test]
456 fn test_colorize_rating_good() {
457 let result = colorize_rating("Good", true, Theme::Dark);
458 assert!(result.contains("[Good]"));
459 }
460
461 #[test]
462 fn test_colorize_rating_fair() {
463 let result = colorize_rating("Fair", true, Theme::Dark);
464 assert!(result.contains("[Fair]"));
465 }
466
467 #[test]
468 fn test_colorize_rating_moderate() {
469 let result = colorize_rating("Moderate", true, Theme::Dark);
470 assert!(result.contains("[Moderate]"));
471 }
472
473 #[test]
474 fn test_colorize_rating_poor() {
475 let result = colorize_rating("Poor", true, Theme::Dark);
476 assert!(result.contains("[Poor]"));
477 }
478
479 #[test]
480 fn test_colorize_rating_slow() {
481 let result = colorize_rating("Slow", true, Theme::Dark);
482 assert!(result.contains("[Slow]"));
483 }
484
485 #[test]
486 fn test_colorize_rating_very_slow() {
487 let result = colorize_rating("Very Slow", true, Theme::Dark);
488 assert!(result.contains("[Very Slow]"));
489 }
490
491 #[test]
492 fn test_colorize_rating_unknown() {
493 let result = colorize_rating("Unknown", false, Theme::Dark);
495 assert_eq!(result, "Unknown");
496 }
497
498 #[test]
501 fn test_format_speed_colored_mbps_excellent() {
502 let result = format_speed_colored(500_000_000.0, false, Theme::Dark);
503 assert!(result.contains("500.00"));
504 assert!(result.contains("Mb/s"));
505 }
506
507 #[test]
508 fn test_format_speed_colored_bytes_mode() {
509 let result = format_speed_colored(8_000_000.0, true, Theme::Dark);
511 assert!(result.contains("1.00"));
512 assert!(result.contains("MB/s"));
513 }
514
515 #[test]
516 fn test_format_speed_colored_light_theme() {
517 let result = format_speed_colored(100_000_000.0, false, Theme::Light);
518 assert!(result.contains("100.00"));
519 }
520
521 #[test]
522 fn test_format_speed_colored_low_speed() {
523 let result = format_speed_colored(5_000_000.0, false, Theme::Dark);
525 assert!(result.contains("5.00"));
526 }
527
528 #[test]
531 fn test_format_speed_plain_mbps() {
532 let result = format_speed_plain(100_000_000.0, false);
533 assert_eq!(result, "100.00 Mb/s");
534 }
535
536 #[test]
537 fn test_format_speed_plain_bytes() {
538 let result = format_speed_plain(8_000_000.0, true);
540 assert_eq!(result, "1.00 MB/s");
541 }
542
543 #[test]
544 fn test_format_speed_plain_zero() {
545 let result = format_speed_plain(0.0, false);
546 assert_eq!(result, "0.00 Mb/s");
547 }
548
549 #[test]
550 fn test_format_speed_plain_fractional() {
551 let result = format_speed_plain(55_555_555.0, false);
552 assert!(result.contains("55.56"));
553 }
554
555 #[test]
558 fn test_format_duration_seconds() {
559 assert_eq!(format_duration(30.0), "30.0s");
560 assert_eq!(format_duration(59.9), "59.9s");
561 assert_eq!(format_duration(0.5), "0.5s");
562 }
563
564 #[test]
565 fn test_format_duration_minutes() {
566 assert_eq!(format_duration(60.0), "1m 0s");
567 assert_eq!(format_duration(90.0), "1m 30s");
568 assert_eq!(format_duration(125.0), "2m 5s");
569 }
570
571 #[test]
572 fn test_format_duration_many_minutes() {
573 assert_eq!(format_duration(600.0), "10m 0s");
574 assert_eq!(format_duration(3661.0), "61m 1s");
575 }
576
577 #[test]
580 fn test_connection_rating_excellent() {
581 let mut result = make_test_result();
582 result.ping = Some(5.0);
583 result.jitter = Some(1.0);
584 result.download = Some(500_000_000.0);
585 result.upload = Some(250_000_000.0);
586 assert_eq!(connection_rating(&result), "Excellent");
587 }
588
589 #[test]
590 fn test_connection_rating_great() {
591 let mut result = make_test_result();
592 result.ping = Some(20.0);
593 result.jitter = Some(3.0);
594 result.download = Some(200_000_000.0);
595 result.upload = Some(100_000_000.0);
596 assert_eq!(connection_rating(&result), "Great");
597 }
598
599 #[test]
600 fn test_connection_rating_good() {
601 let mut result = make_test_result();
602 result.ping = Some(40.0);
603 result.jitter = Some(8.0);
604 result.download = Some(100_000_000.0);
605 result.upload = Some(50_000_000.0);
606 assert_eq!(connection_rating(&result), "Good");
607 }
608
609 #[test]
610 fn test_connection_rating_fair() {
611 let mut result = make_test_result();
612 result.ping = Some(60.0);
613 result.jitter = Some(15.0);
614 result.download = Some(50_000_000.0);
615 result.upload = Some(25_000_000.0);
616 assert_eq!(connection_rating(&result), "Fair");
617 }
618
619 #[test]
620 fn test_connection_rating_moderate() {
621 let mut result = make_test_result();
622 result.ping = Some(80.0);
623 result.jitter = Some(18.0);
624 result.download = Some(25_000_000.0);
625 result.upload = Some(12_000_000.0);
626 assert_eq!(connection_rating(&result), "Moderate");
627 }
628
629 #[test]
630 fn test_connection_rating_poor() {
631 let mut result = make_test_result();
632 result.ping = Some(150.0);
633 result.jitter = Some(30.0);
634 result.download = Some(5_000_000.0);
635 result.upload = Some(1_000_000.0);
636 assert_eq!(connection_rating(&result), "Poor");
637 }
638
639 #[test]
640 fn test_connection_rating_unknown_no_data() {
641 let mut result = make_test_result();
642 result.ping = None;
643 result.jitter = None;
644 result.download = None;
645 result.upload = None;
646 assert_eq!(connection_rating(&result), "Unknown");
647 }
648
649 #[test]
650 fn test_connection_rating_partial_data() {
651 let mut result = make_test_result();
652 result.ping = Some(10.0);
653 result.jitter = None;
654 result.download = None;
655 result.upload = None;
656 let rating = connection_rating(&result);
658 assert!(!rating.is_empty());
659 }
660
661 #[test]
664 fn test_bufferbloat_grade_a() {
665 let (grade, added) = bufferbloat_grade(10.0, 8.0);
666 assert_eq!(grade, BufferbloatGrade::A);
667 assert!((added - 2.0).abs() < 0.1);
668 }
669
670 #[test]
671 fn test_bufferbloat_grade_b() {
672 let (grade, added) = bufferbloat_grade(30.0, 15.0);
673 assert_eq!(grade, BufferbloatGrade::B);
674 assert!((added - 15.0).abs() < 0.1);
675 }
676
677 #[test]
678 fn test_bufferbloat_grade_c() {
679 let (grade, added) = bufferbloat_grade(60.0, 20.0);
680 assert_eq!(grade, BufferbloatGrade::C);
681 assert!((added - 40.0).abs() < 0.1);
682 }
683
684 #[test]
685 fn test_bufferbloat_grade_d() {
686 let (grade, added) = bufferbloat_grade(120.0, 30.0);
687 assert_eq!(grade, BufferbloatGrade::D);
688 assert!((added - 90.0).abs() < 0.1);
689 }
690
691 #[test]
692 fn test_bufferbloat_grade_f() {
693 let (grade, added) = bufferbloat_grade(200.0, 50.0);
694 assert_eq!(grade, BufferbloatGrade::F);
695 assert!((added - 150.0).abs() < 0.1);
696 }
697
698 #[test]
699 fn test_bufferbloat_grade_zero_idle() {
700 let (grade, added) = bufferbloat_grade(10.0, 0.0);
703 assert_eq!(grade, BufferbloatGrade::B);
704 assert!((added - 10.0).abs() < 0.1);
705 }
706
707 #[test]
708 fn test_bufferbloat_grade_boundaries() {
709 let (grade, _) = bufferbloat_grade(4.99, 0.0);
711 assert_eq!(grade, BufferbloatGrade::A);
712
713 let (grade, _) = bufferbloat_grade(5.0, 0.0);
714 assert_eq!(grade, BufferbloatGrade::B);
715
716 let (grade, _) = bufferbloat_grade(19.99, 0.0);
717 assert_eq!(grade, BufferbloatGrade::B);
718
719 let (grade, _) = bufferbloat_grade(20.0, 0.0);
720 assert_eq!(grade, BufferbloatGrade::C);
721 }
722
723 #[test]
726 fn test_bufferbloat_grade_as_str() {
727 assert_eq!(BufferbloatGrade::A.as_str(), "A");
728 assert_eq!(BufferbloatGrade::B.as_str(), "B");
729 assert_eq!(BufferbloatGrade::C.as_str(), "C");
730 assert_eq!(BufferbloatGrade::D.as_str(), "D");
731 assert_eq!(BufferbloatGrade::F.as_str(), "F");
732 }
733
734 #[test]
737 fn test_bufferbloat_colorized_nc_a() {
738 let result = bufferbloat_colorized(BufferbloatGrade::A, 2.0, true, Theme::Dark);
739 assert!(result.contains("A"));
740 assert!(result.contains("2ms"));
741 }
742
743 #[test]
744 fn test_bufferbloat_colorized_nc_f() {
745 let result = bufferbloat_colorized(BufferbloatGrade::F, 150.0, true, Theme::Dark);
746 assert!(result.contains("F"));
747 assert!(result.contains("150ms"));
748 }
749
750 #[test]
751 fn test_bufferbloat_colorized_colored_a() {
752 let result = bufferbloat_colorized(BufferbloatGrade::A, 2.0, false, Theme::Dark);
753 assert!(result.contains("A"));
754 assert!(result.contains("added"));
755 }
756
757 #[test]
758 fn test_bufferbloat_colorized_colored_b() {
759 let result = bufferbloat_colorized(BufferbloatGrade::B, 15.0, false, Theme::Dark);
760 assert!(result.contains("B"));
761 }
762
763 #[test]
764 fn test_bufferbloat_colorized_colored_c() {
765 let result = bufferbloat_colorized(BufferbloatGrade::C, 40.0, false, Theme::Dark);
766 assert!(result.contains("C"));
767 }
768
769 #[test]
770 fn test_bufferbloat_colorized_colored_d() {
771 let result = bufferbloat_colorized(BufferbloatGrade::D, 90.0, false, Theme::Dark);
772 assert!(result.contains("D"));
773 }
774
775 #[test]
776 fn test_bufferbloat_colorized_colored_f() {
777 let result = bufferbloat_colorized(BufferbloatGrade::F, 150.0, false, Theme::Dark);
778 assert!(result.contains("F"));
779 }
780
781 #[test]
784 fn test_format_overall_rating_nc_excellent() {
785 let result = make_test_result();
786 let output = format_overall_rating(&result, true, Theme::Dark);
787 assert!(output.contains("Overall:"));
788 }
789
790 #[test]
791 fn test_format_overall_rating_colored() {
792 let result = make_test_result();
793 let output = format_overall_rating(&result, false, Theme::Dark);
794 assert!(output.contains("Overall:"));
795 }
796
797 #[test]
798 fn test_format_overall_rating_light_theme() {
799 let result = make_test_result();
800 let output = format_overall_rating(&result, false, Theme::Light);
801 assert!(output.contains("Overall:"));
802 }
803
804 #[test]
807 fn test_degradation_str_minimal() {
808 let result = degradation_str(12.0, Some(10.0), true, Theme::Dark);
810 assert!(result.contains("minimal"));
811 }
812
813 #[test]
814 fn test_degradation_str_moderate() {
815 let result = degradation_str(14.0, Some(10.0), true, Theme::Dark);
817 assert!(result.contains("moderate"));
818 }
819
820 #[test]
821 fn test_degradation_str_significant() {
822 let result = degradation_str(16.0, Some(10.0), true, Theme::Dark);
824 assert!(result.contains("significant"));
825 }
826
827 #[test]
828 fn test_degradation_str_no_idle() {
829 let result = degradation_str(15.0, None, false, Theme::Dark);
830 assert_eq!(result, "");
831 }
832
833 #[test]
834 fn test_degradation_str_zero_idle() {
835 let result = degradation_str(15.0, Some(0.0), false, Theme::Dark);
836 assert_eq!(result, "");
837 }
838
839 #[test]
840 fn test_degradation_str_negative_idle() {
841 let result = degradation_str(15.0, Some(-5.0), false, Theme::Dark);
842 assert_eq!(result, "");
843 }
844
845 #[test]
846 fn test_degradation_str_nc_mode() {
847 let result = degradation_str(12.0, Some(10.0), true, Theme::Dark);
848 assert!(result.contains("["));
849 }
850
851 #[test]
852 fn test_degradation_str_colored_minimal() {
853 let result = degradation_str(12.0, Some(10.0), false, Theme::Dark);
854 assert!(!result.is_empty());
855 }
856
857 #[test]
860 fn test_speed_components_helper() {
861 let result = format_speed_plain(100_000_000.0, false);
863 assert!(result.contains("100.00"));
864 assert!(result.contains("Mb/s"));
865 }
866}