1use std::collections::{HashMap, VecDeque};
33
34const WINDOW_SIZE: usize = 50;
40
41#[derive(Clone, Debug, PartialEq)]
49pub enum ChmHealthMetric {
50 Latency(f64),
52 PacketLoss(f64),
54 Bandwidth(f64),
56 Jitter(f64),
58 ErrorRate(f64),
60}
61
62impl ChmHealthMetric {
63 pub fn value(&self) -> f64 {
65 match self {
66 Self::Latency(v)
67 | Self::PacketLoss(v)
68 | Self::Bandwidth(v)
69 | Self::Jitter(v)
70 | Self::ErrorRate(v) => *v,
71 }
72 }
73
74 pub fn kind(&self) -> &'static str {
76 match self {
77 Self::Latency(_) => "latency",
78 Self::PacketLoss(_) => "packet_loss",
79 Self::Bandwidth(_) => "bandwidth",
80 Self::Jitter(_) => "jitter",
81 Self::ErrorRate(_) => "error_rate",
82 }
83 }
84}
85
86#[derive(Clone, Debug)]
92pub struct ChmHealthSample {
93 pub peer_id: String,
95 pub metric: ChmHealthMetric,
97 pub timestamp: u64,
99}
100
101#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
107pub enum ChmAlertSeverity {
108 Info,
110 Warning,
112 Critical,
114}
115
116#[derive(Clone, Debug)]
122pub struct ChmHealthAlert {
123 pub peer_id: String,
125 pub metric: ChmHealthMetric,
127 pub severity: ChmAlertSeverity,
129 pub value: f64,
131 pub threshold: f64,
133 pub timestamp: u64,
135}
136
137#[derive(Clone, Debug)]
146pub struct AlertThresholds {
147 pub max_latency_ms: f64,
149 pub max_packet_loss: f64,
151 pub min_bandwidth_bps: f64,
153 pub max_jitter_ms: f64,
155 pub max_error_rate: f64,
157}
158
159impl Default for AlertThresholds {
160 fn default() -> Self {
161 Self {
162 max_latency_ms: 500.0,
163 max_packet_loss: 0.05,
164 min_bandwidth_bps: 100_000.0,
165 max_jitter_ms: 100.0,
166 max_error_rate: 0.1,
167 }
168 }
169}
170
171impl AlertThresholds {
172 const WARNING_FACTOR: f64 = 0.8;
174
175 pub fn warning_latency_ms(&self) -> f64 {
177 self.max_latency_ms * Self::WARNING_FACTOR
178 }
179
180 pub fn warning_packet_loss(&self) -> f64 {
182 self.max_packet_loss * Self::WARNING_FACTOR
183 }
184
185 pub fn warning_bandwidth_bps(&self) -> f64 {
187 self.min_bandwidth_bps / Self::WARNING_FACTOR
188 }
189
190 pub fn warning_jitter_ms(&self) -> f64 {
192 self.max_jitter_ms * Self::WARNING_FACTOR
193 }
194
195 pub fn warning_error_rate(&self) -> f64 {
197 self.max_error_rate * Self::WARNING_FACTOR
198 }
199}
200
201#[derive(Clone, Debug)]
207pub struct ConnectionHealth {
208 pub peer_id: String,
210 pub health_score: f64,
212 pub samples: VecDeque<ChmHealthSample>,
214 pub last_updated: u64,
216 pub alert_count: u32,
218}
219
220impl ConnectionHealth {
221 fn new(peer_id: String) -> Self {
222 Self {
223 peer_id,
224 health_score: 1.0,
225 samples: VecDeque::new(),
226 last_updated: 0,
227 alert_count: 0,
228 }
229 }
230
231 fn values_for_kind(&self, kind: &str) -> Vec<f64> {
233 self.samples
234 .iter()
235 .filter(|s| s.metric.kind() == kind)
236 .map(|s| s.metric.value())
237 .collect()
238 }
239
240 fn mean(values: &[f64]) -> Option<f64> {
242 if values.is_empty() {
243 return None;
244 }
245 Some(values.iter().sum::<f64>() / values.len() as f64)
246 }
247}
248
249#[derive(Clone, Debug, Default)]
255pub struct ChmMonitorStats {
256 pub total_peers: usize,
258 pub healthy_peers: usize,
260 pub degraded_peers: usize,
262 pub critical_peers: usize,
264 pub total_alerts: usize,
266 pub avg_health_score: f64,
268}
269
270pub struct ConnectionHealthMonitor {
277 pub thresholds: AlertThresholds,
279 pub connections: HashMap<String, ConnectionHealth>,
281 pub alerts: VecDeque<ChmHealthAlert>,
283 pub max_alerts: usize,
285}
286
287impl ConnectionHealthMonitor {
288 pub fn new(thresholds: AlertThresholds, max_alerts: usize) -> Self {
290 Self {
291 thresholds,
292 connections: HashMap::new(),
293 alerts: VecDeque::new(),
294 max_alerts: max_alerts.max(1),
295 }
296 }
297
298 pub fn record(&mut self, sample: ChmHealthSample) -> Vec<ChmHealthAlert> {
305 let peer_id = sample.peer_id.clone();
306 let value = sample.metric.value();
307 let now = sample.timestamp;
308 let metric_clone = sample.metric.clone();
309
310 let conn = self
312 .connections
313 .entry(peer_id.clone())
314 .or_insert_with(|| ConnectionHealth::new(peer_id.clone()));
315
316 let kind = sample.metric.kind();
318 let same_kind_count = conn
319 .samples
320 .iter()
321 .filter(|s| s.metric.kind() == kind)
322 .count();
323 if same_kind_count >= WINDOW_SIZE {
324 if let Some(pos) = conn.samples.iter().position(|s| s.metric.kind() == kind) {
326 conn.samples.remove(pos);
327 }
328 }
329 conn.samples.push_back(sample);
330 conn.last_updated = now;
331
332 let new_score = Self::compute_health_score(conn);
334 conn.health_score = new_score;
335
336 let new_alerts = self.check_thresholds(&peer_id, &metric_clone, value, now);
338
339 if let Some(conn) = self.connections.get_mut(&peer_id) {
341 conn.alert_count = conn.alert_count.saturating_add(new_alerts.len() as u32);
342 }
343 for alert in &new_alerts {
344 self.alerts.push_back(alert.clone());
345 while self.alerts.len() > self.max_alerts {
346 self.alerts.pop_front();
347 }
348 }
349
350 new_alerts
351 }
352
353 pub fn health_score(&self, peer_id: &str) -> Option<f64> {
355 self.connections.get(peer_id).map(|c| c.health_score)
356 }
357
358 pub fn compute_health_score(conn: &ConnectionHealth) -> f64 {
369 let latency_score = ConnectionHealth::mean(&conn.values_for_kind("latency"))
370 .map(|v| (-v / 1_000.0_f64).exp())
371 .unwrap_or(0.5);
372
373 let bandwidth_score = ConnectionHealth::mean(&conn.values_for_kind("bandwidth"))
374 .map(|v| (v / 1_000_000.0).min(1.0))
375 .unwrap_or(0.5);
376
377 let availability_score = ConnectionHealth::mean(&conn.values_for_kind("packet_loss"))
378 .map(|v| 1.0 - v)
379 .unwrap_or(0.5);
380
381 let jitter_score = ConnectionHealth::mean(&conn.values_for_kind("jitter"))
382 .map(|v| (-v / 200.0_f64).exp())
383 .unwrap_or(0.5);
384
385 let reliability_score = ConnectionHealth::mean(&conn.values_for_kind("error_rate"))
386 .map(|v| 1.0 - v)
387 .unwrap_or(0.5);
388
389 let score = 0.30 * latency_score
390 + 0.20 * bandwidth_score
391 + 0.20 * availability_score
392 + 0.15 * jitter_score
393 + 0.15 * reliability_score;
394
395 score.clamp(0.0, 1.0)
396 }
397
398 pub fn check_thresholds(
401 &self,
402 peer_id: &str,
403 metric: &ChmHealthMetric,
404 value: f64,
405 now: u64,
406 ) -> Vec<ChmHealthAlert> {
407 let mut alerts = Vec::new();
408
409 match metric {
410 ChmHealthMetric::Latency(_) => {
411 let critical = self.thresholds.max_latency_ms;
412 let warning = self.thresholds.warning_latency_ms();
413 if value >= critical {
414 alerts.push(self.make_alert(
415 peer_id,
416 metric.clone(),
417 ChmAlertSeverity::Critical,
418 value,
419 critical,
420 now,
421 ));
422 } else if value >= warning {
423 alerts.push(self.make_alert(
424 peer_id,
425 metric.clone(),
426 ChmAlertSeverity::Warning,
427 value,
428 critical,
429 now,
430 ));
431 }
432 }
433 ChmHealthMetric::PacketLoss(_) => {
434 let critical = self.thresholds.max_packet_loss;
435 let warning = self.thresholds.warning_packet_loss();
436 if value >= critical {
437 alerts.push(self.make_alert(
438 peer_id,
439 metric.clone(),
440 ChmAlertSeverity::Critical,
441 value,
442 critical,
443 now,
444 ));
445 } else if value >= warning {
446 alerts.push(self.make_alert(
447 peer_id,
448 metric.clone(),
449 ChmAlertSeverity::Warning,
450 value,
451 critical,
452 now,
453 ));
454 }
455 }
456 ChmHealthMetric::Bandwidth(_) => {
457 let critical = self.thresholds.min_bandwidth_bps;
459 let warning = self.thresholds.warning_bandwidth_bps();
460 if value <= critical {
461 alerts.push(self.make_alert(
462 peer_id,
463 metric.clone(),
464 ChmAlertSeverity::Critical,
465 value,
466 critical,
467 now,
468 ));
469 } else if value <= warning {
470 alerts.push(self.make_alert(
471 peer_id,
472 metric.clone(),
473 ChmAlertSeverity::Warning,
474 value,
475 critical,
476 now,
477 ));
478 }
479 }
480 ChmHealthMetric::Jitter(_) => {
481 let critical = self.thresholds.max_jitter_ms;
482 let warning = self.thresholds.warning_jitter_ms();
483 if value >= critical {
484 alerts.push(self.make_alert(
485 peer_id,
486 metric.clone(),
487 ChmAlertSeverity::Critical,
488 value,
489 critical,
490 now,
491 ));
492 } else if value >= warning {
493 alerts.push(self.make_alert(
494 peer_id,
495 metric.clone(),
496 ChmAlertSeverity::Warning,
497 value,
498 critical,
499 now,
500 ));
501 }
502 }
503 ChmHealthMetric::ErrorRate(_) => {
504 let critical = self.thresholds.max_error_rate;
505 let warning = self.thresholds.warning_error_rate();
506 if value >= critical {
507 alerts.push(self.make_alert(
508 peer_id,
509 metric.clone(),
510 ChmAlertSeverity::Critical,
511 value,
512 critical,
513 now,
514 ));
515 } else if value >= warning {
516 alerts.push(self.make_alert(
517 peer_id,
518 metric.clone(),
519 ChmAlertSeverity::Warning,
520 value,
521 critical,
522 now,
523 ));
524 }
525 }
526 }
527
528 alerts
529 }
530
531 pub fn peers_below_threshold(&self, min_score: f64) -> Vec<&str> {
534 let mut result: Vec<&str> = self
535 .connections
536 .iter()
537 .filter(|(_, c)| c.health_score < min_score)
538 .map(|(id, _)| id.as_str())
539 .collect();
540 result.sort_unstable();
541 result
542 }
543
544 pub fn recent_alerts(&self, since: u64) -> Vec<&ChmHealthAlert> {
546 self.alerts
547 .iter()
548 .filter(|a| a.timestamp >= since)
549 .collect()
550 }
551
552 pub fn clear_peer(&mut self, peer_id: &str) -> bool {
554 self.connections.remove(peer_id).is_some()
555 }
556
557 pub fn evict_stale(&mut self, max_age_ms: u64, now: u64) -> usize {
561 let cutoff = now.saturating_sub(max_age_ms);
562 let before = self.connections.len();
563 self.connections
564 .retain(|_, c| c.last_updated >= cutoff || c.last_updated == 0);
565 before - self.connections.len()
566 }
567
568 pub fn top_peers(&self, n: usize) -> Vec<(&str, f64)> {
570 let mut peers: Vec<(&str, f64)> = self
571 .connections
572 .iter()
573 .map(|(id, c)| (id.as_str(), c.health_score))
574 .collect();
575 peers.sort_by(|a, b| {
577 b.1.partial_cmp(&a.1)
578 .unwrap_or(std::cmp::Ordering::Equal)
579 .then_with(|| a.0.cmp(b.0))
580 });
581 peers.truncate(n);
582 peers
583 }
584
585 pub fn stats(&self) -> ChmMonitorStats {
587 let total_peers = self.connections.len();
588 let mut healthy_peers = 0usize;
589 let mut degraded_peers = 0usize;
590 let mut critical_peers = 0usize;
591 let mut score_sum = 0.0f64;
592
593 for conn in self.connections.values() {
594 let s = conn.health_score;
595 score_sum += s;
596 if s >= 0.8 {
597 healthy_peers += 1;
598 } else if s >= 0.5 {
599 degraded_peers += 1;
600 } else {
601 critical_peers += 1;
602 }
603 }
604
605 let avg_health_score = if total_peers == 0 {
606 0.0
607 } else {
608 score_sum / total_peers as f64
609 };
610
611 ChmMonitorStats {
612 total_peers,
613 healthy_peers,
614 degraded_peers,
615 critical_peers,
616 total_alerts: self.alerts.len(),
617 avg_health_score,
618 }
619 }
620
621 fn make_alert(
626 &self,
627 peer_id: &str,
628 metric: ChmHealthMetric,
629 severity: ChmAlertSeverity,
630 value: f64,
631 threshold: f64,
632 timestamp: u64,
633 ) -> ChmHealthAlert {
634 ChmHealthAlert {
635 peer_id: peer_id.to_string(),
636 metric,
637 severity,
638 value,
639 threshold,
640 timestamp,
641 }
642 }
643}
644
645#[cfg(test)]
650mod tests {
651 use super::{
652 AlertThresholds, ChmAlertSeverity, ChmHealthAlert, ChmHealthMetric, ChmHealthSample,
653 ChmMonitorStats, ConnectionHealth, ConnectionHealthMonitor, WINDOW_SIZE,
654 };
655
656 fn default_monitor() -> ConnectionHealthMonitor {
661 ConnectionHealthMonitor::new(AlertThresholds::default(), 1_000)
662 }
663
664 fn latency_sample(peer_id: &str, ms: f64, ts: u64) -> ChmHealthSample {
665 ChmHealthSample {
666 peer_id: peer_id.to_string(),
667 metric: ChmHealthMetric::Latency(ms),
668 timestamp: ts,
669 }
670 }
671
672 fn bandwidth_sample(peer_id: &str, bps: f64, ts: u64) -> ChmHealthSample {
673 ChmHealthSample {
674 peer_id: peer_id.to_string(),
675 metric: ChmHealthMetric::Bandwidth(bps),
676 timestamp: ts,
677 }
678 }
679
680 fn packet_loss_sample(peer_id: &str, loss: f64, ts: u64) -> ChmHealthSample {
681 ChmHealthSample {
682 peer_id: peer_id.to_string(),
683 metric: ChmHealthMetric::PacketLoss(loss),
684 timestamp: ts,
685 }
686 }
687
688 fn jitter_sample(peer_id: &str, ms: f64, ts: u64) -> ChmHealthSample {
689 ChmHealthSample {
690 peer_id: peer_id.to_string(),
691 metric: ChmHealthMetric::Jitter(ms),
692 timestamp: ts,
693 }
694 }
695
696 fn error_rate_sample(peer_id: &str, rate: f64, ts: u64) -> ChmHealthSample {
697 ChmHealthSample {
698 peer_id: peer_id.to_string(),
699 metric: ChmHealthMetric::ErrorRate(rate),
700 timestamp: ts,
701 }
702 }
703
704 #[test]
709 fn test_record_new_peer_creates_entry() {
710 let mut m = default_monitor();
711 m.record(latency_sample("p1", 50.0, 1000));
712 assert!(m.health_score("p1").is_some());
713 }
714
715 #[test]
716 fn test_health_score_unknown_peer_returns_none() {
717 let m = default_monitor();
718 assert!(m.health_score("nobody").is_none());
719 }
720
721 #[test]
722 fn test_record_returns_empty_alerts_for_good_latency() {
723 let mut m = default_monitor();
724 let alerts = m.record(latency_sample("p1", 50.0, 1000));
725 assert!(alerts.is_empty());
726 }
727
728 #[test]
729 fn test_record_multiple_samples_updates_score() {
730 let mut m = default_monitor();
731 m.record(latency_sample("p1", 100.0, 1000));
732 let score1 = m.health_score("p1").unwrap_or(0.0);
733 m.record(latency_sample("p1", 200.0, 2000));
734 let score2 = m.health_score("p1").unwrap_or(0.0);
735 assert!(score2 < score1 || (score2 - score1).abs() < 1e-9);
737 }
738
739 #[test]
744 fn test_compute_health_score_all_default_missing() {
745 let conn = ConnectionHealth::new("p".to_string());
746 let score = ConnectionHealthMonitor::compute_health_score(&conn);
747 assert!((score - 0.5).abs() < 1e-9, "expected 0.5 got {}", score);
749 }
750
751 #[test]
752 fn test_compute_health_score_perfect_latency() {
753 let mut conn = ConnectionHealth::new("p".to_string());
754 conn.samples.push_back(ChmHealthSample {
756 peer_id: "p".to_string(),
757 metric: ChmHealthMetric::Latency(0.0),
758 timestamp: 1,
759 });
760 let score = ConnectionHealthMonitor::compute_health_score(&conn);
761 let expected = 0.30 * 1.0 + 0.20 * 0.5 + 0.20 * 0.5 + 0.15 * 0.5 + 0.15 * 0.5;
763 assert!((score - expected).abs() < 1e-9);
764 }
765
766 #[test]
767 fn test_compute_health_score_zero_bandwidth() {
768 let mut conn = ConnectionHealth::new("p".to_string());
769 conn.samples.push_back(ChmHealthSample {
770 peer_id: "p".to_string(),
771 metric: ChmHealthMetric::Bandwidth(0.0),
772 timestamp: 1,
773 });
774 let score = ConnectionHealthMonitor::compute_health_score(&conn);
775 let expected = 0.30 * 0.5 + 0.20 * 0.0 + 0.20 * 0.5 + 0.15 * 0.5 + 0.15 * 0.5;
777 assert!((score - expected).abs() < 1e-9);
778 }
779
780 #[test]
781 fn test_compute_health_score_full_packet_loss() {
782 let mut conn = ConnectionHealth::new("p".to_string());
783 conn.samples.push_back(ChmHealthSample {
784 peer_id: "p".to_string(),
785 metric: ChmHealthMetric::PacketLoss(1.0),
786 timestamp: 1,
787 });
788 let score = ConnectionHealthMonitor::compute_health_score(&conn);
789 let expected = 0.30 * 0.5 + 0.20 * 0.5 + 0.20 * 0.0 + 0.15 * 0.5 + 0.15 * 0.5;
791 assert!((score - expected).abs() < 1e-9);
792 }
793
794 #[test]
795 fn test_compute_health_score_all_perfect() {
796 let mut conn = ConnectionHealth::new("p".to_string());
797 conn.samples.push_back(ChmHealthSample {
798 peer_id: "p".to_string(),
799 metric: ChmHealthMetric::Latency(0.0),
800 timestamp: 1,
801 });
802 conn.samples.push_back(ChmHealthSample {
803 peer_id: "p".to_string(),
804 metric: ChmHealthMetric::Bandwidth(1_000_000.0),
805 timestamp: 2,
806 });
807 conn.samples.push_back(ChmHealthSample {
808 peer_id: "p".to_string(),
809 metric: ChmHealthMetric::PacketLoss(0.0),
810 timestamp: 3,
811 });
812 conn.samples.push_back(ChmHealthSample {
813 peer_id: "p".to_string(),
814 metric: ChmHealthMetric::Jitter(0.0),
815 timestamp: 4,
816 });
817 conn.samples.push_back(ChmHealthSample {
818 peer_id: "p".to_string(),
819 metric: ChmHealthMetric::ErrorRate(0.0),
820 timestamp: 5,
821 });
822 let score = ConnectionHealthMonitor::compute_health_score(&conn);
823 assert!((score - 1.0).abs() < 1e-9, "expected 1.0 got {score}");
824 }
825
826 #[test]
827 fn test_health_score_clamped_to_one() {
828 let mut conn = ConnectionHealth::new("p".to_string());
829 conn.samples.push_back(ChmHealthSample {
831 peer_id: "p".to_string(),
832 metric: ChmHealthMetric::Latency(0.0),
833 timestamp: 1,
834 });
835 conn.samples.push_back(ChmHealthSample {
836 peer_id: "p".to_string(),
837 metric: ChmHealthMetric::Bandwidth(10_000_000.0),
838 timestamp: 2,
839 });
840 conn.samples.push_back(ChmHealthSample {
841 peer_id: "p".to_string(),
842 metric: ChmHealthMetric::PacketLoss(0.0),
843 timestamp: 3,
844 });
845 conn.samples.push_back(ChmHealthSample {
846 peer_id: "p".to_string(),
847 metric: ChmHealthMetric::Jitter(0.0),
848 timestamp: 4,
849 });
850 conn.samples.push_back(ChmHealthSample {
851 peer_id: "p".to_string(),
852 metric: ChmHealthMetric::ErrorRate(0.0),
853 timestamp: 5,
854 });
855 let score = ConnectionHealthMonitor::compute_health_score(&conn);
856 assert!(score <= 1.0);
857 }
858
859 #[test]
864 fn test_latency_warning_alert() {
865 let mut m = default_monitor();
866 let alerts = m.record(latency_sample("p1", 400.0, 1000));
868 assert_eq!(alerts.len(), 1);
869 assert_eq!(alerts[0].severity, ChmAlertSeverity::Warning);
870 }
871
872 #[test]
873 fn test_latency_critical_alert() {
874 let mut m = default_monitor();
875 let alerts = m.record(latency_sample("p1", 500.0, 1000));
876 assert_eq!(alerts.len(), 1);
877 assert_eq!(alerts[0].severity, ChmAlertSeverity::Critical);
878 }
879
880 #[test]
881 fn test_latency_above_critical_is_critical() {
882 let mut m = default_monitor();
883 let alerts = m.record(latency_sample("p1", 800.0, 1000));
884 assert_eq!(alerts.len(), 1);
885 assert_eq!(alerts[0].severity, ChmAlertSeverity::Critical);
886 }
887
888 #[test]
889 fn test_latency_below_warning_no_alert() {
890 let mut m = default_monitor();
891 let alerts = m.record(latency_sample("p1", 100.0, 1000));
892 assert!(alerts.is_empty());
893 }
894
895 #[test]
900 fn test_packet_loss_warning_alert() {
901 let mut m = default_monitor();
902 let alerts = m.record(packet_loss_sample("p1", 0.045, 1000));
905 assert_eq!(alerts.len(), 1);
906 assert_eq!(alerts[0].severity, ChmAlertSeverity::Warning);
907 }
908
909 #[test]
910 fn test_packet_loss_critical_alert() {
911 let mut m = default_monitor();
912 let alerts = m.record(packet_loss_sample("p1", 0.05, 1000));
913 assert_eq!(alerts.len(), 1);
914 assert_eq!(alerts[0].severity, ChmAlertSeverity::Critical);
915 }
916
917 #[test]
922 fn test_bandwidth_critical_alert_when_too_low() {
923 let mut m = default_monitor();
924 let alerts = m.record(bandwidth_sample("p1", 50_000.0, 1000));
926 assert_eq!(alerts.len(), 1);
927 assert_eq!(alerts[0].severity, ChmAlertSeverity::Critical);
928 }
929
930 #[test]
931 fn test_bandwidth_warning_alert_between_thresholds() {
932 let mut m = default_monitor();
933 let alerts = m.record(bandwidth_sample("p1", 110_000.0, 1000));
935 assert_eq!(alerts.len(), 1);
936 assert_eq!(alerts[0].severity, ChmAlertSeverity::Warning);
937 }
938
939 #[test]
940 fn test_bandwidth_no_alert_when_high() {
941 let mut m = default_monitor();
942 let alerts = m.record(bandwidth_sample("p1", 1_000_000.0, 1000));
943 assert!(alerts.is_empty());
944 }
945
946 #[test]
951 fn test_jitter_warning_alert() {
952 let mut m = default_monitor();
953 let alerts = m.record(jitter_sample("p1", 80.0, 1000));
955 assert_eq!(alerts.len(), 1);
956 assert_eq!(alerts[0].severity, ChmAlertSeverity::Warning);
957 }
958
959 #[test]
960 fn test_jitter_critical_alert() {
961 let mut m = default_monitor();
962 let alerts = m.record(jitter_sample("p1", 100.0, 1000));
963 assert_eq!(alerts.len(), 1);
964 assert_eq!(alerts[0].severity, ChmAlertSeverity::Critical);
965 }
966
967 #[test]
972 fn test_error_rate_warning_alert() {
973 let mut m = default_monitor();
974 let alerts = m.record(error_rate_sample("p1", 0.09, 1000));
977 assert_eq!(alerts.len(), 1);
978 assert_eq!(alerts[0].severity, ChmAlertSeverity::Warning);
979 }
980
981 #[test]
982 fn test_error_rate_critical_alert() {
983 let mut m = default_monitor();
984 let alerts = m.record(error_rate_sample("p1", 0.1, 1000));
985 assert_eq!(alerts.len(), 1);
986 assert_eq!(alerts[0].severity, ChmAlertSeverity::Critical);
987 }
988
989 #[test]
994 fn test_alerts_stored_internally() {
995 let mut m = default_monitor();
996 m.record(latency_sample("p1", 500.0, 1000));
997 assert_eq!(m.alerts.len(), 1);
998 }
999
1000 #[test]
1001 fn test_max_alerts_eviction() {
1002 let mut m = ConnectionHealthMonitor::new(AlertThresholds::default(), 3);
1003 for i in 0..10u64 {
1004 m.record(latency_sample("p1", 600.0, i * 1000));
1005 }
1006 assert_eq!(m.alerts.len(), 3);
1007 }
1008
1009 #[test]
1010 fn test_recent_alerts_filter_by_timestamp() {
1011 let mut m = default_monitor();
1012 m.record(latency_sample("p1", 600.0, 1_000));
1013 m.record(latency_sample("p1", 600.0, 5_000));
1014 m.record(latency_sample("p1", 600.0, 10_000));
1015 let recent = m.recent_alerts(5_000);
1016 assert_eq!(recent.len(), 2);
1017 }
1018
1019 #[test]
1020 fn test_recent_alerts_returns_all_when_since_zero() {
1021 let mut m = default_monitor();
1022 m.record(latency_sample("p1", 600.0, 1_000));
1023 m.record(latency_sample("p1", 600.0, 2_000));
1024 let recent = m.recent_alerts(0);
1025 assert_eq!(recent.len(), 2);
1026 }
1027
1028 #[test]
1033 fn test_alert_count_increments_per_peer() {
1034 let mut m = default_monitor();
1035 m.record(latency_sample("p1", 600.0, 1_000));
1036 m.record(latency_sample("p1", 600.0, 2_000));
1037 let conn = m.connections.get("p1").expect("peer exists");
1038 assert_eq!(conn.alert_count, 2);
1039 }
1040
1041 #[test]
1046 fn test_peers_below_threshold_sorted() {
1047 let mut m = default_monitor();
1048 m.record(latency_sample("b_peer", 900.0, 1000));
1050 m.record(latency_sample("a_peer", 950.0, 1000));
1051 m.record(latency_sample("c_peer", 10.0, 1000));
1052 let below = m.peers_below_threshold(0.9);
1054 for i in 1..below.len() {
1056 assert!(below[i - 1] <= below[i]);
1057 }
1058 }
1059
1060 #[test]
1061 fn test_peers_below_threshold_none_when_all_healthy() {
1062 let mut m = default_monitor();
1063 m.record(latency_sample("p1", 1.0, 1000));
1064 let below = m.peers_below_threshold(0.0);
1065 assert!(below.is_empty());
1066 }
1067
1068 #[test]
1073 fn test_clear_peer_removes_existing() {
1074 let mut m = default_monitor();
1075 m.record(latency_sample("p1", 50.0, 1000));
1076 assert!(m.clear_peer("p1"));
1077 assert!(m.health_score("p1").is_none());
1078 }
1079
1080 #[test]
1081 fn test_clear_peer_returns_false_for_unknown() {
1082 let mut m = default_monitor();
1083 assert!(!m.clear_peer("ghost"));
1084 }
1085
1086 #[test]
1091 fn test_evict_stale_removes_old_peers() {
1092 let mut m = default_monitor();
1093 m.record(latency_sample("old", 50.0, 1_000));
1094 m.record(latency_sample("new", 50.0, 180_000));
1096 let count = m.evict_stale(50_000, 200_000);
1097 assert_eq!(count, 1);
1098 assert!(m.health_score("old").is_none());
1099 assert!(m.health_score("new").is_some());
1100 }
1101
1102 #[test]
1103 fn test_evict_stale_returns_zero_when_nothing_stale() {
1104 let mut m = default_monitor();
1105 m.record(latency_sample("p1", 50.0, 190_000));
1106 let count = m.evict_stale(50_000, 200_000);
1107 assert_eq!(count, 0);
1108 }
1109
1110 #[test]
1115 fn test_top_peers_returns_n_best() {
1116 let mut m = default_monitor();
1117 m.record(bandwidth_sample("p1", 1_000_000.0, 1000));
1118 m.record(bandwidth_sample("p2", 500_000.0, 1000));
1119 m.record(bandwidth_sample("p3", 10_000.0, 1000));
1120 let top = m.top_peers(2);
1121 assert_eq!(top.len(), 2);
1122 assert!(top[0].1 >= top[1].1);
1124 }
1125
1126 #[test]
1127 fn test_top_peers_handles_n_larger_than_count() {
1128 let mut m = default_monitor();
1129 m.record(latency_sample("p1", 50.0, 1000));
1130 let top = m.top_peers(100);
1131 assert_eq!(top.len(), 1);
1132 }
1133
1134 #[test]
1135 fn test_top_peers_empty_monitor() {
1136 let m = default_monitor();
1137 assert!(m.top_peers(5).is_empty());
1138 }
1139
1140 #[test]
1145 fn test_stats_default_empty() {
1146 let m = default_monitor();
1147 let s = m.stats();
1148 assert_eq!(s.total_peers, 0);
1149 assert_eq!(s.avg_health_score, 0.0);
1150 }
1151
1152 #[test]
1153 fn test_stats_counts_healthy_degraded_critical() {
1154 let mut m = default_monitor();
1155 m.record(latency_sample("healthy", 1.0, 1));
1157 m.record(bandwidth_sample("healthy", 1_000_000.0, 2));
1158 m.record(packet_loss_sample("healthy", 0.0, 3));
1159 m.record(jitter_sample("healthy", 0.0, 4));
1160 m.record(error_rate_sample("healthy", 0.0, 5));
1161
1162 m.record(latency_sample("bad", 10_000.0, 1));
1164 m.record(bandwidth_sample("bad", 1.0, 2));
1165 m.record(packet_loss_sample("bad", 0.9, 3));
1166 m.record(jitter_sample("bad", 5_000.0, 4));
1167 m.record(error_rate_sample("bad", 0.9, 5));
1168
1169 let s = m.stats();
1170 assert_eq!(s.total_peers, 2);
1171 assert_eq!(s.healthy_peers + s.degraded_peers + s.critical_peers, 2);
1172 }
1173
1174 #[test]
1175 fn test_stats_avg_health_score() {
1176 let mut m = default_monitor();
1177 m.record(latency_sample("p1", 1.0, 1));
1179 m.record(latency_sample("p2", 1.0, 1));
1180 let s = m.stats();
1181 let score_p1 = m.health_score("p1").unwrap_or(0.0);
1182 let score_p2 = m.health_score("p2").unwrap_or(0.0);
1183 let expected_avg = (score_p1 + score_p2) / 2.0;
1184 assert!((s.avg_health_score - expected_avg).abs() < 1e-9);
1185 }
1186
1187 #[test]
1192 fn test_window_evicts_oldest_same_metric() {
1193 let mut m = default_monitor();
1194 for i in 0..WINDOW_SIZE {
1196 m.record(latency_sample("p1", 10.0, i as u64 * 100));
1197 }
1198 let conn = m.connections.get("p1").expect("exists");
1199 let latency_count = conn
1200 .samples
1201 .iter()
1202 .filter(|s| s.metric.kind() == "latency")
1203 .count();
1204 assert_eq!(latency_count, WINDOW_SIZE);
1205
1206 m.record(latency_sample("p1", 10.0, WINDOW_SIZE as u64 * 100));
1208 let conn = m.connections.get("p1").expect("exists");
1209 let latency_count_after = conn
1210 .samples
1211 .iter()
1212 .filter(|s| s.metric.kind() == "latency")
1213 .count();
1214 assert_eq!(latency_count_after, WINDOW_SIZE);
1215 }
1216
1217 #[test]
1218 fn test_window_different_metrics_independent() {
1219 let mut m = default_monitor();
1220 for i in 0..WINDOW_SIZE {
1222 m.record(latency_sample("p1", 10.0, i as u64 * 100));
1223 m.record(bandwidth_sample("p1", 1_000_000.0, i as u64 * 100 + 1));
1224 }
1225 let conn = m.connections.get("p1").expect("exists");
1226 let latency_count = conn
1227 .samples
1228 .iter()
1229 .filter(|s| s.metric.kind() == "latency")
1230 .count();
1231 let bandwidth_count = conn
1232 .samples
1233 .iter()
1234 .filter(|s| s.metric.kind() == "bandwidth")
1235 .count();
1236 assert_eq!(latency_count, WINDOW_SIZE);
1237 assert_eq!(bandwidth_count, WINDOW_SIZE);
1238 }
1239
1240 #[test]
1245 fn test_health_metric_value() {
1246 assert_eq!(ChmHealthMetric::Latency(42.0).value(), 42.0);
1247 assert_eq!(ChmHealthMetric::PacketLoss(0.03).value(), 0.03);
1248 assert_eq!(ChmHealthMetric::Bandwidth(1e6).value(), 1e6);
1249 assert_eq!(ChmHealthMetric::Jitter(15.0).value(), 15.0);
1250 assert_eq!(ChmHealthMetric::ErrorRate(0.07).value(), 0.07);
1251 }
1252
1253 #[test]
1254 fn test_health_metric_kind() {
1255 assert_eq!(ChmHealthMetric::Latency(0.0).kind(), "latency");
1256 assert_eq!(ChmHealthMetric::PacketLoss(0.0).kind(), "packet_loss");
1257 assert_eq!(ChmHealthMetric::Bandwidth(0.0).kind(), "bandwidth");
1258 assert_eq!(ChmHealthMetric::Jitter(0.0).kind(), "jitter");
1259 assert_eq!(ChmHealthMetric::ErrorRate(0.0).kind(), "error_rate");
1260 }
1261
1262 #[test]
1267 fn test_alert_thresholds_defaults() {
1268 let t = AlertThresholds::default();
1269 assert_eq!(t.max_latency_ms, 500.0);
1270 assert_eq!(t.max_packet_loss, 0.05);
1271 assert_eq!(t.min_bandwidth_bps, 100_000.0);
1272 assert_eq!(t.max_jitter_ms, 100.0);
1273 assert_eq!(t.max_error_rate, 0.1);
1274 }
1275
1276 #[test]
1277 fn test_alert_thresholds_warning_levels() {
1278 let t = AlertThresholds::default();
1279 assert!((t.warning_latency_ms() - 400.0).abs() < 1e-9);
1280 assert!((t.warning_packet_loss() - 0.04).abs() < 1e-9);
1281 assert!((t.warning_jitter_ms() - 80.0).abs() < 1e-9);
1282 assert!((t.warning_error_rate() - 0.08).abs() < 1e-9);
1283 }
1284
1285 #[test]
1290 fn test_check_thresholds_exact_boundary() {
1291 let m = default_monitor();
1292 let alerts = m.check_thresholds("p1", &ChmHealthMetric::Latency(400.0), 400.0, 1000);
1294 assert_eq!(alerts.len(), 1);
1295 assert_eq!(alerts[0].severity, ChmAlertSeverity::Warning);
1296 }
1297
1298 #[test]
1299 fn test_check_thresholds_returns_threshold_value() {
1300 let m = default_monitor();
1301 let alerts = m.check_thresholds("p1", &ChmHealthMetric::Latency(600.0), 600.0, 1000);
1302 assert_eq!(alerts[0].threshold, 500.0);
1303 assert_eq!(alerts[0].value, 600.0);
1304 }
1305
1306 #[test]
1311 fn test_peers_do_not_interfere() {
1312 let mut m = default_monitor();
1313 m.record(latency_sample("p1", 10.0, 1000));
1314 m.record(latency_sample("p2", 900.0, 1000));
1315 let s1 = m.health_score("p1").unwrap_or(0.0);
1316 let s2 = m.health_score("p2").unwrap_or(0.0);
1317 assert!(s1 > s2);
1318 }
1319
1320 #[test]
1325 fn test_last_updated_reflects_sample_timestamp() {
1326 let mut m = default_monitor();
1327 m.record(latency_sample("p1", 50.0, 42_000));
1328 let conn = m.connections.get("p1").expect("exists");
1329 assert_eq!(conn.last_updated, 42_000);
1330 }
1331
1332 #[test]
1337 fn test_custom_thresholds_stricter_latency() {
1338 let thresholds = AlertThresholds {
1339 max_latency_ms: 100.0,
1340 ..Default::default()
1341 };
1342 let mut m = ConnectionHealthMonitor::new(thresholds, 100);
1343 let alerts = m.record(latency_sample("p1", 80.0, 1000));
1345 assert_eq!(alerts.len(), 1);
1346 assert_eq!(alerts[0].severity, ChmAlertSeverity::Warning);
1347 }
1348
1349 #[test]
1350 fn test_custom_thresholds_relaxed_packet_loss() {
1351 let thresholds = AlertThresholds {
1352 max_packet_loss: 0.5,
1353 ..Default::default()
1354 };
1355 let mut m = ConnectionHealthMonitor::new(thresholds, 100);
1356 let alerts = m.record(packet_loss_sample("p1", 0.05, 1000));
1358 assert!(alerts.is_empty());
1359 }
1360
1361 #[test]
1366 fn test_bandwidth_score_capped_at_one() {
1367 let mut conn = ConnectionHealth::new("p".to_string());
1368 conn.samples.push_back(ChmHealthSample {
1370 peer_id: "p".to_string(),
1371 metric: ChmHealthMetric::Bandwidth(100_000_000.0),
1372 timestamp: 1,
1373 });
1374 let score = ConnectionHealthMonitor::compute_health_score(&conn);
1375 assert!(score <= 1.0);
1376 }
1377
1378 #[test]
1383 fn test_mixed_metrics_all_recorded() {
1384 let mut m = default_monitor();
1385 m.record(latency_sample("p1", 10.0, 1));
1386 m.record(bandwidth_sample("p1", 1_000_000.0, 2));
1387 m.record(packet_loss_sample("p1", 0.0, 3));
1388 m.record(jitter_sample("p1", 5.0, 4));
1389 m.record(error_rate_sample("p1", 0.0, 5));
1390 let conn = m.connections.get("p1").expect("exists");
1391 assert_eq!(conn.samples.len(), 5);
1392 }
1393
1394 #[test]
1395 fn test_mixed_metrics_health_score_near_one() {
1396 let mut m = default_monitor();
1397 m.record(latency_sample("p1", 0.0, 1));
1398 m.record(bandwidth_sample("p1", 1_000_000.0, 2));
1399 m.record(packet_loss_sample("p1", 0.0, 3));
1400 m.record(jitter_sample("p1", 0.0, 4));
1401 m.record(error_rate_sample("p1", 0.0, 5));
1402 let score = m.health_score("p1").unwrap_or(0.0);
1403 assert!(score > 0.95, "score={score}");
1404 }
1405
1406 #[test]
1411 fn test_monitor_stats_fields_accessible() {
1412 let s = ChmMonitorStats::default();
1413 assert_eq!(s.total_peers, 0);
1414 assert_eq!(s.healthy_peers, 0);
1415 assert_eq!(s.degraded_peers, 0);
1416 assert_eq!(s.critical_peers, 0);
1417 assert_eq!(s.total_alerts, 0);
1418 assert_eq!(s.avg_health_score, 0.0);
1419 }
1420
1421 #[test]
1426 fn test_health_alert_fields() {
1427 let alert = ChmHealthAlert {
1428 peer_id: "test".to_string(),
1429 metric: ChmHealthMetric::Latency(600.0),
1430 severity: ChmAlertSeverity::Critical,
1431 value: 600.0,
1432 threshold: 500.0,
1433 timestamp: 99_000,
1434 };
1435 assert_eq!(alert.peer_id, "test");
1436 assert_eq!(alert.value, 600.0);
1437 assert_eq!(alert.threshold, 500.0);
1438 assert_eq!(alert.timestamp, 99_000);
1439 }
1440}