use std::process::Command;
use crate::verdict::PingStats;
pub const PING_COUNT: u32 = 3;
pub fn run_ping(host: &str, count: u32) -> std::io::Result<PingStats> {
let output = Command::new("ping")
.arg("-c")
.arg(count.to_string())
.arg("-W")
.arg("2000")
.arg(host)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
Ok(parse_ping_output(host, count, &stdout, &stderr))
}
pub async fn run_ping_async(host: String, count: u32) -> PingStats {
let outer_fallback = host.clone();
let inner_fallback = host.clone();
let inner = tokio::task::spawn_blocking(move || {
run_ping(&host, count).unwrap_or_else(|_| PingStats {
target: inner_fallback,
samples_ms: Vec::new(),
packets_sent: count,
packets_received: 0,
})
});
inner
.await
.unwrap_or_else(|_| PingStats {
target: outer_fallback,
samples_ms: Vec::new(),
packets_sent: count,
packets_received: 0,
})
}
pub fn parse_ping_output(host: &str, sent: u32, stdout: &str, _stderr: &str) -> PingStats {
let samples = extract_rtts(stdout);
let received = samples.len() as u32;
PingStats {
target: host.to_string(),
samples_ms: samples,
packets_sent: sent,
packets_received: received,
}
}
pub fn extract_rtts(stdout: &str) -> Vec<f64> {
stdout
.lines()
.filter_map(extract_time_from_line)
.collect()
}
fn extract_time_from_line(line: &str) -> Option<f64> {
let idx = line.find("time=")?;
let rest = &line[idx + "time=".len()..];
let num_str: String = rest
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
num_str.parse::<f64>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
const MACOS_OUTPUT: &str = "PING 8.8.8.8 (8.8.8.8): 56 data bytes\n\
64 bytes from 8.8.8.8: icmp_seq=0 ttl=119 time=12.345 ms\n\
64 bytes from 8.8.8.8: icmp_seq=1 ttl=119 time=13.456 ms\n\
64 bytes from 8.8.8.8: icmp_seq=2 ttl=119 time=11.234 ms\n\
64 bytes from 8.8.8.8: icmp_seq=3 ttl=119 time=14.567 ms\n\n\
--- 8.8.8.8 ping statistics ---\n\
4 packets transmitted, 4 packets received, 0.0% packet loss\n\
round-trip min/avg/max/stddev = 11.234/12.900/14.567/1.234 ms\n";
const LINUX_OUTPUT: &str = "PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n\
64 bytes from 8.8.8.8: icmp_seq=1 ttl=119 time=12.3 ms\n\
64 bytes from 8.8.8.8: icmp_seq=2 ttl=119 time=13.4 ms\n\
64 bytes from 8.8.8.8: icmp_seq=3 ttl=119 time=11.2 ms\n\n\
--- 8.8.8.8 ping statistics ---\n\
3 packets transmitted, 3 received, 0% packet loss, time 2003ms\n\
rtt min/avg/max/mdev = 11.200/12.300/13.400/0.900 ms\n";
#[test]
fn parses_macos_rtts_in_order() {
let rtts = extract_rtts(MACOS_OUTPUT);
assert_eq!(rtts, vec![12.345, 13.456, 11.234, 14.567]);
}
#[test]
fn parses_linux_rtts_in_order() {
let rtts = extract_rtts(LINUX_OUTPUT);
assert_eq!(rtts, vec![12.3, 13.4, 11.2]);
}
fn approx(a: Option<f64>, b: f64) -> bool {
match a {
Some(v) => (v - b).abs() < 1e-3,
None => false,
}
}
#[test]
fn macos_stats_have_no_loss() {
let s = parse_ping_output("8.8.8.8", 4, MACOS_OUTPUT, "");
assert_eq!(s.target, "8.8.8.8");
assert_eq!(s.packets_sent, 4);
assert_eq!(s.packets_received, 4);
assert_eq!(s.loss_pct(), 0.0);
assert!(approx(s.avg_ms(), 12.9));
}
#[test]
fn linux_stats_have_no_loss() {
let s = parse_ping_output("8.8.8.8", 3, LINUX_OUTPUT, "");
assert_eq!(s.packets_sent, 3);
assert_eq!(s.packets_received, 3);
assert_eq!(s.loss_pct(), 0.0);
assert!(approx(s.avg_ms(), 12.3));
}
#[test]
fn packet_loss_when_some_replies_missing() {
let partial = "PING x (1.2.3.4): 56 data bytes\n\
64 bytes from 1.2.3.4: icmp_seq=0 ttl=64 time=10.000 ms\n\
64 bytes from 1.2.3.4: icmp_seq=2 ttl=64 time=11.000 ms\n";
let s = parse_ping_output("x", 4, partial, "");
assert_eq!(s.packets_sent, 4);
assert_eq!(s.packets_received, 2);
assert_eq!(s.loss_pct(), 50.0);
}
#[test]
fn empty_output_yields_no_samples() {
let s = parse_ping_output("x", 4, "", "");
assert!(s.samples_ms.is_empty());
assert_eq!(s.packets_sent, 4);
assert_eq!(s.packets_received, 0);
assert_eq!(s.loss_pct(), 100.0);
}
#[test]
fn ignores_summary_line_time_field() {
let summary_only = "--- 8.8.8.8 ping statistics ---\n\
3 packets transmitted, 3 received, 0% packet loss, time 2003ms\n";
let rtts = extract_rtts(summary_only);
assert!(rtts.is_empty(), "got {:?}", rtts);
}
#[test]
fn handles_decimal_with_one_digit() {
let line = "64 bytes from 1.2.3.4: icmp_seq=0 ttl=64 time=5.1 ms";
assert_eq!(extract_time_from_line(line), Some(5.1));
}
#[test]
fn handles_integer_time() {
let line = "64 bytes from 1.2.3.4: icmp_seq=0 ttl=64 time=7 ms";
assert_eq!(extract_time_from_line(line), Some(7.0));
}
}