1use std::cmp::Reverse;
18use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
19
20pub type NtmNodeId = [u8; 32];
24
25pub type NtmEdgeId = u64;
27
28pub type NtmNetworkTopologyMapper = NetworkTopologyMapper;
30
31#[inline]
34fn xorshift64(state: &mut u64) -> u64 {
35 let mut x = *state;
36 x ^= x << 13;
37 x ^= x >> 7;
38 x ^= x << 17;
39 *state = x;
40 x
41}
42
43#[inline]
44fn fnv1a_64(data: &[u8]) -> u64 {
45 let mut h: u64 = 14_695_981_039_346_656_037;
46 for &b in data {
47 h ^= b as u64;
48 h = h.wrapping_mul(1_099_511_628_211);
49 }
50 h
51}
52
53#[derive(Debug, Clone)]
57pub struct NtmMapperConfig {
58 pub max_nodes: usize,
60 pub max_edges: usize,
62 pub snapshot_interval_secs: u64,
64 pub prune_disconnected_after_secs: u64,
66}
67
68impl Default for NtmMapperConfig {
69 fn default() -> Self {
70 Self {
71 max_nodes: 4_096,
72 max_edges: 65_536,
73 snapshot_interval_secs: 60,
74 prune_disconnected_after_secs: 300,
75 }
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum NtmMapperError {
84 NotFound(String),
86 CapacityExceeded(String),
88 Duplicate(String),
90 Internal(String),
92}
93
94impl std::fmt::Display for NtmMapperError {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 match self {
97 Self::NotFound(s) => write!(f, "not found: {s}"),
98 Self::CapacityExceeded(s) => write!(f, "capacity exceeded: {s}"),
99 Self::Duplicate(s) => write!(f, "duplicate: {s}"),
100 Self::Internal(s) => write!(f, "internal error: {s}"),
101 }
102 }
103}
104
105impl std::error::Error for NtmMapperError {}
106
107#[derive(Debug, Clone)]
111pub struct NtmNode {
112 pub id: NtmNodeId,
114 pub addr: String,
116 pub region: Option<String>,
118 pub rtt_ms: f64,
120 pub last_seen: u64,
122 pub degree: u32,
124 pub is_bootstrap: bool,
126}
127
128#[derive(Debug, Clone)]
130pub struct NtmEdge {
131 pub id: NtmEdgeId,
133 pub src: NtmNodeId,
135 pub dst: NtmNodeId,
137 pub latency_ms: f64,
139 pub bandwidth_kbps: f64,
141 pub observed_at: u64,
143}
144
145#[derive(Debug, Clone)]
147pub struct NtmSnapshot {
148 pub ts: u64,
150 pub node_count: usize,
152 pub edge_count: usize,
154 pub avg_degree: f64,
156 pub diameter: u32,
158 pub clustering_coeff: f64,
160}
161
162#[derive(Debug, Clone)]
164pub struct NtmTopologyMetrics {
165 pub density: f64,
167 pub avg_path_length: f64,
169 pub betweenness: HashMap<NtmNodeId, f64>,
171 pub centrality: HashMap<NtmNodeId, f64>,
173}
174
175type EdgeKey = (NtmNodeId, NtmNodeId);
179
180pub struct NetworkTopologyMapper {
187 nodes: HashMap<NtmNodeId, NtmNode>,
189 edges: HashMap<NtmEdgeId, NtmEdge>,
191 adj: HashMap<EdgeKey, NtmEdgeId>,
193 out_edges: HashMap<NtmNodeId, Vec<NtmEdgeId>>,
195 in_edges: HashMap<NtmNodeId, Vec<NtmEdgeId>>,
197 snapshots: VecDeque<NtmSnapshot>,
199 config: NtmMapperConfig,
201 prng_state: u64,
203 edge_counter: u64,
205}
206
207impl NetworkTopologyMapper {
208 pub fn new(config: NtmMapperConfig) -> Self {
212 let seed = fnv1a_64(b"NetworkTopologyMapper:v1");
215 Self {
216 nodes: HashMap::new(),
217 edges: HashMap::new(),
218 adj: HashMap::new(),
219 out_edges: HashMap::new(),
220 in_edges: HashMap::new(),
221 snapshots: VecDeque::with_capacity(20),
222 config,
223 prng_state: seed | 1, edge_counter: 0,
225 }
226 }
227
228 pub fn with_defaults() -> Self {
230 Self::new(NtmMapperConfig::default())
231 }
232
233 pub fn add_node(
241 &mut self,
242 id: NtmNodeId,
243 addr: impl Into<String>,
244 region: Option<String>,
245 rtt_ms: f64,
246 last_seen: u64,
247 ) -> Result<(), NtmMapperError> {
248 if let Some(existing) = self.nodes.get_mut(&id) {
249 existing.addr = addr.into();
250 existing.region = region;
251 existing.rtt_ms = rtt_ms;
252 existing.last_seen = last_seen;
253 return Ok(());
254 }
255 if self.nodes.len() >= self.config.max_nodes {
256 return Err(NtmMapperError::CapacityExceeded(format!(
257 "node limit {} reached",
258 self.config.max_nodes
259 )));
260 }
261 self.nodes.insert(
262 id,
263 NtmNode {
264 id,
265 addr: addr.into(),
266 region,
267 rtt_ms,
268 last_seen,
269 degree: 0,
270 is_bootstrap: false,
271 },
272 );
273 Ok(())
274 }
275
276 pub fn remove_node(&mut self, id: &NtmNodeId) -> Result<(), NtmMapperError> {
278 if !self.nodes.contains_key(id) {
279 return Err(NtmMapperError::NotFound(format!("{id:?}")));
280 }
281 let mut to_remove: Vec<NtmEdgeId> = Vec::new();
283 if let Some(outs) = self.out_edges.get(id) {
284 to_remove.extend_from_slice(outs);
285 }
286 if let Some(ins) = self.in_edges.get(id) {
287 to_remove.extend_from_slice(ins);
288 }
289 for eid in to_remove {
290 let _ = self.remove_edge(eid);
291 }
292 self.out_edges.remove(id);
293 self.in_edges.remove(id);
294 self.nodes.remove(id);
295 Ok(())
296 }
297
298 pub fn update_node_rtt(&mut self, id: &NtmNodeId, rtt_ms: f64) -> Result<(), NtmMapperError> {
300 let node = self
301 .nodes
302 .get_mut(id)
303 .ok_or_else(|| NtmMapperError::NotFound(format!("{id:?}")))?;
304 node.rtt_ms = rtt_ms;
305 Ok(())
306 }
307
308 pub fn set_bootstrap(&mut self, id: &NtmNodeId, flag: bool) -> Result<(), NtmMapperError> {
310 let node = self
311 .nodes
312 .get_mut(id)
313 .ok_or_else(|| NtmMapperError::NotFound(format!("{id:?}")))?;
314 node.is_bootstrap = flag;
315 Ok(())
316 }
317
318 pub fn get_node(&self, id: &NtmNodeId) -> Option<&NtmNode> {
320 self.nodes.get(id)
321 }
322
323 pub fn nodes(&self) -> impl Iterator<Item = &NtmNode> {
325 self.nodes.values()
326 }
327
328 pub fn add_edge(
336 &mut self,
337 src: NtmNodeId,
338 dst: NtmNodeId,
339 latency_ms: f64,
340 bandwidth_kbps: f64,
341 observed_at: u64,
342 ) -> Result<NtmEdgeId, NtmMapperError> {
343 if !self.nodes.contains_key(&src) {
344 return Err(NtmMapperError::NotFound(format!("src {:?}", src)));
345 }
346 if !self.nodes.contains_key(&dst) {
347 return Err(NtmMapperError::NotFound(format!("dst {:?}", dst)));
348 }
349
350 if let Some(&eid) = self.adj.get(&(src, dst)) {
352 if let Some(edge) = self.edges.get_mut(&eid) {
353 edge.latency_ms = latency_ms;
354 edge.bandwidth_kbps = bandwidth_kbps;
355 edge.observed_at = observed_at;
356 }
357 return Ok(eid);
358 }
359
360 if self.edges.len() >= self.config.max_edges {
361 return Err(NtmMapperError::CapacityExceeded(format!(
362 "edge limit {} reached",
363 self.config.max_edges
364 )));
365 }
366
367 let eid = self.gen_edge_id(&src, &dst);
368 let edge = NtmEdge {
369 id: eid,
370 src,
371 dst,
372 latency_ms,
373 bandwidth_kbps,
374 observed_at,
375 };
376 self.edges.insert(eid, edge);
377 self.adj.insert((src, dst), eid);
378 self.out_edges.entry(src).or_default().push(eid);
379 self.in_edges.entry(dst).or_default().push(eid);
380
381 self.recompute_degree(&src);
383 self.recompute_degree(&dst);
384
385 Ok(eid)
386 }
387
388 pub fn remove_edge(&mut self, id: NtmEdgeId) -> Result<(), NtmMapperError> {
390 let edge = self
391 .edges
392 .remove(&id)
393 .ok_or_else(|| NtmMapperError::NotFound(format!("edge {id}")))?;
394 self.adj.remove(&(edge.src, edge.dst));
395 if let Some(list) = self.out_edges.get_mut(&edge.src) {
396 list.retain(|&e| e != id);
397 }
398 if let Some(list) = self.in_edges.get_mut(&edge.dst) {
399 list.retain(|&e| e != id);
400 }
401 self.recompute_degree(&edge.src);
402 self.recompute_degree(&edge.dst);
403 Ok(())
404 }
405
406 pub fn update_edge_latency(
408 &mut self,
409 id: NtmEdgeId,
410 latency_ms: f64,
411 ) -> Result<(), NtmMapperError> {
412 let edge = self
413 .edges
414 .get_mut(&id)
415 .ok_or_else(|| NtmMapperError::NotFound(format!("edge {id}")))?;
416 edge.latency_ms = latency_ms;
417 Ok(())
418 }
419
420 pub fn get_edge(&self, id: NtmEdgeId) -> Option<&NtmEdge> {
422 self.edges.get(&id)
423 }
424
425 pub fn find_edge(&self, src: &NtmNodeId, dst: &NtmNodeId) -> Option<NtmEdgeId> {
427 self.adj.get(&(*src, *dst)).copied()
428 }
429
430 pub fn edges(&self) -> impl Iterator<Item = &NtmEdge> {
432 self.edges.values()
433 }
434
435 pub fn neighbors(&self, node_id: &NtmNodeId) -> Vec<NtmNodeId> {
439 match self.out_edges.get(node_id) {
440 None => Vec::new(),
441 Some(eids) => eids
442 .iter()
443 .filter_map(|eid| self.edges.get(eid).map(|e| e.dst))
444 .collect(),
445 }
446 }
447
448 pub fn shortest_path(&self, src: &NtmNodeId, dst: &NtmNodeId) -> Option<Vec<NtmNodeId>> {
452 if !self.nodes.contains_key(src) || !self.nodes.contains_key(dst) {
453 return None;
454 }
455 if src == dst {
456 return Some(vec![*src]);
457 }
458
459 let mut dist: HashMap<NtmNodeId, f64> = HashMap::new();
462 let mut prev: HashMap<NtmNodeId, NtmNodeId> = HashMap::new();
463 let mut heap: BinaryHeap<Reverse<(u64, NtmNodeId)>> = BinaryHeap::new();
465
466 dist.insert(*src, 0.0);
467 heap.push(Reverse((0u64, *src)));
468
469 while let Some(Reverse((d_bits, u))) = heap.pop() {
470 let d = f64::from_bits(d_bits);
471 if let Some(&best) = dist.get(&u) {
473 if d > best + f64::EPSILON {
474 continue;
475 }
476 }
477 if &u == dst {
478 let mut path = vec![u];
480 let mut cur = u;
481 while let Some(&p) = prev.get(&cur) {
482 path.push(p);
483 cur = p;
484 }
485 path.reverse();
486 return Some(path);
487 }
488 if let Some(eids) = self.out_edges.get(&u) {
489 for &eid in eids {
490 if let Some(edge) = self.edges.get(&eid) {
491 let new_d = d + edge.latency_ms.max(0.0);
492 let better = match dist.get(&edge.dst) {
493 None => true,
494 Some(&old) => new_d < old - f64::EPSILON,
495 };
496 if better {
497 dist.insert(edge.dst, new_d);
498 prev.insert(edge.dst, u);
499 heap.push(Reverse((new_d.to_bits(), edge.dst)));
500 }
501 }
502 }
503 }
504 }
505 None
506 }
507
508 pub fn bfs_distance(&self, src: &NtmNodeId, dst: &NtmNodeId) -> Option<u32> {
512 if !self.nodes.contains_key(src) || !self.nodes.contains_key(dst) {
513 return None;
514 }
515 if src == dst {
516 return Some(0);
517 }
518 let mut visited: HashSet<NtmNodeId> = HashSet::new();
519 let mut queue: VecDeque<(NtmNodeId, u32)> = VecDeque::new();
520 queue.push_back((*src, 0));
521 visited.insert(*src);
522 while let Some((cur, d)) = queue.pop_front() {
523 for nb in self.neighbors(&cur) {
524 if &nb == dst {
525 return Some(d + 1);
526 }
527 if visited.insert(nb) {
528 queue.push_back((nb, d + 1));
529 }
530 }
531 }
532 None
533 }
534
535 pub fn diameter(&self) -> u32 {
539 let ids: Vec<NtmNodeId> = self.nodes.keys().copied().collect();
540 if ids.len() < 2 {
541 return 0;
542 }
543 let mut max_d = 0u32;
544 for &start in &ids {
545 let mut dist: HashMap<NtmNodeId, u32> = HashMap::new();
547 let mut queue: VecDeque<NtmNodeId> = VecDeque::new();
548 dist.insert(start, 0);
549 queue.push_back(start);
550 while let Some(cur) = queue.pop_front() {
551 let d = dist[&cur];
552 for nb in self.neighbors(&cur) {
553 if let std::collections::hash_map::Entry::Vacant(e) = dist.entry(nb) {
554 e.insert(d + 1);
555 queue.push_back(nb);
556 }
557 }
558 }
559 for &v in dist.values() {
560 if v > max_d {
561 max_d = v;
562 }
563 }
564 }
565 max_d
566 }
567
568 pub fn clustering_coefficient(&self, node_id: &NtmNodeId) -> f64 {
573 let nbs: Vec<NtmNodeId> = self.neighbors(node_id);
574 let k = nbs.len();
575 if k < 2 {
576 return 0.0;
577 }
578 let mut triangles = 0u64;
579 for &nb in &nbs {
580 for &nb2 in &nbs {
581 if nb == nb2 {
582 continue;
583 }
584 if self.adj.contains_key(&(nb, nb2)) {
585 triangles += 1;
586 }
587 }
588 }
589 triangles as f64 / (k as f64 * (k as f64 - 1.0))
591 }
592
593 pub fn compute_betweenness_centrality(&self) -> HashMap<NtmNodeId, f64> {
597 let mut cb: HashMap<NtmNodeId, f64> = self.nodes.keys().map(|&id| (id, 0.0)).collect();
598
599 let all_nodes: Vec<NtmNodeId> = self.nodes.keys().copied().collect();
600
601 for &s in &all_nodes {
602 let mut stack: Vec<NtmNodeId> = Vec::new();
604 let mut pred: HashMap<NtmNodeId, Vec<NtmNodeId>> = HashMap::new();
605 let mut sigma: HashMap<NtmNodeId, f64> = HashMap::new();
606 let mut dist_map: HashMap<NtmNodeId, i64> = HashMap::new();
607
608 for &v in &all_nodes {
609 pred.insert(v, Vec::new());
610 sigma.insert(v, 0.0);
611 dist_map.insert(v, -1);
612 }
613 if let Some(sig) = sigma.get_mut(&s) {
614 *sig = 1.0;
615 }
616 if let Some(d) = dist_map.get_mut(&s) {
617 *d = 0;
618 }
619
620 let mut queue: VecDeque<NtmNodeId> = VecDeque::new();
621 queue.push_back(s);
622
623 while let Some(v) = queue.pop_front() {
624 stack.push(v);
625 let dv = dist_map[&v];
626 let sig_v = sigma[&v];
627 if let Some(eids) = self.out_edges.get(&v) {
628 for &eid in eids {
629 if let Some(edge) = self.edges.get(&eid) {
630 let w = edge.dst;
631 if dist_map[&w] < 0 {
632 queue.push_back(w);
633 if let Some(d) = dist_map.get_mut(&w) {
634 *d = dv + 1;
635 }
636 }
637 if dist_map[&w] == dv + 1 {
638 if let Some(sig) = sigma.get_mut(&w) {
639 *sig += sig_v;
640 }
641 if let Some(p) = pred.get_mut(&w) {
642 p.push(v);
643 }
644 }
645 }
646 }
647 }
648 }
649
650 let mut delta: HashMap<NtmNodeId, f64> = all_nodes.iter().map(|&v| (v, 0.0)).collect();
651
652 while let Some(w) = stack.pop() {
653 let sig_w = sigma[&w];
654 let delta_w = delta[&w];
655 let preds = pred[&w].clone();
656 for v in preds {
657 let coeff = (sigma[&v] / sig_w) * (1.0 + delta_w);
658 if let Some(d) = delta.get_mut(&v) {
659 *d += coeff;
660 }
661 }
662 if w != s {
663 if let Some(c) = cb.get_mut(&w) {
664 *c += delta_w;
665 }
666 }
667 }
668 }
669
670 let n = all_nodes.len() as f64;
672 if n > 2.0 {
673 let norm = (n - 1.0) * (n - 2.0);
674 for v in cb.values_mut() {
675 *v /= norm;
676 }
677 }
678
679 cb
680 }
681
682 pub fn take_snapshot(&mut self, ts: u64) -> NtmSnapshot {
689 let node_count = self.nodes.len();
690 let edge_count = self.edges.len();
691 let avg_degree = if node_count == 0 {
692 0.0
693 } else {
694 self.nodes.values().map(|n| n.degree as f64).sum::<f64>() / node_count as f64
695 };
696 let diameter = self.diameter();
697 let clustering_coeff = if node_count == 0 {
698 0.0
699 } else {
700 let sum: f64 = self
701 .nodes
702 .keys()
703 .map(|id| self.clustering_coefficient(id))
704 .sum();
705 sum / node_count as f64
706 };
707 let snap = NtmSnapshot {
708 ts,
709 node_count,
710 edge_count,
711 avg_degree,
712 diameter,
713 clustering_coeff,
714 };
715 if self.snapshots.len() == 20 {
716 self.snapshots.pop_front();
717 }
718 self.snapshots.push_back(snap.clone());
719 snap
720 }
721
722 pub fn snapshots(&self) -> &VecDeque<NtmSnapshot> {
724 &self.snapshots
725 }
726
727 pub fn prune_stale(&mut self, now_ts: u64) {
732 let threshold = now_ts.saturating_sub(self.config.prune_disconnected_after_secs);
733 let stale: Vec<NtmNodeId> = self
734 .nodes
735 .values()
736 .filter(|n| n.last_seen < threshold)
737 .map(|n| n.id)
738 .collect();
739 for id in stale {
740 let _ = self.remove_node(&id);
741 }
742 }
743
744 pub fn topology_stats(&self) -> NtmTopologyMetrics {
748 let n = self.nodes.len();
749 let e = self.edges.len();
750
751 let density = if n > 1 {
752 e as f64 / (n as f64 * (n as f64 - 1.0))
753 } else {
754 0.0
755 };
756
757 let avg_path_length = self.compute_avg_path_length();
758 let betweenness = self.compute_betweenness_centrality();
759
760 let centrality: HashMap<NtmNodeId, f64> = if n > 1 {
761 self.nodes
762 .values()
763 .map(|node| (node.id, node.degree as f64 / (n as f64 - 1.0)))
764 .collect()
765 } else {
766 self.nodes.values().map(|n| (n.id, 0.0)).collect()
767 };
768
769 NtmTopologyMetrics {
770 density,
771 avg_path_length,
772 betweenness,
773 centrality,
774 }
775 }
776
777 pub fn node_count(&self) -> usize {
781 self.nodes.len()
782 }
783
784 pub fn edge_count(&self) -> usize {
786 self.edges.len()
787 }
788
789 pub fn config(&self) -> &NtmMapperConfig {
791 &self.config
792 }
793
794 fn gen_edge_id(&mut self, src: &NtmNodeId, dst: &NtmNodeId) -> NtmEdgeId {
798 self.edge_counter += 1;
799 let src_h = fnv1a_64(src);
801 let dst_h = fnv1a_64(dst);
802 let rnd = xorshift64(&mut self.prng_state);
803 src_h ^ dst_h.rotate_left(31) ^ rnd ^ self.edge_counter.wrapping_mul(0x9e3779b97f4a7c15)
804 }
805
806 fn recompute_degree(&mut self, id: &NtmNodeId) {
808 let out = self.out_edges.get(id).map(|v| v.len()).unwrap_or(0);
809 let inc = self.in_edges.get(id).map(|v| v.len()).unwrap_or(0);
810 if let Some(node) = self.nodes.get_mut(id) {
811 node.degree = (out + inc) as u32;
812 }
813 }
814
815 fn compute_avg_path_length(&self) -> f64 {
817 let ids: Vec<NtmNodeId> = self.nodes.keys().copied().collect();
818 let n = ids.len();
819 if n < 2 {
820 return 0.0;
821 }
822 let mut total = 0u64;
823 let mut pairs = 0u64;
824 for &s in &ids {
825 let mut dist: HashMap<NtmNodeId, u32> = HashMap::new();
827 let mut queue: VecDeque<NtmNodeId> = VecDeque::new();
828 dist.insert(s, 0);
829 queue.push_back(s);
830 while let Some(cur) = queue.pop_front() {
831 let d = dist[&cur];
832 for nb in self.neighbors(&cur) {
833 if let std::collections::hash_map::Entry::Vacant(e) = dist.entry(nb) {
834 e.insert(d + 1);
835 queue.push_back(nb);
836 }
837 }
838 }
839 for (&v, &d) in &dist {
840 if v != s {
841 total += d as u64;
842 pairs += 1;
843 }
844 }
845 }
846 if pairs == 0 {
847 0.0
848 } else {
849 total as f64 / pairs as f64
850 }
851 }
852}
853
854#[cfg(test)]
857mod tests {
858 use super::*;
859
860 fn make_id(v: u8) -> NtmNodeId {
863 let mut id = [0u8; 32];
864 id[0] = v;
865 id
866 }
867
868 fn make_mapper() -> NetworkTopologyMapper {
869 NetworkTopologyMapper::with_defaults()
870 }
871
872 fn add_n(m: &mut NetworkTopologyMapper, v: u8) {
873 m.add_node(make_id(v), format!("127.0.0.{v}:4001"), None, 1.0, 100)
874 .expect("add_node failed");
875 }
876
877 fn add_e(m: &mut NetworkTopologyMapper, u: u8, v: u8, lat: f64) -> NtmEdgeId {
878 m.add_edge(make_id(u), make_id(v), lat, 1000.0, 100)
879 .expect("add_edge failed")
880 }
881
882 #[test]
885 fn test_config_defaults() {
886 let c = NtmMapperConfig::default();
887 assert_eq!(c.max_nodes, 4_096);
888 assert_eq!(c.max_edges, 65_536);
889 assert_eq!(c.snapshot_interval_secs, 60);
890 assert_eq!(c.prune_disconnected_after_secs, 300);
891 }
892
893 #[test]
894 fn test_config_custom() {
895 let c = NtmMapperConfig {
896 max_nodes: 10,
897 max_edges: 20,
898 snapshot_interval_secs: 5,
899 prune_disconnected_after_secs: 15,
900 };
901 assert_eq!(c.max_nodes, 10);
902 }
903
904 #[test]
907 fn test_new_mapper_empty() {
908 let m = make_mapper();
909 assert_eq!(m.node_count(), 0);
910 assert_eq!(m.edge_count(), 0);
911 }
912
913 #[test]
914 fn test_with_defaults() {
915 let m = NetworkTopologyMapper::with_defaults();
916 assert_eq!(m.node_count(), 0);
917 }
918
919 #[test]
922 fn test_add_single_node() {
923 let mut m = make_mapper();
924 add_n(&mut m, 1);
925 assert_eq!(m.node_count(), 1);
926 }
927
928 #[test]
929 fn test_add_node_idempotent() {
930 let mut m = make_mapper();
931 add_n(&mut m, 1);
932 add_n(&mut m, 1); assert_eq!(m.node_count(), 1);
934 }
935
936 #[test]
937 fn test_add_node_updates_rtt() {
938 let mut m = make_mapper();
939 m.add_node(make_id(1), "a:1", None, 5.0, 100)
940 .expect("test: add_node for node 1 (initial)");
941 m.add_node(make_id(1), "a:1", None, 99.0, 200)
942 .expect("test: add_node for node 1 (update rtt)");
943 assert!(
944 (m.get_node(&make_id(1))
945 .expect("test: get_node for node 1 to check rtt")
946 .rtt_ms
947 - 99.0)
948 .abs()
949 < f64::EPSILON
950 );
951 }
952
953 #[test]
954 fn test_add_node_capacity_exceeded() {
955 let config = NtmMapperConfig {
956 max_nodes: 2,
957 ..Default::default()
958 };
959 let mut m = NetworkTopologyMapper::new(config);
960 add_n(&mut m, 1);
961 add_n(&mut m, 2);
962 let r = m.add_node(make_id(3), "x", None, 1.0, 0);
963 assert!(matches!(r, Err(NtmMapperError::CapacityExceeded(_))));
964 }
965
966 #[test]
967 fn test_add_node_with_region() {
968 let mut m = make_mapper();
969 m.add_node(make_id(5), "addr", Some("eu-west".into()), 1.0, 0)
970 .expect("test: add_node for node 5 with region");
971 assert_eq!(
972 m.get_node(&make_id(5))
973 .expect("test: get_node for node 5 to check region")
974 .region
975 .as_deref(),
976 Some("eu-west")
977 );
978 }
979
980 #[test]
983 fn test_get_node_existing() {
984 let mut m = make_mapper();
985 add_n(&mut m, 7);
986 let n = m.get_node(&make_id(7));
987 assert!(n.is_some());
988 }
989
990 #[test]
991 fn test_get_node_missing() {
992 let m = make_mapper();
993 assert!(m.get_node(&make_id(99)).is_none());
994 }
995
996 #[test]
999 fn test_remove_node_basic() {
1000 let mut m = make_mapper();
1001 add_n(&mut m, 1);
1002 m.remove_node(&make_id(1))
1003 .expect("test: remove_node should succeed for existing node 1");
1004 assert_eq!(m.node_count(), 0);
1005 }
1006
1007 #[test]
1008 fn test_remove_node_missing() {
1009 let mut m = make_mapper();
1010 let r = m.remove_node(&make_id(42));
1011 assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1012 }
1013
1014 #[test]
1015 fn test_remove_node_cascades_edges() {
1016 let mut m = make_mapper();
1017 add_n(&mut m, 1);
1018 add_n(&mut m, 2);
1019 add_e(&mut m, 1, 2, 5.0);
1020 m.remove_node(&make_id(1))
1021 .expect("test: remove_node should cascade edges for node 1");
1022 assert_eq!(m.edge_count(), 0);
1023 }
1024
1025 #[test]
1028 fn test_update_rtt_ok() {
1029 let mut m = make_mapper();
1030 add_n(&mut m, 3);
1031 m.update_node_rtt(&make_id(3), 42.0)
1032 .expect("test: update_node_rtt should succeed for existing node 3");
1033 assert!(
1034 (m.get_node(&make_id(3))
1035 .expect("test: get_node should return node 3 after rtt update")
1036 .rtt_ms
1037 - 42.0)
1038 .abs()
1039 < f64::EPSILON
1040 );
1041 }
1042
1043 #[test]
1044 fn test_update_rtt_missing() {
1045 let mut m = make_mapper();
1046 let r = m.update_node_rtt(&make_id(99), 1.0);
1047 assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1048 }
1049
1050 #[test]
1053 fn test_set_bootstrap_true() {
1054 let mut m = make_mapper();
1055 add_n(&mut m, 1);
1056 m.set_bootstrap(&make_id(1), true)
1057 .expect("test: set_bootstrap true should succeed for node 1");
1058 assert!(
1059 m.get_node(&make_id(1))
1060 .expect("test: get_node should return node 1 after set_bootstrap")
1061 .is_bootstrap
1062 );
1063 }
1064
1065 #[test]
1066 fn test_set_bootstrap_false() {
1067 let mut m = make_mapper();
1068 add_n(&mut m, 1);
1069 m.set_bootstrap(&make_id(1), true)
1070 .expect("test: set_bootstrap true should succeed");
1071 m.set_bootstrap(&make_id(1), false)
1072 .expect("test: set_bootstrap false should succeed");
1073 assert!(
1074 !m.get_node(&make_id(1))
1075 .expect("test: get_node should return node 1 after set_bootstrap false")
1076 .is_bootstrap
1077 );
1078 }
1079
1080 #[test]
1083 fn test_add_edge_basic() {
1084 let mut m = make_mapper();
1085 add_n(&mut m, 1);
1086 add_n(&mut m, 2);
1087 add_e(&mut m, 1, 2, 10.0);
1088 assert_eq!(m.edge_count(), 1);
1089 }
1090
1091 #[test]
1092 fn test_add_edge_updates_latency() {
1093 let mut m = make_mapper();
1094 add_n(&mut m, 1);
1095 add_n(&mut m, 2);
1096 let eid = add_e(&mut m, 1, 2, 10.0);
1097 m.add_edge(make_id(1), make_id(2), 99.0, 500.0, 200)
1098 .expect("test: add_edge update should succeed");
1099 assert!(
1100 (m.get_edge(eid)
1101 .expect("test: get_edge should return edge after latency update")
1102 .latency_ms
1103 - 99.0)
1104 .abs()
1105 < f64::EPSILON
1106 );
1107 }
1108
1109 #[test]
1110 fn test_add_edge_missing_src() {
1111 let mut m = make_mapper();
1112 add_n(&mut m, 2);
1113 let r = m.add_edge(make_id(1), make_id(2), 1.0, 1.0, 0);
1114 assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1115 }
1116
1117 #[test]
1118 fn test_add_edge_missing_dst() {
1119 let mut m = make_mapper();
1120 add_n(&mut m, 1);
1121 let r = m.add_edge(make_id(1), make_id(2), 1.0, 1.0, 0);
1122 assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1123 }
1124
1125 #[test]
1126 fn test_add_edge_capacity_exceeded() {
1127 let config = NtmMapperConfig {
1128 max_edges: 1,
1129 ..Default::default()
1130 };
1131 let mut m = NetworkTopologyMapper::new(config);
1132 add_n(&mut m, 1);
1133 add_n(&mut m, 2);
1134 add_n(&mut m, 3);
1135 add_e(&mut m, 1, 2, 1.0);
1136 let r = m.add_edge(make_id(2), make_id(3), 1.0, 1.0, 0);
1137 assert!(matches!(r, Err(NtmMapperError::CapacityExceeded(_))));
1138 }
1139
1140 #[test]
1143 fn test_remove_edge_ok() {
1144 let mut m = make_mapper();
1145 add_n(&mut m, 1);
1146 add_n(&mut m, 2);
1147 let eid = add_e(&mut m, 1, 2, 1.0);
1148 m.remove_edge(eid)
1149 .expect("test: remove_edge should succeed for existing edge");
1150 assert_eq!(m.edge_count(), 0);
1151 }
1152
1153 #[test]
1154 fn test_remove_edge_missing() {
1155 let mut m = make_mapper();
1156 let r = m.remove_edge(0xdeadbeef);
1157 assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1158 }
1159
1160 #[test]
1161 fn test_remove_edge_updates_degree() {
1162 let mut m = make_mapper();
1163 add_n(&mut m, 1);
1164 add_n(&mut m, 2);
1165 let eid = add_e(&mut m, 1, 2, 1.0);
1166 assert_eq!(
1167 m.get_node(&make_id(1))
1168 .expect("test: get_node should return node 1 before remove_edge")
1169 .degree,
1170 1
1171 );
1172 m.remove_edge(eid)
1173 .expect("test: remove_edge should succeed when updating degree");
1174 assert_eq!(
1175 m.get_node(&make_id(1))
1176 .expect("test: get_node should return node 1 after remove_edge")
1177 .degree,
1178 0
1179 );
1180 }
1181
1182 #[test]
1185 fn test_update_edge_latency_ok() {
1186 let mut m = make_mapper();
1187 add_n(&mut m, 1);
1188 add_n(&mut m, 2);
1189 let eid = add_e(&mut m, 1, 2, 5.0);
1190 m.update_edge_latency(eid, 77.0)
1191 .expect("test: update_edge_latency should succeed");
1192 assert!(
1193 (m.get_edge(eid)
1194 .expect("test: get_edge should return edge after latency update")
1195 .latency_ms
1196 - 77.0)
1197 .abs()
1198 < f64::EPSILON
1199 );
1200 }
1201
1202 #[test]
1203 fn test_update_edge_latency_missing() {
1204 let mut m = make_mapper();
1205 let r = m.update_edge_latency(999, 1.0);
1206 assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1207 }
1208
1209 #[test]
1212 fn test_find_edge_exists() {
1213 let mut m = make_mapper();
1214 add_n(&mut m, 1);
1215 add_n(&mut m, 2);
1216 let eid = add_e(&mut m, 1, 2, 1.0);
1217 assert_eq!(m.find_edge(&make_id(1), &make_id(2)), Some(eid));
1218 }
1219
1220 #[test]
1221 fn test_find_edge_not_exists() {
1222 let m = make_mapper();
1223 assert!(m.find_edge(&make_id(1), &make_id(2)).is_none());
1224 }
1225
1226 #[test]
1229 fn test_neighbors_empty() {
1230 let mut m = make_mapper();
1231 add_n(&mut m, 1);
1232 assert!(m.neighbors(&make_id(1)).is_empty());
1233 }
1234
1235 #[test]
1236 fn test_neighbors_single() {
1237 let mut m = make_mapper();
1238 add_n(&mut m, 1);
1239 add_n(&mut m, 2);
1240 add_e(&mut m, 1, 2, 1.0);
1241 assert_eq!(m.neighbors(&make_id(1)), vec![make_id(2)]);
1242 }
1243
1244 #[test]
1245 fn test_neighbors_multiple() {
1246 let mut m = make_mapper();
1247 for v in 1..=4 {
1248 add_n(&mut m, v);
1249 }
1250 add_e(&mut m, 1, 2, 1.0);
1251 add_e(&mut m, 1, 3, 2.0);
1252 add_e(&mut m, 1, 4, 3.0);
1253 let nbs = m.neighbors(&make_id(1));
1254 assert_eq!(nbs.len(), 3);
1255 }
1256
1257 #[test]
1260 fn test_shortest_path_direct() {
1261 let mut m = make_mapper();
1262 add_n(&mut m, 1);
1263 add_n(&mut m, 2);
1264 add_e(&mut m, 1, 2, 5.0);
1265 let path = m
1266 .shortest_path(&make_id(1), &make_id(2))
1267 .expect("test: shortest_path should find direct path");
1268 assert_eq!(path, vec![make_id(1), make_id(2)]);
1269 }
1270
1271 #[test]
1272 fn test_shortest_path_multi_hop() {
1273 let mut m = make_mapper();
1274 for v in 1..=4 {
1275 add_n(&mut m, v);
1276 }
1277 add_e(&mut m, 1, 2, 100.0);
1279 add_e(&mut m, 1, 3, 1.0);
1280 add_e(&mut m, 3, 4, 1.0);
1281 add_e(&mut m, 2, 4, 1.0);
1282 let path = m
1283 .shortest_path(&make_id(1), &make_id(4))
1284 .expect("test: shortest_path should find multi-hop path");
1285 assert_eq!(path, vec![make_id(1), make_id(3), make_id(4)]);
1287 }
1288
1289 #[test]
1290 fn test_shortest_path_no_path() {
1291 let mut m = make_mapper();
1292 add_n(&mut m, 1);
1293 add_n(&mut m, 2);
1294 assert!(m.shortest_path(&make_id(1), &make_id(2)).is_none());
1295 }
1296
1297 #[test]
1298 fn test_shortest_path_same_node() {
1299 let mut m = make_mapper();
1300 add_n(&mut m, 1);
1301 let path = m
1302 .shortest_path(&make_id(1), &make_id(1))
1303 .expect("test: shortest_path to self should return single node");
1304 assert_eq!(path, vec![make_id(1)]);
1305 }
1306
1307 #[test]
1308 fn test_shortest_path_missing_node() {
1309 let m = make_mapper();
1310 assert!(m.shortest_path(&make_id(1), &make_id(2)).is_none());
1311 }
1312
1313 #[test]
1316 fn test_bfs_distance_direct() {
1317 let mut m = make_mapper();
1318 add_n(&mut m, 1);
1319 add_n(&mut m, 2);
1320 add_e(&mut m, 1, 2, 1.0);
1321 assert_eq!(m.bfs_distance(&make_id(1), &make_id(2)), Some(1));
1322 }
1323
1324 #[test]
1325 fn test_bfs_distance_two_hops() {
1326 let mut m = make_mapper();
1327 add_n(&mut m, 1);
1328 add_n(&mut m, 2);
1329 add_n(&mut m, 3);
1330 add_e(&mut m, 1, 2, 1.0);
1331 add_e(&mut m, 2, 3, 1.0);
1332 assert_eq!(m.bfs_distance(&make_id(1), &make_id(3)), Some(2));
1333 }
1334
1335 #[test]
1336 fn test_bfs_distance_self() {
1337 let mut m = make_mapper();
1338 add_n(&mut m, 1);
1339 assert_eq!(m.bfs_distance(&make_id(1), &make_id(1)), Some(0));
1340 }
1341
1342 #[test]
1343 fn test_bfs_distance_unreachable() {
1344 let mut m = make_mapper();
1345 add_n(&mut m, 1);
1346 add_n(&mut m, 2);
1347 assert!(m.bfs_distance(&make_id(1), &make_id(2)).is_none());
1348 }
1349
1350 #[test]
1353 fn test_diameter_empty() {
1354 let m = make_mapper();
1355 assert_eq!(m.diameter(), 0);
1356 }
1357
1358 #[test]
1359 fn test_diameter_single_node() {
1360 let mut m = make_mapper();
1361 add_n(&mut m, 1);
1362 assert_eq!(m.diameter(), 0);
1363 }
1364
1365 #[test]
1366 fn test_diameter_two_connected() {
1367 let mut m = make_mapper();
1368 add_n(&mut m, 1);
1369 add_n(&mut m, 2);
1370 add_e(&mut m, 1, 2, 1.0);
1371 assert_eq!(m.diameter(), 1);
1372 }
1373
1374 #[test]
1375 fn test_diameter_chain() {
1376 let mut m = make_mapper();
1378 for v in 1..=4 {
1379 add_n(&mut m, v);
1380 }
1381 add_e(&mut m, 1, 2, 1.0);
1382 add_e(&mut m, 2, 3, 1.0);
1383 add_e(&mut m, 3, 4, 1.0);
1384 assert_eq!(m.diameter(), 3);
1385 }
1386
1387 #[test]
1390 fn test_clustering_low_degree() {
1391 let mut m = make_mapper();
1392 add_n(&mut m, 1);
1393 assert!((m.clustering_coefficient(&make_id(1)) - 0.0).abs() < f64::EPSILON);
1394 }
1395
1396 #[test]
1397 fn test_clustering_triangle() {
1398 let mut m = make_mapper();
1400 add_n(&mut m, 1);
1401 add_n(&mut m, 2);
1402 add_n(&mut m, 3);
1403 add_e(&mut m, 1, 2, 1.0);
1404 add_e(&mut m, 1, 3, 1.0);
1405 add_e(&mut m, 2, 3, 1.0); add_e(&mut m, 3, 2, 1.0); let c = m.clustering_coefficient(&make_id(1));
1409 assert!(c > 0.0);
1410 }
1411
1412 #[test]
1413 fn test_clustering_no_edges_between_neighbours() {
1414 let mut m = make_mapper();
1415 add_n(&mut m, 1);
1416 add_n(&mut m, 2);
1417 add_n(&mut m, 3);
1418 add_e(&mut m, 1, 2, 1.0);
1419 add_e(&mut m, 1, 3, 1.0);
1420 let c = m.clustering_coefficient(&make_id(1));
1422 assert!((c - 0.0).abs() < f64::EPSILON);
1423 }
1424
1425 #[test]
1428 fn test_snapshot_basic() {
1429 let mut m = make_mapper();
1430 add_n(&mut m, 1);
1431 add_n(&mut m, 2);
1432 let snap = m.take_snapshot(1000);
1433 assert_eq!(snap.node_count, 2);
1434 assert_eq!(snap.ts, 1000);
1435 }
1436
1437 #[test]
1438 fn test_snapshot_stored() {
1439 let mut m = make_mapper();
1440 m.take_snapshot(1);
1441 assert_eq!(m.snapshots().len(), 1);
1442 }
1443
1444 #[test]
1445 fn test_snapshot_ring_bounded() {
1446 let mut m = make_mapper();
1447 for i in 0..25u64 {
1448 m.take_snapshot(i);
1449 }
1450 assert_eq!(m.snapshots().len(), 20);
1451 }
1452
1453 #[test]
1454 fn test_snapshot_oldest_dropped() {
1455 let mut m = make_mapper();
1456 for i in 0..21u64 {
1457 m.take_snapshot(i);
1458 }
1459 assert_eq!(
1461 m.snapshots()
1462 .front()
1463 .expect("test: snapshots should have at least one entry after 21 takes")
1464 .ts,
1465 1
1466 );
1467 }
1468
1469 #[test]
1470 fn test_snapshot_edge_count() {
1471 let mut m = make_mapper();
1472 add_n(&mut m, 1);
1473 add_n(&mut m, 2);
1474 add_e(&mut m, 1, 2, 1.0);
1475 let snap = m.take_snapshot(0);
1476 assert_eq!(snap.edge_count, 1);
1477 }
1478
1479 #[test]
1482 fn test_prune_stale_removes_old() {
1483 let config = NtmMapperConfig {
1484 prune_disconnected_after_secs: 100,
1485 ..Default::default()
1486 };
1487 let mut m = NetworkTopologyMapper::new(config);
1488 m.add_node(make_id(1), "a", None, 1.0, 0)
1489 .expect("test: add_node for stale node 1 should succeed"); m.add_node(make_id(2), "b", None, 1.0, 500)
1491 .expect("test: add_node for fresh node 2 should succeed"); m.prune_stale(600); assert_eq!(m.node_count(), 1);
1494 assert!(m.get_node(&make_id(2)).is_some());
1495 }
1496
1497 #[test]
1498 fn test_prune_stale_keeps_recent() {
1499 let config = NtmMapperConfig {
1500 prune_disconnected_after_secs: 300,
1501 ..Default::default()
1502 };
1503 let mut m = NetworkTopologyMapper::new(config);
1504 m.add_node(make_id(1), "a", None, 1.0, 1000)
1505 .expect("test: add_node for recent node should succeed");
1506 m.prune_stale(1200); assert_eq!(m.node_count(), 1);
1508 }
1509
1510 #[test]
1511 fn test_prune_stale_cascades_edges() {
1512 let config = NtmMapperConfig {
1513 prune_disconnected_after_secs: 100,
1514 ..Default::default()
1515 };
1516 let mut m = NetworkTopologyMapper::new(config);
1517 m.add_node(make_id(1), "a", None, 1.0, 0)
1518 .expect("test: add_node for cascade edge test node 1 should succeed");
1519 m.add_node(make_id(2), "b", None, 1.0, 0)
1520 .expect("test: add_node for cascade edge test node 2 should succeed");
1521 add_e(&mut m, 1, 2, 1.0);
1522 m.prune_stale(1000);
1523 assert_eq!(m.edge_count(), 0);
1524 }
1525
1526 #[test]
1529 fn test_topology_stats_empty() {
1530 let m = make_mapper();
1531 let s = m.topology_stats();
1532 assert!((s.density - 0.0).abs() < f64::EPSILON);
1533 }
1534
1535 #[test]
1536 fn test_topology_stats_density() {
1537 let mut m = make_mapper();
1539 add_n(&mut m, 1);
1540 add_n(&mut m, 2);
1541 add_e(&mut m, 1, 2, 1.0);
1542 let s = m.topology_stats();
1543 assert!((s.density - 0.5).abs() < 1e-9);
1544 }
1545
1546 #[test]
1547 fn test_topology_stats_centrality_present() {
1548 let mut m = make_mapper();
1549 add_n(&mut m, 1);
1550 add_n(&mut m, 2);
1551 add_n(&mut m, 3);
1552 add_e(&mut m, 1, 2, 1.0);
1553 add_e(&mut m, 2, 3, 1.0);
1554 let s = m.topology_stats();
1555 assert!(s.centrality.contains_key(&make_id(1)));
1556 assert!(s.centrality.contains_key(&make_id(2)));
1557 assert!(s.centrality.contains_key(&make_id(3)));
1558 }
1559
1560 #[test]
1563 fn test_betweenness_single_node() {
1564 let mut m = make_mapper();
1565 add_n(&mut m, 1);
1566 let b = m.compute_betweenness_centrality();
1567 assert!((b[&make_id(1)] - 0.0).abs() < f64::EPSILON);
1568 }
1569
1570 #[test]
1571 fn test_betweenness_chain() {
1572 let mut m = make_mapper();
1574 add_n(&mut m, 1);
1575 add_n(&mut m, 2);
1576 add_n(&mut m, 3);
1577 add_e(&mut m, 1, 2, 1.0);
1578 add_e(&mut m, 2, 3, 1.0);
1579 let b = m.compute_betweenness_centrality();
1580 assert!(b[&make_id(2)] >= b[&make_id(1)]);
1582 assert!(b[&make_id(2)] >= b[&make_id(3)]);
1583 }
1584
1585 #[test]
1588 fn test_xorshift64_nonzero() {
1589 let mut state = 12345u64;
1590 let v = xorshift64(&mut state);
1591 assert_ne!(v, 0);
1592 }
1593
1594 #[test]
1595 fn test_xorshift64_changes_state() {
1596 let mut state = 1u64;
1597 let first = xorshift64(&mut state);
1598 let second = xorshift64(&mut state);
1599 assert_ne!(first, second);
1600 }
1601
1602 #[test]
1603 fn test_fnv1a_64_empty() {
1604 let h = fnv1a_64(b"");
1605 assert_eq!(h, 14_695_981_039_346_656_037u64);
1606 }
1607
1608 #[test]
1609 fn test_fnv1a_64_known() {
1610 let h = fnv1a_64(b"hello");
1612 assert_ne!(h, 0);
1613 assert_ne!(h, 14_695_981_039_346_656_037u64);
1614 }
1615
1616 #[test]
1617 fn test_fnv1a_64_different_inputs() {
1618 let h1 = fnv1a_64(b"foo");
1619 let h2 = fnv1a_64(b"bar");
1620 assert_ne!(h1, h2);
1621 }
1622
1623 #[test]
1626 fn test_edge_ids_unique() {
1627 let mut m = make_mapper();
1628 for v in 1..=5 {
1629 add_n(&mut m, v);
1630 }
1631 let e12 = add_e(&mut m, 1, 2, 1.0);
1632 let e13 = add_e(&mut m, 1, 3, 1.0);
1633 let e14 = add_e(&mut m, 1, 4, 1.0);
1634 assert_ne!(e12, e13);
1635 assert_ne!(e12, e14);
1636 assert_ne!(e13, e14);
1637 }
1638
1639 #[test]
1642 fn test_degree_after_add_edge() {
1643 let mut m = make_mapper();
1644 add_n(&mut m, 1);
1645 add_n(&mut m, 2);
1646 add_e(&mut m, 1, 2, 1.0);
1647 assert_eq!(
1649 m.get_node(&make_id(1))
1650 .expect("test: get_node for node 1 degree after add_edge")
1651 .degree,
1652 1
1653 );
1654 assert_eq!(
1655 m.get_node(&make_id(2))
1656 .expect("test: get_node for node 2 degree after add_edge")
1657 .degree,
1658 1
1659 );
1660 }
1661
1662 #[test]
1663 fn test_degree_bidirectional() {
1664 let mut m = make_mapper();
1665 add_n(&mut m, 1);
1666 add_n(&mut m, 2);
1667 add_e(&mut m, 1, 2, 1.0);
1668 add_e(&mut m, 2, 1, 1.0);
1669 assert_eq!(
1670 m.get_node(&make_id(1))
1671 .expect("test: get_node for node 1 degree after bidirectional edges")
1672 .degree,
1673 2
1674 );
1675 assert_eq!(
1676 m.get_node(&make_id(2))
1677 .expect("test: get_node for node 2 degree after bidirectional edges")
1678 .degree,
1679 2
1680 );
1681 }
1682
1683 #[test]
1686 fn test_nodes_iterator() {
1687 let mut m = make_mapper();
1688 add_n(&mut m, 1);
1689 add_n(&mut m, 2);
1690 let ids: Vec<NtmNodeId> = m.nodes().map(|n| n.id).collect();
1691 assert_eq!(ids.len(), 2);
1692 }
1693
1694 #[test]
1695 fn test_edges_iterator() {
1696 let mut m = make_mapper();
1697 add_n(&mut m, 1);
1698 add_n(&mut m, 2);
1699 add_e(&mut m, 1, 2, 1.0);
1700 let eids: Vec<NtmEdgeId> = m.edges().map(|e| e.id).collect();
1701 assert_eq!(eids.len(), 1);
1702 }
1703
1704 #[test]
1707 fn test_error_display_not_found() {
1708 let e = NtmMapperError::NotFound("x".into());
1709 assert!(e.to_string().contains("not found"));
1710 }
1711
1712 #[test]
1713 fn test_error_display_capacity() {
1714 let e = NtmMapperError::CapacityExceeded("full".into());
1715 assert!(e.to_string().contains("capacity exceeded"));
1716 }
1717
1718 #[test]
1719 fn test_error_display_internal() {
1720 let e = NtmMapperError::Internal("oops".into());
1721 assert!(e.to_string().contains("internal error"));
1722 }
1723
1724 #[test]
1727 fn test_avg_path_length_empty() {
1728 let m = make_mapper();
1729 let s = m.topology_stats();
1730 assert!((s.avg_path_length - 0.0).abs() < f64::EPSILON);
1731 }
1732
1733 #[test]
1734 fn test_avg_path_length_two_nodes_connected() {
1735 let mut m = make_mapper();
1736 add_n(&mut m, 1);
1737 add_n(&mut m, 2);
1738 add_e(&mut m, 1, 2, 1.0);
1739 let s = m.topology_stats();
1740 assert!(s.avg_path_length >= 0.0);
1742 }
1743
1744 #[test]
1747 fn test_remove_nonexistent_edge_error() {
1748 let mut m = make_mapper();
1749 assert!(m.remove_edge(42).is_err());
1750 }
1751
1752 #[test]
1753 fn test_snapshot_avg_degree() {
1754 let mut m = make_mapper();
1755 add_n(&mut m, 1);
1756 add_n(&mut m, 2);
1757 add_e(&mut m, 1, 2, 1.0);
1758 let snap = m.take_snapshot(0);
1760 assert!((snap.avg_degree - 1.0).abs() < f64::EPSILON);
1761 }
1762
1763 #[test]
1764 fn test_many_nodes_and_edges() {
1765 let mut m = make_mapper();
1766 for v in 0..10u8 {
1767 add_n(&mut m, v);
1768 }
1769 for u in 0..9u8 {
1770 add_e(&mut m, u, u + 1, (u + 1) as f64);
1771 }
1772 assert_eq!(m.node_count(), 10);
1773 assert_eq!(m.edge_count(), 9);
1774 let path = m.shortest_path(&make_id(0), &make_id(9));
1775 assert!(path.is_some());
1776 }
1777
1778 #[test]
1779 fn test_type_alias_ntm_network_topology_mapper() {
1780 let _m: NtmNetworkTopologyMapper = NtmNetworkTopologyMapper::with_defaults();
1781 }
1782}