1use crate::test_config::TestConfig;
11use std::io::IsTerminal;
12
13#[must_use]
28pub fn get_terminal_width() -> Option<u16> {
29 if !std::io::stdout().is_terminal() {
30 return None;
31 }
32 terminal_size::terminal_size().map(|(w, _)| w.0)
33}
34
35#[must_use]
49pub fn get_terminal_width_bounded(min_width: u16, max_width: u16, default_width: u16) -> u16 {
50 match get_terminal_width() {
51 Some(w) => w.clamp(min_width, max_width),
52 None => default_width,
53 }
54}
55
56#[must_use]
66pub fn calculate_bandwidth(total_bytes: u64, elapsed_secs: f64) -> f64 {
67 if elapsed_secs > 0.0 {
68 (total_bytes as f64 * 8.0) / elapsed_secs
71 } else {
72 0.0
73 }
74}
75
76#[deprecated(
88 since = "0.9.0",
89 note = "Use TestConfig::stream_count_for(single) instead"
90)]
91#[must_use]
92pub fn determine_stream_count(single: bool) -> usize {
93 TestConfig::stream_count_for(single)
94}
95
96#[must_use]
106pub fn format_distance(km: f64) -> String {
107 if km < 100.0 {
108 format!("{km:.1} km")
109 } else {
110 format!("{km:.0} km")
111 }
112}
113
114#[must_use]
125pub fn format_data_size(bytes: u64) -> String {
126 if bytes < 1024 * 1024 {
129 format!("{:.1} KB", bytes as f64 / 1024.0)
130 } else if bytes < 1024 * 1024 * 1024 {
131 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
132 } else {
133 format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
134 }
135}
136
137#[must_use]
147pub fn is_valid_ipv4(s: &str) -> bool {
148 let parts: Vec<&str> = s.split('.').collect();
149 if parts.len() != 4 {
150 return false;
151 }
152 parts.iter().all(|p| p.parse::<u8>().is_ok())
153}
154
155#[must_use]
169pub fn bar_chart(value: f64, max: f64, width: usize) -> String {
170 if max <= 0.0 || width == 0 {
171 return "░".repeat(width);
172 }
173 let pct = (value / max).clamp(0.0, 1.0);
174 let filled = (pct * width as f64).round().clamp(0.0, usize::MAX as f64) as usize;
176 let empty = width.saturating_sub(filled);
177 format!("{}{}", "█".repeat(filled), "░".repeat(empty))
178}
179
180#[must_use]
198pub fn tabular_number(value: f64, width: usize, decimals: usize) -> String {
199 if decimals == 0 {
200 format!("{:>width$}", value.clamp(0.0, i64::MAX as f64) as i64)
203 } else {
204 format!("{value:>width$.decimals$}")
205 }
206}
207
208#[must_use]
223pub fn format_speed_tabular(bps: f64, total_width: usize) -> String {
224 let (value, unit) = if bps >= 1_000_000_000.0 {
225 (bps / 1_000_000_000.0, "Gb/s")
226 } else if bps >= 1_000_000.0 {
227 (bps / 1_000_000.0, "Mb/s")
228 } else if bps >= 1_000.0 {
229 (bps / 1_000.0, "Kb/s")
230 } else {
231 return format!(
233 "{:>total_width$} b/s",
234 bps.clamp(0.0, i64::MAX as f64) as i64
235 );
236 };
237 let unit_width = unit.len();
238 let val_width = total_width.saturating_sub(unit_width + 1); format!("{value:>val_width$.2} {unit}")
240}
241
242#[must_use]
253pub fn format_latency_tabular(ms: f64, width: usize) -> String {
254 format!("{ms:>width$.1} ms")
255}
256
257#[must_use]
259pub fn format_jitter_tabular(ms: f64, width: usize) -> String {
260 format!("{ms:>width$.1} ms")
261}
262
263#[must_use]
265pub fn format_loss_tabular(pct: f64, width: usize) -> String {
266 format!("{pct:>width$.1}%")
267}
268
269#[must_use]
272pub fn format_data_size_tabular(bytes: u64, width: usize) -> String {
273 let (value, unit) = if bytes < 1024 * 1024 {
275 (bytes as f64 / 1024.0, "KB")
276 } else if bytes < 1024 * 1024 * 1024 {
277 (bytes as f64 / (1024.0 * 1024.0), "MB")
278 } else {
279 let val = bytes as f64 / (1024.0 * 1024.0 * 1024.0);
280 let unit_width = 2; let val_width = width.saturating_sub(unit_width + 1);
282 return format!("{val:>val_width$.2} GB");
283 };
284 let unit_width = unit.len();
285 let val_width = width.saturating_sub(unit_width + 1);
286 format!("{value:>val_width$.1} {unit}")
287}
288
289#[must_use]
291pub fn format_duration_tabular(secs: f64, width: usize) -> String {
292 if secs < 60.0 {
293 format!("{secs:>width$.1}s")
294 } else {
295 let mins = (secs / 60.0).clamp(0.0, u64::MAX as f64) as u64;
297 let rem = secs % 60.0;
298 format!("{mins:>width$}m {rem:04.1}s")
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_calculate_bandwidth_normal() {
308 assert!((calculate_bandwidth(10_000_000, 2.0) - 40_000_000.0).abs() < f64::EPSILON);
309 }
310
311 #[test]
312 fn test_calculate_bandwidth_zero_elapsed() {
313 assert!(calculate_bandwidth(10_000_000, 0.0).abs() < f64::EPSILON);
314 }
315
316 #[test]
317 fn test_determine_stream_count_single() {
318 #[allow(deprecated)]
320 {
321 assert_eq!(determine_stream_count(true), 1);
322 }
323 }
324
325 #[test]
326 fn test_determine_stream_count_multi() {
327 #[allow(deprecated)]
329 {
330 assert_eq!(determine_stream_count(false), 4);
331 }
332 }
333
334 #[test]
335 fn test_format_distance_under_100() {
336 assert_eq!(format_distance(50.5), "50.5 km");
337 assert_eq!(format_distance(99.9), "99.9 km");
338 }
339
340 #[test]
341 fn test_format_distance_100_plus() {
342 assert_eq!(format_distance(100.0), "100 km");
343 assert_eq!(format_distance(150.5), "150 km");
344 }
345
346 #[test]
347 fn test_format_data_size_bytes() {
348 assert!(format_data_size(512).contains("KB"));
349 }
350
351 #[test]
352 fn test_format_data_size_kilobytes() {
353 assert!(format_data_size(500 * 1024).contains("KB"));
354 }
355
356 #[test]
357 fn test_format_data_size_megabytes() {
358 assert!(format_data_size(10 * 1024 * 1024).contains("MB"));
359 }
360
361 #[test]
362 fn test_format_data_size_gigabytes() {
363 assert!(format_data_size(4 * 1024 * 1024 * 1024).contains("GB"));
364 }
365
366 #[test]
367 fn test_is_valid_ipv4_valid() {
368 assert!(is_valid_ipv4("192.168.1.1"));
369 assert!(is_valid_ipv4("0.0.0.0"));
370 assert!(is_valid_ipv4("255.255.255.255"));
371 }
372
373 #[test]
374 fn test_is_valid_ipv4_invalid() {
375 assert!(!is_valid_ipv4("256.1.1.1"));
376 assert!(!is_valid_ipv4("1.2.3"));
377 assert!(!is_valid_ipv4("abc"));
378 assert!(!is_valid_ipv4(""));
379 assert!(!is_valid_ipv4("1.2.3.4.5"));
380 }
381
382 #[test]
383 fn test_bar_chart_half() {
384 let bar = bar_chart(50.0, 100.0, 10);
385 assert_eq!(bar.chars().count(), 10);
386 assert_eq!(bar, "█████░░░░░");
387 }
388
389 #[test]
390 fn test_bar_chart_full() {
391 let bar = bar_chart(100.0, 100.0, 10);
392 assert_eq!(bar.chars().count(), 10);
393 assert_eq!(bar, "██████████");
394 }
395
396 #[test]
397 fn test_bar_chart_empty_val() {
398 let bar = bar_chart(0.0, 100.0, 10);
399 assert_eq!(bar, "░░░░░░░░░░");
400 }
401
402 #[test]
403 fn test_bar_chart_zero_max() {
404 let bar = bar_chart(50.0, 0.0, 10);
405 assert_eq!(bar, "░░░░░░░░░░");
406 }
407
408 #[test]
409 fn test_bar_chart_zero_width() {
410 let bar = bar_chart(50.0, 100.0, 0);
411 assert_eq!(bar, "");
412 }
413
414 #[test]
415 fn test_bar_chart_over_max() {
416 let bar = bar_chart(150.0, 100.0, 10);
417 assert_eq!(bar, "██████████"); }
419
420 #[test]
421 fn test_get_terminal_width_bounded_default() {
422 let width = get_terminal_width_bounded(60, 120, 80);
424 assert!((60..=120).contains(&width));
425 }
426
427 #[test]
428 fn test_get_terminal_width_bounded_clamps() {
429 let def = get_terminal_width_bounded(80, 100, 90);
432 assert!((80..=120).contains(&def));
434
435 let def2 = get_terminal_width_bounded(60, 80, 70);
436 assert!((60..=120).contains(&def2));
437 }
438
439 #[test]
442 fn test_tabular_number_right_aligned() {
443 assert_eq!(tabular_number(15.2, 8, 1), " 15.2");
444 assert_eq!(tabular_number(150.0, 8, 1), " 150.0");
445 assert_eq!(tabular_number(1234.5, 8, 1), " 1234.5");
446 }
447
448 #[test]
449 fn test_tabular_number_zero_decimals() {
450 assert_eq!(tabular_number(42.0, 6, 0), " 42");
451 assert_eq!(tabular_number(1000.0, 6, 0), " 1000");
452 }
453
454 #[test]
455 fn test_format_speed_tabular_mbps() {
456 let s = format_speed_tabular(150_000_000.0, 14);
457 assert_eq!(s, " 150.00 Mb/s");
458 assert_eq!(s.len(), 14);
459 }
460
461 #[test]
462 fn test_format_speed_tabular_gbps() {
463 let s = format_speed_tabular(1_200_000_000.0, 14);
464 assert_eq!(s, " 1.20 Gb/s");
465 assert_eq!(s.len(), 14);
466 }
467
468 #[test]
469 fn test_format_speed_tabular_kbps() {
470 let s = format_speed_tabular(50_000.0, 14);
471 assert_eq!(s, " 50.00 Kb/s");
472 assert_eq!(s.len(), 14);
473 }
474
475 #[test]
476 fn test_format_latency_tabular() {
477 assert_eq!(format_latency_tabular(12.1, 10), " 12.1 ms");
478 assert_eq!(format_latency_tabular(150.5, 10), " 150.5 ms");
479 }
480
481 #[test]
482 fn test_format_jitter_tabular() {
483 assert_eq!(format_jitter_tabular(1.5, 10), " 1.5 ms");
484 }
485
486 #[test]
487 fn test_format_loss_tabular() {
488 assert_eq!(format_loss_tabular(0.0, 8), " 0.0%");
489 assert_eq!(format_loss_tabular(5.5, 8), " 5.5%");
490 }
491
492 #[test]
493 fn test_format_data_size_tabular_mb() {
494 let s = format_data_size_tabular(15 * 1024 * 1024, 10);
495 assert_eq!(s, " 15.0 MB");
496 assert_eq!(s.len(), 10);
497 }
498
499 #[test]
500 fn test_format_data_size_tabular_gb() {
501 let s = format_data_size_tabular(4 * 1024 * 1024 * 1024, 10);
502 assert_eq!(s, " 4.00 GB");
503 assert_eq!(s.len(), 10);
504 }
505
506 #[test]
507 fn test_format_duration_tabular_seconds() {
508 assert_eq!(format_duration_tabular(30.5, 8), " 30.5s");
509 }
510
511 #[test]
512 fn test_format_duration_tabular_minutes() {
513 let s = format_duration_tabular(90.5, 10);
514 assert!(s.contains('m'));
515 }
516
517 #[cfg(test)]
519 mod proptests {
520 use super::*;
521 use proptest::prelude::*;
522
523 proptest! {
524 #[test]
525 fn prop_bandwidth_always_non_negative(bytes in 0u64..u64::MAX, elapsed in 0.0f64..1e6) {
526 let result = calculate_bandwidth(bytes, elapsed);
527 prop_assert!(result >= 0.0, "bandwidth must never be negative");
528 }
529
530 #[test]
531 fn prop_bandwidth_zero_elapsed_returns_zero(bytes in 0u64..1_000_000) {
532 let result = calculate_bandwidth(bytes, 0.0);
533 prop_assert!(result.abs() < f64::EPSILON);
534 }
535
536 #[test]
537 fn prop_bandwidth_linear_scaling(bytes in 1u64..1_000_000) {
538 let r1 = calculate_bandwidth(bytes, 1.0);
539 let r2 = calculate_bandwidth(bytes, 2.0);
540 prop_assert!((r1 - 2.0 * r2).abs() < f64::EPSILON, "doubling time should halve bandwidth");
541 }
542
543 #[test]
544 fn prop_bar_chart_length(width in 1usize..200, value in 0.0f64..1000.0, max in 1.0f64..1000.0) {
545 let bar = bar_chart(value, max, width);
546 prop_assert_eq!(bar.chars().count(), width, "bar must have exactly width characters");
547 }
548
549 #[test]
550 fn prop_distance_always_ends_with_km(km in 0.0f64..10000.0) {
551 let result = format_distance(km);
552 prop_assert!(result.ends_with(" km"));
553 }
554
555 #[test]
556 fn prop_data_size_always_has_unit(bytes in 0u64..u64::MAX) {
557 let result = format_data_size(bytes);
558 prop_assert!(
559 result.contains("KB") || result.contains("MB") || result.contains("GB"),
560 "formatted size must contain a unit"
561 );
562 }
563 }
564 }
565}