Add end-to-end coverage for latency checks

Refactor the ping execution path behind a small executor so tests can exercise the command launch, output handling, and error branches without touching the network.

This keeps the current platform-specific ping behavior unchanged while adding coverage for the actual flow requested in review.
This commit is contained in:
suryansh deshwal 2026-05-30 00:03:37 +05:30
parent 3c5e510949
commit f5158614fe

View file

@ -1,5 +1,6 @@
use std::io;
use std::net::IpAddr;
use std::process::Command;
use std::process::{Command, Output};
use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Eq)]
@ -24,11 +25,16 @@ pub async fn measure_latency(ip: IpAddr) -> (IpAddr, LatencyStatus) {
}
fn measure_latency_inner(ip: IpAddr) -> LatencyStatus {
measure_latency_with_executor(ip, &SystemPingExecutor)
}
fn measure_latency_with_executor(ip: IpAddr, executor: &impl PingExecutor) -> LatencyStatus {
let ip = ip.to_string();
let output = ping_command(&ip).output();
let mut command = ping_command(&ip);
let output = executor.execute(&mut command);
match output {
Ok(output) if output.status.success() => {
Ok(output) if output.success => {
let stdout = String::from_utf8_lossy(&output.stdout);
parse_ping_latency(&stdout).map_or_else(
|| LatencyStatus::Failed("Latency unavailable".to_string()),
@ -47,6 +53,34 @@ fn measure_latency_inner(ip: IpAddr) -> LatencyStatus {
}
}
struct PingOutput {
success: bool,
stdout: Vec<u8>,
stderr: Vec<u8>,
}
impl From<Output> for PingOutput {
fn from(output: Output) -> Self {
Self {
success: output.status.success(),
stdout: output.stdout,
stderr: output.stderr,
}
}
}
trait PingExecutor {
fn execute(&self, command: &mut Command) -> io::Result<PingOutput>;
}
struct SystemPingExecutor;
impl PingExecutor for SystemPingExecutor {
fn execute(&self, command: &mut Command) -> io::Result<PingOutput> {
command.output().map(PingOutput::from)
}
}
fn ping_command(ip: &str) -> Command {
let mut command = Command::new("ping");
@ -83,7 +117,108 @@ fn parse_ping_latency(output: &str) -> Option<Duration> {
#[cfg(test)]
mod tests {
use super::parse_ping_latency;
use std::cell::RefCell;
use std::ffi::OsString;
use std::io;
use std::net::{IpAddr, Ipv4Addr};
use std::process::Command;
use std::time::Duration;
use super::{
LatencyStatus, PingExecutor, PingOutput, measure_latency_with_executor, parse_ping_latency,
ping_command,
};
struct FakePingExecutor {
output: RefCell<Option<io::Result<PingOutput>>>,
observed_command: RefCell<Option<(OsString, Vec<OsString>)>>,
}
impl FakePingExecutor {
fn new(output: io::Result<PingOutput>) -> Self {
Self {
output: RefCell::new(Some(output)),
observed_command: RefCell::new(None),
}
}
fn observed_command(&self) -> (String, Vec<String>) {
let (program, args) = self
.observed_command
.borrow()
.as_ref()
.expect("ping command should be executed")
.clone();
(
program.to_string_lossy().into_owned(),
args.into_iter()
.map(|arg| arg.to_string_lossy().into_owned())
.collect(),
)
}
}
impl PingExecutor for FakePingExecutor {
fn execute(&self, command: &mut Command) -> io::Result<PingOutput> {
self.observed_command.replace(Some((
command.get_program().to_os_string(),
command.get_args().map(|arg| arg.to_os_string()).collect(),
)));
self.output
.borrow_mut()
.take()
.expect("fake ping output should only be consumed once")
}
}
fn successful_ping(stdout: &str) -> io::Result<PingOutput> {
Ok(PingOutput {
success: true,
stdout: stdout.as_bytes().to_vec(),
stderr: Vec::new(),
})
}
fn failed_ping(stderr: &str) -> io::Result<PingOutput> {
Ok(PingOutput {
success: false,
stdout: Vec::new(),
stderr: stderr.as_bytes().to_vec(),
})
}
fn command_args(command: &Command) -> Vec<String> {
command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect()
}
#[cfg(target_os = "windows")]
fn expected_ping_args(ip: &str) -> Vec<String> {
["-n", "1", "-w", "1000", ip]
.into_iter()
.map(String::from)
.collect()
}
#[cfg(target_os = "macos")]
fn expected_ping_args(ip: &str) -> Vec<String> {
["-c", "1", "-W", "1000", ip]
.into_iter()
.map(String::from)
.collect()
}
#[cfg(all(unix, not(target_os = "macos")))]
fn expected_ping_args(ip: &str) -> Vec<String> {
["-c", "1", "-W", "1", ip]
.into_iter()
.map(String::from)
.collect()
}
#[test]
fn parses_linux_ping_latency() {
@ -105,4 +240,86 @@ mod tests {
assert_eq!(parse_ping_latency(output).unwrap().as_millis(), 1);
}
#[test]
fn builds_ping_command_for_current_platform() {
let command = ping_command("1.1.1.1");
assert_eq!(command.get_program().to_string_lossy(), "ping");
assert_eq!(command_args(&command), expected_ping_args("1.1.1.1"));
}
#[test]
fn executes_ping_command_and_parses_latency() {
let executor = FakePingExecutor::new(successful_ping(
"64 bytes from 1.1.1.1: icmp_seq=1 ttl=58 time=14.2 ms",
));
let status =
measure_latency_with_executor(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), &executor);
assert_eq!(
status,
LatencyStatus::Measured(Duration::from_micros(14_200))
);
assert_eq!(
executor.observed_command(),
("ping".to_string(), expected_ping_args("1.1.1.1"))
);
}
#[test]
fn reports_unavailable_latency_when_ping_output_has_no_latency() {
let executor = FakePingExecutor::new(successful_ping("1 packets transmitted, 0 received"));
let status =
measure_latency_with_executor(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), &executor);
assert_eq!(
status,
LatencyStatus::Failed("Latency unavailable".to_string())
);
}
#[test]
fn reports_ping_stderr_when_command_fails() {
let executor = FakePingExecutor::new(failed_ping("Request timeout for icmp_seq 0\n"));
let status =
measure_latency_with_executor(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), &executor);
assert_eq!(
status,
LatencyStatus::Failed("Request timeout for icmp_seq 0".to_string())
);
}
#[test]
fn reports_unavailable_latency_when_failed_ping_has_no_stderr() {
let executor = FakePingExecutor::new(failed_ping(""));
let status =
measure_latency_with_executor(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), &executor);
assert_eq!(
status,
LatencyStatus::Failed("Latency unavailable".to_string())
);
}
#[test]
fn reports_launch_error_when_ping_cannot_start() {
let executor = FakePingExecutor::new(Err(io::Error::new(
io::ErrorKind::NotFound,
"ping not found",
)));
let status =
measure_latency_with_executor(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), &executor);
assert_eq!(
status,
LatencyStatus::Failed("Unable to run ping".to_string())
);
}
}