tcping/
stats.rs

1//! Runtime statistics and data structures.
2//!
3//! [Stats] accumulates per-probe results and emits a final [Summary].
4//! Both [PingResult] and [Summary] are serde-serialisable so the
5//! formatting layer can dump them directly.
6
7use crate::cli::Args;
8use serde::Serialize;
9use std::net::SocketAddr;
10
11/// Result of a single probe.
12///
13/// This structure may be serialised as JSON / CSV by the formatter layer.
14#[derive(Clone, Serialize)]
15pub struct PingResult {
16    pub success: bool,
17    pub duration_ms: f64,
18    pub jitter_ms: Option<f64>,
19    pub addr: SocketAddr,
20}
21
22/// Roll-up of an entire probing session.
23#[derive(Serialize)]
24pub struct Summary {
25    pub addr: SocketAddr,
26    pub total_attempts: usize,
27    pub successful_pings: usize,
28    pub packet_loss: f64,
29    pub min_duration_ms: f64,
30    pub avg_duration_ms: f64,
31    pub max_duration_ms: f64,
32    pub resolve_time_ms: f64,
33}
34
35/// Mutable accumulator used during a session.
36pub struct Stats {
37    addr: SocketAddr,
38    sent: usize,
39    ok: usize,
40    total_rtt: f64,
41    min_rtt: f64,
42    max_rtt: f64,
43    last_rtt: Option<f64>,
44    resolve_ms: f64,
45}
46
47impl Stats {
48    /// Create a new accumulator.
49    pub fn new(addr: SocketAddr, resolve_ms: f64) -> Self {
50        Self {
51            addr,
52            sent: 0,
53            ok: 0,
54            total_rtt: 0.0,
55            min_rtt: f64::MAX,
56            max_rtt: 0.0,
57            last_rtt: None,
58            resolve_ms,
59        }
60    }
61
62    /// Feed one probe result and obtain a [PingResult] to hand to the formatter.
63    pub fn feed(&mut self, success: bool, rtt: f64, want_jitter: bool) -> PingResult {
64        self.sent += 1;
65
66        let jitter = if want_jitter {
67            self.last_rtt.map(|prev| (rtt - prev).abs())
68        } else {
69            None
70        };
71
72        if success {
73            self.ok += 1;
74            self.total_rtt += rtt;
75            self.min_rtt = self.min_rtt.min(rtt);
76            self.max_rtt = self.max_rtt.max(rtt);
77            self.last_rtt = Some(rtt);
78        }
79
80        PingResult {
81            success,
82            duration_ms: rtt,
83            jitter_ms: jitter,
84            addr: self.addr,
85        }
86    }
87
88    /// Should the main loop continue?
89    pub fn should_continue(&self, args: &Args) -> bool {
90        args.continuous || self.sent < args.count
91    }
92
93    /// Should we break early because -e/--exit-on-success?
94    pub fn should_break(&self, success: bool, args: &Args) -> bool {
95        success && args.exit_on_success
96    }
97
98    /// Produce the final [Summary].
99    pub fn summary(&self) -> Summary {
100        let packet_loss = if self.sent == 0 {
101            0.0
102        } else {
103            100.0 * (1.0 - self.ok as f64 / self.sent as f64)
104        };
105
106        Summary {
107            addr: self.addr,
108            total_attempts: self.sent,
109            successful_pings: self.ok,
110            packet_loss,
111            min_duration_ms: if self.ok > 0 { self.min_rtt } else { 0.0 },
112            avg_duration_ms: if self.ok > 0 {
113                self.total_rtt / self.ok as f64
114            } else {
115                0.0
116            },
117            max_duration_ms: if self.ok > 0 { self.max_rtt } else { 0.0 },
118            resolve_time_ms: self.resolve_ms,
119        }
120    }
121
122    /// Map statistics to a conventional Unix exit code.
123    pub fn exit_code(&self) -> i32 {
124        if self.ok == self.sent && self.sent > 0 {
125            0
126        } else {
127            1
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::net::{IpAddr, Ipv4Addr};
136
137    fn loopback_addr() -> SocketAddr {
138        SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 80)
139    }
140
141    #[test]
142    fn summary_handles_zero_probes() {
143        let stats = Stats::new(loopback_addr(), 0.0);
144        let summary = stats.summary();
145        assert_eq!(summary.total_attempts, 0);
146        assert_eq!(summary.packet_loss, 0.0);
147    }
148
149    #[test]
150    fn jitter_is_difference_between_successive_successes() {
151        let mut stats = Stats::new(loopback_addr(), 0.0);
152        let first = stats.feed(true, 10.0, true);
153        assert_eq!(first.jitter_ms, None);
154
155        let second = stats.feed(true, 15.0, true);
156        assert_eq!(second.jitter_ms, Some(5.0));
157    }
158}