use crate::config::MAJORITY_THRESHOLD;
use crate::config::{LATENCY_OK_THRESHOLD_MS, LATENCY_SLOW_THRESHOLD_MS};
use crate::monitor::{ConnectionStatus, NetworkStats, ProbeRound};
use chrono::Utc;
pub fn is_packet_lost(round: &ProbeRound) -> bool {
let failed_count = round.results.iter().filter(|r| !r.success).count();
failed_count >= MAJORITY_THRESHOLD
}
fn get_round_latency(round: &ProbeRound) -> Option<f64> {
let successful_latencies: Vec<f64> =
round.results.iter().filter_map(|r| r.latency_ms).collect();
if successful_latencies.is_empty() {
None
} else {
Some(successful_latencies.iter().sum::<f64>() / successful_latencies.len() as f64)
}
}
fn determine_status_from_latency(latency_ms: Option<f64>) -> ConnectionStatus {
match latency_ms {
None => ConnectionStatus::Disconnected,
Some(lat) if lat > LATENCY_SLOW_THRESHOLD_MS => ConnectionStatus::Disconnected,
Some(lat) if lat > LATENCY_OK_THRESHOLD_MS => ConnectionStatus::Slow,
Some(_) => ConnectionStatus::Ok,
}
}
pub fn aggregate_stats(rounds: &[ProbeRound]) -> NetworkStats {
if rounds.is_empty() {
return NetworkStats {
avg_latency_ms: 0.0,
loss_pct: 0.0,
jitter_ms: 0.0,
status: ConnectionStatus::Disconnected,
timestamp: Utc::now(),
};
}
let latest_round = rounds.last().unwrap();
let is_lost = is_packet_lost(latest_round);
let avg_latency_ms = get_round_latency(latest_round).unwrap_or(f64::MAX);
let status = if is_lost {
ConnectionStatus::Disconnected
} else {
determine_status_from_latency(Some(avg_latency_ms))
};
NetworkStats {
avg_latency_ms: if avg_latency_ms == f64::MAX {
0.0
} else {
avg_latency_ms
},
loss_pct: 0.0, jitter_ms: 0.0, status,
timestamp: Utc::now(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monitor::PingResult;
use chrono::Utc;
fn create_ping_result(target: &str, success: bool, latency_ms: Option<f64>) -> PingResult {
PingResult {
target: target.to_string(),
success,
latency_ms,
timestamp: Utc::now(),
}
}
fn create_probe_round(results: Vec<(bool, Option<f64>)>) -> ProbeRound {
let targets = [
"8.8.8.8",
"s3.amazonaws.com",
"portal.azure.com",
"1.1.1.1",
"9.9.9.9",
"208.67.222.222",
];
let ping_results: Vec<PingResult> = results
.into_iter()
.enumerate()
.map(|(i, (success, latency))| {
create_ping_result(targets[i % targets.len()], success, latency)
})
.collect();
ProbeRound {
results: ping_results,
timestamp: Utc::now(),
}
}
#[test]
fn test_is_packet_lost_all_success() {
let round = create_probe_round(vec![
(true, Some(50.0)),
(true, Some(60.0)),
(true, Some(55.0)),
(true, Some(58.0)),
(true, Some(50.0)),
(true, Some(52.0)),
]);
assert!(!is_packet_lost(&round));
}
#[test]
fn test_is_packet_lost_one_failure() {
let round = create_probe_round(vec![
(false, None),
(true, Some(60.0)),
(true, Some(55.0)),
(true, Some(58.0)),
(true, Some(50.0)),
(true, Some(52.0)),
]);
assert!(!is_packet_lost(&round));
}
#[test]
fn test_is_packet_lost_three_failures() {
let round = create_probe_round(vec![
(false, None),
(false, None),
(false, None),
(true, Some(58.0)),
(true, Some(50.0)),
(true, Some(52.0)),
]);
assert!(!is_packet_lost(&round));
}
#[test]
fn test_is_packet_lost_majority_failure() {
let round = create_probe_round(vec![
(false, None),
(false, None),
(false, None),
(false, None),
(true, Some(50.0)),
(true, Some(52.0)),
]);
assert!(is_packet_lost(&round));
}
#[test]
fn test_is_packet_lost_all_failure() {
let round = create_probe_round(vec![
(false, None),
(false, None),
(false, None),
(false, None),
(false, None),
(false, None),
]);
assert!(is_packet_lost(&round));
}
#[test]
fn test_aggregate_stats_empty() {
let stats = aggregate_stats(&[]);
assert_eq!(stats.status, ConnectionStatus::Disconnected);
}
#[test]
fn test_aggregate_stats_ok_latency() {
let rounds = vec![create_probe_round(vec![
(true, Some(50.0)),
(true, Some(60.0)),
(true, Some(55.0)),
(true, Some(55.0)),
(true, Some(55.0)),
(true, Some(55.0)),
])];
let stats = aggregate_stats(&rounds);
assert_eq!(stats.status, ConnectionStatus::Ok);
assert!((stats.avg_latency_ms - 55.0).abs() < 5.0);
}
#[test]
fn test_aggregate_stats_slow_latency() {
let rounds = vec![create_probe_round(vec![
(true, Some(150.0)),
(true, Some(160.0)),
(true, Some(155.0)),
(true, Some(155.0)),
(true, Some(155.0)),
(true, Some(155.0)),
])];
let stats = aggregate_stats(&rounds);
assert_eq!(stats.status, ConnectionStatus::Slow);
}
#[test]
fn test_aggregate_stats_disconnected_by_latency() {
let rounds = vec![create_probe_round(vec![
(true, Some(350.0)),
(true, Some(360.0)),
(true, Some(355.0)),
(true, Some(355.0)),
(true, Some(355.0)),
(true, Some(355.0)),
])];
let stats = aggregate_stats(&rounds);
assert_eq!(stats.status, ConnectionStatus::Disconnected);
}
#[test]
fn test_aggregate_stats_disconnected_by_loss() {
let rounds = vec![create_probe_round(vec![
(false, None),
(false, None),
(false, None),
(false, None),
(true, Some(50.0)),
(true, Some(52.0)),
])];
let stats = aggregate_stats(&rounds);
assert_eq!(stats.status, ConnectionStatus::Disconnected);
}
#[test]
fn test_instant_recovery() {
let round1 = create_probe_round(vec![
(false, None),
(false, None),
(false, None),
(false, None),
(false, None),
(false, None),
]);
let stats1 = aggregate_stats(&[round1.clone()]);
assert_eq!(stats1.status, ConnectionStatus::Disconnected);
let round2 = create_probe_round(vec![
(true, Some(50.0)),
(true, Some(60.0)),
(true, Some(55.0)),
(true, Some(55.0)),
(true, Some(55.0)),
(true, Some(55.0)),
]);
let stats2 = aggregate_stats(&[round1, round2]);
assert_eq!(stats2.status, ConnectionStatus::Ok);
}
}