1use std::cmp::Reverse;
16use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
17
18#[derive(Debug, Clone)]
22pub struct TopoNode {
23 pub node_id: String,
25 pub address: String,
27 pub discovered_at: u64,
29 pub hop_distance: u32,
31}
32
33#[derive(Debug, Clone)]
35pub struct TopoEdge {
36 pub from: String,
38 pub to: String,
40 pub latency_ms: f64,
42 pub bandwidth_bps: u64,
44 pub discovered_at: u64,
46}
47
48#[derive(Debug, Clone)]
50pub struct TopologySnapshot {
51 pub nodes: Vec<TopoNode>,
53 pub edges: Vec<TopoEdge>,
55 pub snapshot_at: u64,
57 pub local_node_id: String,
59}
60
61#[derive(Debug, Clone, PartialEq)]
63pub struct PathResult {
64 pub path: Vec<String>,
66 pub total_latency_ms: f64,
68 pub hop_count: usize,
70}
71
72#[derive(Debug, Clone, PartialEq)]
74pub struct TopologyStats {
75 pub node_count: usize,
77 pub edge_count: usize,
79 pub avg_latency_ms: f64,
81 pub max_latency_ms: f64,
83 pub avg_out_degree: f64,
85 pub component_count: usize,
87 pub diameter: Option<f64>,
89}
90
91pub type PeerEdge = TopoEdge;
97
98pub type TopologyNode = TopoNode;
102
103#[derive(Debug, Clone)]
130pub struct NetworkTopologyMapper {
131 pub local_id: String,
133 pub nodes: HashMap<String, TopoNode>,
135 pub edges: Vec<TopoEdge>,
138 pub adjacency: HashMap<String, Vec<String>>,
140}
141
142impl NetworkTopologyMapper {
143 pub fn new(local_id: String) -> Self {
147 Self {
148 local_id,
149 nodes: HashMap::new(),
150 edges: Vec::new(),
151 adjacency: HashMap::new(),
152 }
153 }
154
155 pub fn add_node(&mut self, node: TopoNode) {
160 let id = node.node_id.clone();
161 self.nodes.insert(id.clone(), node);
162 self.adjacency.entry(id).or_default();
164 self.rebuild_adjacency_for_edges();
166 }
167
168 pub fn remove_node(&mut self, node_id: &str) -> bool {
172 if self.nodes.remove(node_id).is_none() {
173 return false;
174 }
175 self.edges.retain(|e| e.from != node_id && e.to != node_id);
176 self.adjacency.remove(node_id);
177 for neighbours in self.adjacency.values_mut() {
179 neighbours.retain(|n| n != node_id);
180 }
181 true
182 }
183
184 pub fn add_edge(&mut self, edge: TopoEdge) {
191 let from = edge.from.clone();
192 let to = edge.to.clone();
193 self.edges.push(edge);
194 let neighbours = self.adjacency.entry(from).or_default();
196 if !neighbours.contains(&to) {
197 neighbours.push(to.clone());
198 }
199 self.adjacency.entry(to).or_default();
201 }
202
203 pub fn remove_edge(&mut self, from: &str, to: &str) -> bool {
207 let before = self.edges.len();
208 self.edges.retain(|e| !(e.from == from && e.to == to));
209 let removed = self.edges.len() < before;
210 if removed {
211 if let Some(nbrs) = self.adjacency.get_mut(from) {
213 let still_connected = self.edges.iter().any(|e| e.from == from && e.to == to);
215 if !still_connected {
216 nbrs.retain(|n| n != to);
217 }
218 }
219 }
220 removed
221 }
222
223 pub fn shortest_path(&self, from: &str, to: &str) -> Option<PathResult> {
229 if from == to {
230 return None;
231 }
232
233 let min_lat = self.min_latency_map();
235
236 let mut dist: HashMap<&str, f64> = HashMap::new();
238 let mut prev: HashMap<&str, &str> = HashMap::new();
239 let mut heap: BinaryHeap<(Reverse<u64>, &str)> = BinaryHeap::new();
241
242 dist.insert(from, 0.0);
243 heap.push((Reverse(0), from));
244
245 while let Some((Reverse(cost_bits), node)) = heap.pop() {
246 let cost = f64::from_bits(cost_bits);
247
248 if node == to {
249 let mut path: Vec<String> = Vec::new();
251 let mut cur = to;
252 loop {
253 path.push(cur.to_string());
254 if let Some(&p) = prev.get(cur) {
255 cur = p;
256 } else {
257 break;
258 }
259 }
260 path.reverse();
261 let hop_count = path.len().saturating_sub(1);
262 return Some(PathResult {
263 path,
264 total_latency_ms: cost,
265 hop_count,
266 });
267 }
268
269 if let Some(&best) = dist.get(node) {
271 if cost > best {
272 continue;
273 }
274 }
275
276 if let Some(neighbours) = self.adjacency.get(node) {
278 for nb in neighbours {
279 let edge_lat = min_lat
280 .get(&(node, nb.as_str()))
281 .copied()
282 .unwrap_or(f64::INFINITY);
283 let new_cost = cost + edge_lat;
284 let better = dist.get(nb.as_str()).map(|&d| new_cost < d).unwrap_or(true);
285 if better {
286 dist.insert(nb.as_str(), new_cost);
287 prev.insert(nb.as_str(), node);
288 heap.push((Reverse(new_cost.to_bits()), nb.as_str()));
289 }
290 }
291 }
292 }
293
294 None
295 }
296
297 pub fn hop_shortest_path(&self, from: &str, to: &str) -> Option<PathResult> {
301 if from == to {
302 return None;
303 }
304
305 let mut visited: HashSet<&str> = HashSet::new();
306 let mut queue: VecDeque<(&str, Vec<&str>)> = VecDeque::new();
307
308 visited.insert(from);
309 queue.push_back((from, vec![from]));
310
311 while let Some((node, path)) = queue.pop_front() {
312 if let Some(neighbours) = self.adjacency.get(node) {
313 for nb in neighbours {
314 let nb_str = nb.as_str();
315 if nb_str == to {
316 let mut full: Vec<String> = path.iter().map(|s| s.to_string()).collect();
317 full.push(to.to_string());
318 let hop_count = full.len().saturating_sub(1);
319 let min_lat = self.min_latency_map();
321 let total = full
322 .windows(2)
323 .map(|w| {
324 min_lat
325 .get(&(w[0].as_str(), w[1].as_str()))
326 .copied()
327 .unwrap_or(0.0)
328 })
329 .sum();
330 return Some(PathResult {
331 path: full,
332 total_latency_ms: total,
333 hop_count,
334 });
335 }
336 if !visited.contains(nb_str) {
337 visited.insert(nb_str);
338 let mut new_path = path.clone();
339 new_path.push(nb_str);
340 queue.push_back((nb_str, new_path));
341 }
342 }
343 }
344 }
345
346 None
347 }
348
349 pub fn neighbors(&self, node_id: &str) -> Vec<&str> {
353 self.adjacency
354 .get(node_id)
355 .map(|v| v.iter().map(String::as_str).collect())
356 .unwrap_or_default()
357 }
358
359 pub fn in_degree(&self, node_id: &str) -> usize {
361 self.edges.iter().filter(|e| e.to == node_id).count()
362 }
363
364 pub fn out_degree(&self, node_id: &str) -> usize {
366 self.edges.iter().filter(|e| e.from == node_id).count()
367 }
368
369 pub fn is_reachable(&self, from: &str, to: &str) -> bool {
371 if from == to {
372 return true;
373 }
374 let mut visited: HashSet<&str> = HashSet::new();
375 let mut queue: VecDeque<&str> = VecDeque::new();
376 visited.insert(from);
377 queue.push_back(from);
378 while let Some(node) = queue.pop_front() {
379 if let Some(nbrs) = self.adjacency.get(node) {
380 for nb in nbrs {
381 let nb_str = nb.as_str();
382 if nb_str == to {
383 return true;
384 }
385 if visited.insert(nb_str) {
386 queue.push_back(nb_str);
387 }
388 }
389 }
390 }
391 false
392 }
393
394 pub fn connected_components(&self) -> Vec<Vec<String>> {
399 let mut undirected: HashMap<&str, HashSet<&str>> = HashMap::new();
401
402 for id in self.nodes.keys() {
404 undirected.entry(id.as_str()).or_default();
405 }
406 for id in self.adjacency.keys() {
407 undirected.entry(id.as_str()).or_default();
408 }
409
410 for edge in &self.edges {
411 undirected
412 .entry(edge.from.as_str())
413 .or_default()
414 .insert(edge.to.as_str());
415 undirected
416 .entry(edge.to.as_str())
417 .or_default()
418 .insert(edge.from.as_str());
419 }
420
421 let mut visited: HashSet<&str> = HashSet::new();
422 let mut components: Vec<Vec<String>> = Vec::new();
423
424 for &start in undirected.keys() {
425 if visited.contains(start) {
426 continue;
427 }
428 let mut component: Vec<String> = Vec::new();
430 let mut queue: VecDeque<&str> = VecDeque::new();
431 visited.insert(start);
432 queue.push_back(start);
433 while let Some(node) = queue.pop_front() {
434 component.push(node.to_string());
435 if let Some(nbrs) = undirected.get(node) {
436 for &nb in nbrs {
437 if visited.insert(nb) {
438 queue.push_back(nb);
439 }
440 }
441 }
442 }
443 component.sort();
444 components.push(component);
445 }
446
447 components.sort_by(|a, b| {
448 a.first()
449 .unwrap_or(&String::new())
450 .cmp(b.first().unwrap_or(&String::new()))
451 });
452 components
453 }
454
455 pub fn diameter(&self) -> Option<f64> {
460 let ids: Vec<&str> = self
461 .nodes
462 .keys()
463 .map(String::as_str)
464 .chain(self.adjacency.keys().map(String::as_str))
465 .collect::<HashSet<_>>()
466 .into_iter()
467 .collect();
468
469 if ids.len() < 2 {
470 return None;
471 }
472
473 let mut max_lat: f64 = 0.0;
474 let mut found_any = false;
475
476 for &src in &ids {
477 for &dst in &ids {
478 if src == dst {
479 continue;
480 }
481 if let Some(res) = self.shortest_path(src, dst) {
482 found_any = true;
483 if res.total_latency_ms > max_lat {
484 max_lat = res.total_latency_ms;
485 }
486 }
487 }
488 }
489
490 if found_any {
491 Some(max_lat)
492 } else {
493 None
494 }
495 }
496
497 pub fn average_clustering_coefficient(&self) -> f64 {
509 let mut total_cc = 0.0_f64;
510 let mut count = 0_usize;
511
512 let undirected_nbrs = self.undirected_neighbour_sets();
514 let edge_set: HashSet<(&str, &str)> = self
516 .edges
517 .iter()
518 .map(|e| (e.from.as_str(), e.to.as_str()))
519 .collect();
520
521 for (node, nbrs) in &undirected_nbrs {
522 let k = nbrs.len();
523 if k < 2 {
524 continue;
525 }
526 let nbr_vec: Vec<&str> = nbrs.iter().copied().collect();
527 let mut triangles = 0_usize;
528 for i in 0..nbr_vec.len() {
529 for j in (i + 1)..nbr_vec.len() {
530 let u = nbr_vec[i];
531 let v = nbr_vec[j];
532 if edge_set.contains(&(u, v)) || edge_set.contains(&(v, u)) {
534 triangles += 1;
535 }
536 }
537 }
538 let possible = (k * (k - 1)) / 2;
539 let cc = triangles as f64 / possible as f64;
540 total_cc += cc;
541 count += 1;
542 let _ = node; }
544
545 if count == 0 {
546 0.0
547 } else {
548 total_cc / count as f64
549 }
550 }
551
552 pub fn snapshot(&self) -> TopologySnapshot {
556 use std::time::{SystemTime, UNIX_EPOCH};
557 let now = SystemTime::now()
558 .duration_since(UNIX_EPOCH)
559 .map(|d| d.as_millis() as u64)
560 .unwrap_or(0);
561
562 TopologySnapshot {
563 nodes: self.nodes.values().cloned().collect(),
564 edges: self.edges.clone(),
565 snapshot_at: now,
566 local_node_id: self.local_id.clone(),
567 }
568 }
569
570 pub fn node_count(&self) -> usize {
572 self.nodes.len()
573 }
574
575 pub fn edge_count(&self) -> usize {
577 self.edges.len()
578 }
579
580 pub fn evict_stale(&mut self, max_age_ms: u64, now: u64) -> usize {
585 let stale_ids: Vec<String> = self
586 .nodes
587 .iter()
588 .filter(|(_, n)| now.saturating_sub(n.discovered_at) > max_age_ms)
589 .map(|(id, _)| id.clone())
590 .collect();
591
592 let removed = stale_ids.len();
593 for id in &stale_ids {
594 self.nodes.remove(id);
595 self.edges.retain(|e| &e.from != id && &e.to != id);
596 self.adjacency.remove(id);
597 for nbrs in self.adjacency.values_mut() {
598 nbrs.retain(|n| n != id);
599 }
600 }
601 removed
602 }
603
604 pub fn stats(&self) -> TopologyStats {
606 let node_count = self.node_count();
607 let edge_count = self.edge_count();
608
609 let (avg_latency_ms, max_latency_ms) = if edge_count == 0 {
610 (0.0, 0.0)
611 } else {
612 let sum: f64 = self.edges.iter().map(|e| e.latency_ms).sum();
613 let max = self
614 .edges
615 .iter()
616 .map(|e| e.latency_ms)
617 .fold(0.0_f64, f64::max);
618 (sum / edge_count as f64, max)
619 };
620
621 let avg_out_degree = if node_count == 0 {
622 0.0
623 } else {
624 edge_count as f64 / node_count as f64
625 };
626
627 let component_count = self.connected_components().len();
628 let diameter = self.diameter();
629
630 TopologyStats {
631 node_count,
632 edge_count,
633 avg_latency_ms,
634 max_latency_ms,
635 avg_out_degree,
636 component_count,
637 diameter,
638 }
639 }
640
641 fn min_latency_map(&self) -> HashMap<(&str, &str), f64> {
645 let mut map: HashMap<(&str, &str), f64> = HashMap::new();
646 for edge in &self.edges {
647 let key = (edge.from.as_str(), edge.to.as_str());
648 let entry = map.entry(key).or_insert(f64::INFINITY);
649 if edge.latency_ms < *entry {
650 *entry = edge.latency_ms;
651 }
652 }
653 map
654 }
655
656 fn rebuild_adjacency_for_edges(&mut self) {
658 for id in self.nodes.keys() {
661 self.adjacency.entry(id.clone()).or_default();
662 }
663 for edge in &self.edges {
664 let from = edge.from.clone();
665 let to = edge.to.clone();
666 let nbrs = self.adjacency.entry(from).or_default();
667 if !nbrs.contains(&to) {
668 nbrs.push(to);
669 }
670 self.adjacency.entry(edge.to.clone()).or_default();
671 }
672 }
673
674 fn undirected_neighbour_sets(&self) -> HashMap<&str, HashSet<&str>> {
676 let mut map: HashMap<&str, HashSet<&str>> = HashMap::new();
677 for id in self.nodes.keys() {
678 map.entry(id.as_str()).or_default();
679 }
680 for edge in &self.edges {
681 map.entry(edge.from.as_str())
682 .or_default()
683 .insert(edge.to.as_str());
684 map.entry(edge.to.as_str())
685 .or_default()
686 .insert(edge.from.as_str());
687 }
688 map
689 }
690}
691
692#[cfg(test)]
695mod tests {
696 use crate::topology_mapper::{
697 NetworkTopologyMapper, PathResult, TopoEdge, TopoNode, TopologyStats,
698 };
699
700 fn node(id: &str, hop: u32) -> TopoNode {
703 TopoNode {
704 node_id: id.to_string(),
705 address: format!("/ip4/127.0.0.1/tcp/{}", 4000 + hop),
706 discovered_at: 1_000,
707 hop_distance: hop,
708 }
709 }
710
711 fn edge(from: &str, to: &str, lat: f64) -> TopoEdge {
712 TopoEdge {
713 from: from.to_string(),
714 to: to.to_string(),
715 latency_ms: lat,
716 bandwidth_bps: 1_000_000,
717 discovered_at: 1_000,
718 }
719 }
720
721 fn edge_ts(from: &str, to: &str, lat: f64, ts: u64) -> TopoEdge {
722 TopoEdge {
723 from: from.to_string(),
724 to: to.to_string(),
725 latency_ms: lat,
726 bandwidth_bps: 1_000_000,
727 discovered_at: ts,
728 }
729 }
730
731 fn linear_mapper() -> NetworkTopologyMapper {
733 let mut m = NetworkTopologyMapper::new("local".to_string());
734 for (id, hop) in [("local", 0), ("A", 1), ("B", 2), ("C", 3)] {
735 m.add_node(node(id, hop));
736 }
737 m.add_edge(edge("local", "A", 10.0));
738 m.add_edge(edge("A", "B", 20.0));
739 m.add_edge(edge("B", "C", 30.0));
740 m
741 }
742
743 #[test]
746 fn test_new_empty() {
747 let m = NetworkTopologyMapper::new("local".to_string());
748 assert_eq!(m.local_id, "local");
749 assert!(m.nodes.is_empty());
750 assert!(m.edges.is_empty());
751 assert!(m.adjacency.is_empty());
752 }
753
754 #[test]
757 fn test_add_node_creates_entry() {
758 let mut m = NetworkTopologyMapper::new("local".to_string());
759 m.add_node(node("A", 1));
760 assert!(m.nodes.contains_key("A"));
761 assert!(m.adjacency.contains_key("A"));
762 }
763
764 #[test]
765 fn test_add_node_updates_existing() {
766 let mut m = NetworkTopologyMapper::new("local".to_string());
767 m.add_node(node("A", 1));
768 m.add_node(TopoNode {
769 node_id: "A".to_string(),
770 address: "/ip4/1.2.3.4/tcp/9999".to_string(),
771 discovered_at: 9999,
772 hop_distance: 3,
773 });
774 let n = m.nodes.get("A").expect("A should exist");
775 assert_eq!(n.address, "/ip4/1.2.3.4/tcp/9999");
776 assert_eq!(n.hop_distance, 3);
777 }
778
779 #[test]
782 fn test_remove_node_returns_true_when_exists() {
783 let mut m = NetworkTopologyMapper::new("local".to_string());
784 m.add_node(node("A", 1));
785 assert!(m.remove_node("A"));
786 }
787
788 #[test]
789 fn test_remove_node_returns_false_when_missing() {
790 let mut m = NetworkTopologyMapper::new("local".to_string());
791 assert!(!m.remove_node("Z"));
792 }
793
794 #[test]
795 fn test_remove_node_cleans_edges() {
796 let mut m = linear_mapper();
797 m.remove_node("A");
798 assert!(!m.edges.iter().any(|e| e.from == "A" || e.to == "A"));
799 }
800
801 #[test]
802 fn test_remove_node_cleans_adjacency() {
803 let mut m = linear_mapper();
804 m.remove_node("A");
805 assert!(!m.adjacency.contains_key("A"));
806 let local_nbrs = m.adjacency.get("local").cloned().unwrap_or_default();
808 assert!(!local_nbrs.contains(&"A".to_string()));
809 }
810
811 #[test]
814 fn test_add_edge_updates_adjacency() {
815 let mut m = NetworkTopologyMapper::new("local".to_string());
816 m.add_edge(edge("X", "Y", 5.0));
817 let nbrs = m.adjacency.get("X").expect("X should have adjacency");
818 assert!(nbrs.contains(&"Y".to_string()));
819 }
820
821 #[test]
822 fn test_add_edge_no_duplicate_adjacency() {
823 let mut m = NetworkTopologyMapper::new("local".to_string());
824 m.add_edge(edge("X", "Y", 5.0));
825 m.add_edge(edge("X", "Y", 10.0));
826 let nbrs = m.adjacency.get("X").expect("X should exist");
827 assert_eq!(nbrs.iter().filter(|n| n.as_str() == "Y").count(), 1);
828 }
829
830 #[test]
833 fn test_remove_edge_returns_true() {
834 let mut m = linear_mapper();
835 assert!(m.remove_edge("local", "A"));
836 }
837
838 #[test]
839 fn test_remove_edge_returns_false_when_missing() {
840 let mut m = linear_mapper();
841 assert!(!m.remove_edge("local", "C"));
842 }
843
844 #[test]
845 fn test_remove_edge_removes_from_edge_list() {
846 let mut m = linear_mapper();
847 m.remove_edge("local", "A");
848 assert!(!m.edges.iter().any(|e| e.from == "local" && e.to == "A"));
849 }
850
851 #[test]
854 fn test_shortest_path_direct() {
855 let m = linear_mapper();
856 let r = m.shortest_path("local", "A").expect("path should exist");
857 assert_eq!(r.path, vec!["local", "A"]);
858 assert_eq!(r.hop_count, 1);
859 assert!((r.total_latency_ms - 10.0).abs() < 1e-9);
860 }
861
862 #[test]
863 fn test_shortest_path_multi_hop() {
864 let m = linear_mapper();
865 let r = m.shortest_path("local", "C").expect("path should exist");
866 assert_eq!(r.path, vec!["local", "A", "B", "C"]);
867 assert_eq!(r.hop_count, 3);
868 assert!((r.total_latency_ms - 60.0).abs() < 1e-9);
869 }
870
871 #[test]
872 fn test_shortest_path_prefers_low_latency() {
873 let mut m = NetworkTopologyMapper::new("s".to_string());
874 for id in ["s", "fast", "slow", "t"] {
875 m.add_node(node(id, 0));
876 }
877 m.add_edge(edge("s", "fast", 2.0));
879 m.add_edge(edge("fast", "t", 3.0));
880 m.add_edge(edge("s", "slow", 500.0));
881 m.add_edge(edge("slow", "t", 500.0));
882 let r = m.shortest_path("s", "t").expect("path should exist");
883 assert_eq!(r.path, vec!["s", "fast", "t"]);
884 assert!((r.total_latency_ms - 5.0).abs() < 1e-9);
885 }
886
887 #[test]
888 fn test_shortest_path_same_node_returns_none() {
889 let m = linear_mapper();
890 assert!(m.shortest_path("local", "local").is_none());
891 }
892
893 #[test]
894 fn test_shortest_path_disconnected_returns_none() {
895 let mut m = linear_mapper();
896 m.add_node(node("island", 99));
897 assert!(m.shortest_path("local", "island").is_none());
898 }
899
900 #[test]
903 fn test_hop_shortest_path_direct() {
904 let m = linear_mapper();
905 let r = m
906 .hop_shortest_path("local", "A")
907 .expect("path should exist");
908 assert_eq!(r.hop_count, 1);
909 assert_eq!(r.path, vec!["local", "A"]);
910 }
911
912 #[test]
913 fn test_hop_shortest_path_prefers_fewer_hops() {
914 let mut m = NetworkTopologyMapper::new("s".to_string());
915 for id in ["s", "x", "y", "z", "t"] {
916 m.add_node(node(id, 0));
917 }
918 m.add_edge(edge("s", "t", 9999.0));
920 m.add_edge(edge("s", "x", 1.0));
921 m.add_edge(edge("x", "y", 1.0));
922 m.add_edge(edge("y", "z", 1.0));
923 m.add_edge(edge("z", "t", 1.0));
924 let r = m.hop_shortest_path("s", "t").expect("path should exist");
925 assert_eq!(r.hop_count, 1);
926 assert_eq!(r.path, vec!["s", "t"]);
927 }
928
929 #[test]
930 fn test_hop_shortest_path_same_node_none() {
931 let m = linear_mapper();
932 assert!(m.hop_shortest_path("A", "A").is_none());
933 }
934
935 #[test]
938 fn test_neighbors_correct() {
939 let m = linear_mapper();
940 let mut nbrs = m.neighbors("local");
941 nbrs.sort_unstable();
942 assert_eq!(nbrs, vec!["A"]);
943 }
944
945 #[test]
946 fn test_neighbors_unknown_node_empty() {
947 let m = linear_mapper();
948 assert!(m.neighbors("zzz").is_empty());
949 }
950
951 #[test]
954 fn test_in_degree() {
955 let m = linear_mapper();
956 assert_eq!(m.in_degree("A"), 1);
958 assert_eq!(m.in_degree("C"), 1);
960 assert_eq!(m.in_degree("local"), 0);
962 }
963
964 #[test]
965 fn test_out_degree() {
966 let m = linear_mapper();
967 assert_eq!(m.out_degree("local"), 1);
968 assert_eq!(m.out_degree("A"), 1);
969 assert_eq!(m.out_degree("C"), 0);
970 }
971
972 #[test]
975 fn test_is_reachable_direct() {
976 let m = linear_mapper();
977 assert!(m.is_reachable("local", "A"));
978 }
979
980 #[test]
981 fn test_is_reachable_transitive() {
982 let m = linear_mapper();
983 assert!(m.is_reachable("local", "C"));
984 }
985
986 #[test]
987 fn test_is_reachable_same_node() {
988 let m = linear_mapper();
989 assert!(m.is_reachable("local", "local"));
990 }
991
992 #[test]
993 fn test_is_reachable_unreachable() {
994 let mut m = linear_mapper();
995 m.add_node(node("island", 99));
996 assert!(!m.is_reachable("local", "island"));
997 }
998
999 #[test]
1000 fn test_is_reachable_reverse_not_reachable() {
1001 let m = linear_mapper();
1003 assert!(!m.is_reachable("C", "local"));
1004 }
1005
1006 #[test]
1009 fn test_connected_components_single() {
1010 let m = linear_mapper();
1011 let comps = m.connected_components();
1012 assert_eq!(comps.len(), 1);
1014 let mut comp = comps[0].clone();
1015 comp.sort();
1016 assert_eq!(comp, vec!["A", "B", "C", "local"]);
1017 }
1018
1019 #[test]
1020 fn test_connected_components_two_islands() {
1021 let mut m = NetworkTopologyMapper::new("L".to_string());
1022 for id in ["L", "M", "X", "Y"] {
1023 m.add_node(node(id, 0));
1024 }
1025 m.add_edge(edge("L", "M", 1.0));
1026 m.add_edge(edge("X", "Y", 1.0));
1027 let comps = m.connected_components();
1028 assert_eq!(comps.len(), 2);
1029 }
1030
1031 #[test]
1032 fn test_connected_components_sorted() {
1033 let mut m = NetworkTopologyMapper::new("Z".to_string());
1034 for id in ["Z", "A", "M", "N"] {
1035 m.add_node(node(id, 0));
1036 }
1037 m.add_edge(edge("Z", "A", 1.0));
1038 m.add_edge(edge("M", "N", 1.0));
1039 let comps = m.connected_components();
1040 for comp in &comps {
1042 let mut sorted = comp.clone();
1043 sorted.sort();
1044 assert_eq!(comp, &sorted, "component should be sorted");
1045 }
1046 for i in 1..comps.len() {
1048 assert!(comps[i - 1][0] <= comps[i][0]);
1049 }
1050 }
1051
1052 #[test]
1055 fn test_diameter_linear() {
1056 let m = linear_mapper();
1057 let d = m.diameter().expect("diameter should exist");
1059 assert!((d - 60.0).abs() < 1e-9);
1060 }
1061
1062 #[test]
1063 fn test_diameter_none_single_node() {
1064 let mut m = NetworkTopologyMapper::new("x".to_string());
1065 m.add_node(node("x", 0));
1066 assert!(m.diameter().is_none());
1067 }
1068
1069 #[test]
1070 fn test_diameter_none_disconnected_graph() {
1071 let mut m = NetworkTopologyMapper::new("a".to_string());
1073 m.add_node(node("a", 0));
1074 m.add_node(node("b", 0));
1075 assert!(m.diameter().is_none());
1077 }
1078
1079 #[test]
1082 fn test_clustering_coefficient_triangle() {
1083 let mut m = NetworkTopologyMapper::new("A".to_string());
1084 for id in ["A", "B", "C"] {
1085 m.add_node(node(id, 0));
1086 }
1087 m.add_edge(edge("A", "B", 1.0));
1089 m.add_edge(edge("B", "C", 1.0));
1090 m.add_edge(edge("A", "C", 1.0));
1091 let cc = m.average_clustering_coefficient();
1092 assert!((cc - 1.0).abs() < 1e-9);
1094 }
1095
1096 #[test]
1097 fn test_clustering_coefficient_no_triangles() {
1098 let m = linear_mapper();
1099 let cc = m.average_clustering_coefficient();
1102 assert!((cc - 0.0).abs() < 1e-9);
1103 }
1104
1105 #[test]
1106 fn test_clustering_coefficient_empty() {
1107 let m = NetworkTopologyMapper::new("x".to_string());
1108 assert_eq!(m.average_clustering_coefficient(), 0.0);
1109 }
1110
1111 #[test]
1114 fn test_snapshot_counts() {
1115 let m = linear_mapper();
1116 let snap = m.snapshot();
1117 assert_eq!(snap.local_node_id, "local");
1118 assert_eq!(snap.nodes.len(), 4);
1119 assert_eq!(snap.edges.len(), 3);
1120 }
1121
1122 #[test]
1125 fn test_node_count() {
1126 let m = linear_mapper();
1127 assert_eq!(m.node_count(), 4);
1128 }
1129
1130 #[test]
1131 fn test_edge_count() {
1132 let m = linear_mapper();
1133 assert_eq!(m.edge_count(), 3);
1134 }
1135
1136 #[test]
1139 fn test_evict_stale_removes_old_nodes() {
1140 let mut m = NetworkTopologyMapper::new("local".to_string());
1141 m.add_node(TopoNode {
1142 node_id: "old".to_string(),
1143 address: "".to_string(),
1144 discovered_at: 0,
1145 hop_distance: 1,
1146 });
1147 m.add_node(TopoNode {
1148 node_id: "fresh".to_string(),
1149 address: "".to_string(),
1150 discovered_at: 900,
1151 hop_distance: 1,
1152 });
1153 let removed = m.evict_stale(500, 1000);
1155 assert_eq!(removed, 1);
1156 assert!(!m.nodes.contains_key("old"));
1157 assert!(m.nodes.contains_key("fresh"));
1158 }
1159
1160 #[test]
1161 fn test_evict_stale_removes_associated_edges() {
1162 let mut m = NetworkTopologyMapper::new("local".to_string());
1163 m.add_node(TopoNode {
1164 node_id: "A".to_string(),
1165 address: "".to_string(),
1166 discovered_at: 0,
1167 hop_distance: 1,
1168 });
1169 m.add_node(TopoNode {
1170 node_id: "B".to_string(),
1171 address: "".to_string(),
1172 discovered_at: 999,
1173 hop_distance: 2,
1174 });
1175 m.add_edge(edge("A", "B", 5.0));
1176 m.evict_stale(500, 1000);
1177 assert!(!m.edges.iter().any(|e| e.from == "A" || e.to == "A"));
1179 }
1180
1181 #[test]
1182 fn test_evict_stale_returns_zero_when_all_fresh() {
1183 let m = linear_mapper(); let mut m2 = m.clone();
1185 let removed = m2.evict_stale(5000, 2000); assert_eq!(removed, 0);
1187 }
1188
1189 #[test]
1192 fn test_stats_empty() {
1193 let m = NetworkTopologyMapper::new("local".to_string());
1194 let s = m.stats();
1195 assert_eq!(
1196 s,
1197 TopologyStats {
1198 node_count: 0,
1199 edge_count: 0,
1200 avg_latency_ms: 0.0,
1201 max_latency_ms: 0.0,
1202 avg_out_degree: 0.0,
1203 component_count: 0,
1204 diameter: None,
1205 }
1206 );
1207 }
1208
1209 #[test]
1210 fn test_stats_linear_graph() {
1211 let m = linear_mapper();
1212 let s = m.stats();
1213 assert_eq!(s.node_count, 4);
1214 assert_eq!(s.edge_count, 3);
1215 assert!((s.avg_latency_ms - 20.0).abs() < 1e-9);
1217 assert!((s.max_latency_ms - 30.0).abs() < 1e-9);
1219 assert_eq!(s.component_count, 1);
1220 }
1221
1222 #[test]
1225 fn test_path_result_equality() {
1226 let p1 = PathResult {
1227 path: vec!["A".to_string(), "B".to_string()],
1228 total_latency_ms: 5.0,
1229 hop_count: 1,
1230 };
1231 let p2 = p1.clone();
1232 assert_eq!(p1, p2);
1233 }
1234
1235 #[test]
1238 fn test_shortest_path_uses_minimum_latency_edge() {
1239 let mut m = NetworkTopologyMapper::new("s".to_string());
1240 m.add_node(node("s", 0));
1241 m.add_node(node("t", 0));
1242 m.add_edge(edge("s", "t", 100.0));
1243 m.add_edge(edge("s", "t", 5.0));
1244 let r = m.shortest_path("s", "t").expect("path should exist");
1245 assert!((r.total_latency_ms - 5.0).abs() < 1e-9);
1246 }
1247
1248 #[test]
1251 fn test_edge_has_discovered_at() {
1252 let e = edge_ts("X", "Y", 1.0, 42_000);
1253 assert_eq!(e.discovered_at, 42_000);
1254 }
1255
1256 #[test]
1259 fn test_add_node_rebuilds_adjacency() {
1260 let mut m = NetworkTopologyMapper::new("local".to_string());
1261 m.add_edge(edge("local", "peer", 1.0));
1262 m.add_node(node("peer", 1));
1263 let nbrs = m.neighbors("local");
1264 assert!(nbrs.contains(&"peer"));
1265 }
1266
1267 #[test]
1270 fn test_is_reachable_after_remove_edge() {
1271 let mut m = linear_mapper();
1272 m.remove_edge("A", "B");
1273 assert!(!m.is_reachable("local", "B"));
1274 }
1275
1276 #[test]
1279 fn test_evict_stale_boundary_exact_age() {
1280 let mut m = NetworkTopologyMapper::new("local".to_string());
1282 m.add_node(TopoNode {
1283 node_id: "A".to_string(),
1284 address: "".to_string(),
1285 discovered_at: 500,
1286 hop_distance: 1,
1287 });
1288 let removed = m.evict_stale(500, 1000);
1289 assert_eq!(removed, 0);
1290 assert!(m.nodes.contains_key("A"));
1291 }
1292
1293 #[test]
1296 fn test_hop_shortest_path_disconnected_none() {
1297 let mut m = linear_mapper();
1298 m.add_node(node("isolated", 99));
1299 assert!(m.hop_shortest_path("local", "isolated").is_none());
1300 }
1301
1302 #[test]
1305 fn test_in_degree_multiple_sources() {
1306 let mut m = NetworkTopologyMapper::new("hub".to_string());
1307 for id in ["hub", "s1", "s2", "s3"] {
1308 m.add_node(node(id, 0));
1309 }
1310 m.add_edge(edge("s1", "hub", 1.0));
1311 m.add_edge(edge("s2", "hub", 2.0));
1312 m.add_edge(edge("s3", "hub", 3.0));
1313 assert_eq!(m.in_degree("hub"), 3);
1314 }
1315
1316 #[test]
1319 fn test_snapshot_local_id() {
1320 let m = NetworkTopologyMapper::new("my-peer".to_string());
1321 assert_eq!(m.snapshot().local_node_id, "my-peer");
1322 }
1323
1324 #[test]
1327 fn test_remove_node_reduces_count() {
1328 let mut m = linear_mapper();
1329 let before = m.node_count();
1330 m.remove_node("C");
1331 assert_eq!(m.node_count(), before - 1);
1332 }
1333
1334 #[test]
1337 fn test_add_edge_creates_destination_adjacency() {
1338 let mut m = NetworkTopologyMapper::new("s".to_string());
1339 m.add_edge(edge("s", "d", 1.0));
1340 assert!(m.adjacency.contains_key("d"));
1341 }
1342
1343 #[test]
1346 fn test_clustering_partial_triangle() {
1347 let mut m = NetworkTopologyMapper::new("A".to_string());
1348 for id in ["A", "B", "C", "D"] {
1349 m.add_node(node(id, 0));
1350 }
1351 m.add_edge(edge("A", "B", 1.0));
1353 m.add_edge(edge("A", "C", 1.0));
1354 m.add_edge(edge("A", "D", 1.0));
1355 let cc = m.average_clustering_coefficient();
1356 assert!((cc - 0.0).abs() < 1e-9);
1358 }
1359
1360 #[test]
1363 fn test_diameter_picks_max() {
1364 let mut m = NetworkTopologyMapper::new("A".to_string());
1365 for id in ["A", "B", "C"] {
1366 m.add_node(node(id, 0));
1367 }
1368 m.add_edge(edge("A", "B", 1.0));
1369 m.add_edge(edge("B", "C", 999.0));
1370 let d = m.diameter().expect("diameter should exist");
1372 assert!((d - 1000.0).abs() < 1e-9);
1373 }
1374
1375 #[test]
1378 fn test_stats_component_count() {
1379 let mut m = NetworkTopologyMapper::new("A".to_string());
1380 for id in ["A", "B", "C", "D"] {
1381 m.add_node(node(id, 0));
1382 }
1383 m.add_edge(edge("A", "B", 1.0));
1384 m.add_edge(edge("C", "D", 1.0));
1385 let s = m.stats();
1386 assert_eq!(s.component_count, 2);
1387 }
1388
1389 #[test]
1392 fn test_topo_node_hop_distance() {
1393 let n = TopoNode {
1394 node_id: "peer".to_string(),
1395 address: "/ip4/1.2.3.4/tcp/1234".to_string(),
1396 discovered_at: 500,
1397 hop_distance: 7,
1398 };
1399 assert_eq!(n.hop_distance, 7);
1400 }
1401}