1pub(crate) mod columns;
2pub(crate) mod csr;
3mod error;
4mod graph;
5pub(crate) mod matrices;
6mod schema;
7pub(crate) mod storage;
8
9pub use error::Error;
10pub use graph::{DegreeDirection, Graph, ReadTxn, TriangleCountSpec, WriteTxn};
11pub use schema::{
12 DirectedNeighborEntry, EdgeId, EdgeRecord, LabelId, Language, NeighborEntry, NodeId,
13 NodeRecord, PropValue, TypeId, WeightedPath,
14};
15
16#[cfg(test)]
17mod tests {
18 use serde_json::json;
19 use tempfile::TempDir;
20
21 use super::*;
22
23 fn open_tmp() -> (TempDir, Graph) {
24 let dir = TempDir::new().unwrap();
25 let g = Graph::open(dir.path(), 1).unwrap();
26 (dir, g)
27 }
28
29 #[test]
30 fn insert_and_read_node() {
31 let (_dir, g) = open_tmp();
32
33 let id = g
34 .add_node("Person", &json!({ "name": "Alice", "age": 30 }))
35 .unwrap();
36 let record = g.get_node(id).unwrap().expect("node should exist");
37
38 let props: serde_json::Value = rmp_serde::from_slice(&record.props).unwrap();
40 assert_eq!(props["name"], "Alice");
41 assert_eq!(props["age"], 30);
42 }
43
44 #[test]
45 fn insert_and_read_edge() {
46 let (_dir, g) = open_tmp();
47
48 let alice = g.add_node("Person", &json!({ "name": "Alice" })).unwrap();
49 let bob = g.add_node("Person", &json!({ "name": "Bob" })).unwrap();
50 let eid = g
51 .add_edge(alice, bob, "KNOWS", &json!({ "since": 2020 }))
52 .unwrap();
53
54 let edge = g.get_edge(eid).unwrap().expect("edge should exist");
55 assert_eq!(edge.src, alice);
56 assert_eq!(edge.dst, bob);
57
58 let neighbors = g.out_neighbors(alice).unwrap();
59 assert_eq!(neighbors.len(), 1);
60 assert_eq!(neighbors[0].node, bob);
61 assert_eq!(neighbors[0].edge, eid);
62 }
63
64 #[test]
65 fn multiple_nodes_get_unique_ids() {
66 let (_dir, g) = open_tmp();
67 let ids: Vec<_> = (0..10)
68 .map(|i| g.add_node("Node", &json!({ "i": i })).unwrap())
69 .collect();
70 let unique: std::collections::HashSet<_> = ids.iter().collect();
71 assert_eq!(unique.len(), 10);
72 }
73
74 #[test]
75 fn nodes_by_label_returns_correct_set() {
76 let (_dir, g) = open_tmp();
77
78 let a = g.add_node("Person", &json!({})).unwrap();
79 let b = g.add_node("Person", &json!({})).unwrap();
80 let _c = g.add_node("Company", &json!({})).unwrap();
81
82 let mut persons = g.nodes_by_label("Person").unwrap();
83 persons.sort_unstable();
84 assert_eq!(persons, vec![a, b]);
85
86 let companies = g.nodes_by_label("Company").unwrap();
87 assert_eq!(companies.len(), 1);
88
89 let missing = g.nodes_by_label("Robot").unwrap();
90 assert!(missing.is_empty());
91 }
92
93 #[test]
94 fn edges_by_type_returns_correct_set() {
95 let (_dir, g) = open_tmp();
96
97 let alice = g.add_node("Person", &json!({})).unwrap();
98 let bob = g.add_node("Person", &json!({})).unwrap();
99 let corp = g.add_node("Company", &json!({})).unwrap();
100
101 let e1 = g.add_edge(alice, bob, "KNOWS", &json!({})).unwrap();
102 let e2 = g.add_edge(alice, corp, "WORKS_AT", &json!({})).unwrap();
103 let e3 = g.add_edge(bob, corp, "WORKS_AT", &json!({})).unwrap();
104
105 let knows = g.edges_by_type("KNOWS").unwrap();
106 assert_eq!(knows, vec![e1]);
107
108 let mut works = g.edges_by_type("WORKS_AT").unwrap();
109 works.sort_unstable();
110 assert_eq!(works, vec![e2, e3]);
111
112 let missing = g.edges_by_type("FOLLOWS").unwrap();
113 assert!(missing.is_empty());
114 }
115
116 #[test]
117 fn label_idx_consistent_across_reopen() {
118 let dir = TempDir::new().unwrap();
119 let id = {
120 let g = Graph::open(dir.path(), 1).unwrap();
121 g.add_node("Person", &json!({})).unwrap()
122 };
123 let g2 = Graph::open(dir.path(), 1).unwrap();
124 let persons = g2.nodes_by_label("Person").unwrap();
125 assert_eq!(persons, vec![id]);
126 }
127
128 #[test]
129 fn csr_hot_path_returns_correct_neighbors() {
130 let (_dir, g) = open_tmp();
131 let a = g.add_node("N", &json!({})).unwrap();
132 let b = g.add_node("N", &json!({})).unwrap();
133 let c = g.add_node("N", &json!({})).unwrap();
134 let e1 = g.add_edge(a, b, "E", &json!({})).unwrap();
135 let e2 = g.add_edge(a, c, "E", &json!({})).unwrap();
136
137 g.rebuild_csr().unwrap();
138
139 let mut out = g.out_neighbors(a).unwrap();
140 out.sort_unstable_by_key(|ne| ne.node);
141 assert_eq!(out.len(), 2);
142 let edge_ids: Vec<_> = out.iter().map(|ne| ne.edge).collect();
143 assert!(edge_ids.contains(&e1));
144 assert!(edge_ids.contains(&e2));
145 }
146
147 #[test]
148 fn csr_fallback_to_lmdb_for_new_nodes() {
149 let (_dir, g) = open_tmp();
150 let a = g.add_node("N", &json!({})).unwrap();
153 let b = g.add_node("N", &json!({})).unwrap();
154 let eid = g.add_edge(a, b, "E", &json!({})).unwrap();
155
156 let out = g.out_neighbors(a).unwrap();
157 assert_eq!(out.len(), 1);
158 assert_eq!(out[0].edge, eid);
159 }
160
161 #[test]
162 fn csr_snapshot_rebuilds_correctly_on_reopen() {
163 let dir = TempDir::new().unwrap();
164 let (a, b, eid) = {
165 let g = Graph::open(dir.path(), 1).unwrap();
166 let a = g.add_node("N", &json!({})).unwrap();
167 let b = g.add_node("N", &json!({})).unwrap();
168 let eid = g.add_edge(a, b, "E", &json!({})).unwrap();
169 (a, b, eid)
170 };
171 let g2 = Graph::open(dir.path(), 1).unwrap();
172 let out = g2.out_neighbors(a).unwrap();
173 assert_eq!(out.len(), 1);
174 assert_eq!(out[0].node, b);
175 assert_eq!(out[0].edge, eid);
176 }
177
178 #[test]
183 fn bfs_hops_zero_returns_start_only() {
184 let (_dir, g) = open_tmp();
185 let a = g.add_node("N", &json!({})).unwrap();
186 let b = g.add_node("N", &json!({})).unwrap();
187 g.add_edge(a, b, "E", &json!({})).unwrap();
188
189 let result = g.bfs(a, 0).unwrap();
190 assert_eq!(result, vec![a]);
191 }
192
193 #[test]
194 fn bfs_linear_chain_respects_hop_limit() {
195 let (_dir, g) = open_tmp();
196 let a = g.add_node("N", &json!({})).unwrap();
198 let b = g.add_node("N", &json!({})).unwrap();
199 let c = g.add_node("N", &json!({})).unwrap();
200 let d = g.add_node("N", &json!({})).unwrap();
201 g.add_edge(a, b, "E", &json!({})).unwrap();
202 g.add_edge(b, c, "E", &json!({})).unwrap();
203 g.add_edge(c, d, "E", &json!({})).unwrap();
204 g.rebuild_csr().unwrap();
205
206 let mut hop1 = g.bfs(a, 1).unwrap();
207 hop1.sort_unstable();
208 assert_eq!(hop1, vec![a, b]);
209
210 let mut hop2 = g.bfs(a, 2).unwrap();
211 hop2.sort_unstable();
212 assert_eq!(hop2, vec![a, b, c]);
213
214 let mut hop3 = g.bfs(a, 3).unwrap();
215 hop3.sort_unstable();
216 assert_eq!(hop3, vec![a, b, c, d]);
217 }
218
219 #[test]
220 fn bfs_does_not_revisit_nodes_in_a_cycle() {
221 let (_dir, g) = open_tmp();
222 let a = g.add_node("N", &json!({})).unwrap();
224 let b = g.add_node("N", &json!({})).unwrap();
225 let c = g.add_node("N", &json!({})).unwrap();
226 g.add_edge(a, b, "E", &json!({})).unwrap();
227 g.add_edge(b, c, "E", &json!({})).unwrap();
228 g.add_edge(c, a, "E", &json!({})).unwrap();
229 g.rebuild_csr().unwrap();
230
231 let mut result = g.bfs(a, 10).unwrap();
232 result.sort_unstable();
233 assert_eq!(result, vec![a, b, c]);
234 }
235
236 #[test]
237 fn bfs_isolated_node_returns_only_itself() {
238 let (_dir, g) = open_tmp();
239 let a = g.add_node("N", &json!({})).unwrap();
240 let _b = g.add_node("N", &json!({})).unwrap();
241 g.rebuild_csr().unwrap();
242
243 let result = g.bfs(a, 5).unwrap();
244 assert_eq!(result, vec![a]);
245 }
246
247 #[test]
248 fn bfs_works_via_dynamic_matrix_materialization_without_manual_rebuild() {
249 let (_dir, g) = open_tmp();
250 let a = g.add_node("N", &json!({})).unwrap();
252 let b = g.add_node("N", &json!({})).unwrap();
253 let c = g.add_node("N", &json!({})).unwrap();
254 g.add_edge(a, b, "E", &json!({})).unwrap();
255 g.add_edge(b, c, "E", &json!({})).unwrap();
256
257 let mut result = g.bfs(a, 2).unwrap();
258 result.sort_unstable();
259 assert_eq!(result, vec![a, b, c]);
260 }
261
262 #[test]
267 fn dfs_hops_zero_returns_start_only() {
268 let (_dir, g) = open_tmp();
269 let a = g.add_node("N", &json!({})).unwrap();
270 let b = g.add_node("N", &json!({})).unwrap();
271 g.add_edge(a, b, "E", &json!({})).unwrap();
272
273 let result = g.dfs(a, 0).unwrap();
274 assert_eq!(result, vec![a]);
275 }
276
277 #[test]
278 fn dfs_linear_chain_pre_order_and_limit() {
279 let (_dir, g) = open_tmp();
280 let a = g.add_node("N", &json!({})).unwrap();
282 let b = g.add_node("N", &json!({})).unwrap();
283 let c = g.add_node("N", &json!({})).unwrap();
284 let d = g.add_node("N", &json!({})).unwrap();
285 g.add_edge(a, b, "E", &json!({})).unwrap();
286 g.add_edge(b, c, "E", &json!({})).unwrap();
287 g.add_edge(c, d, "E", &json!({})).unwrap();
288 g.rebuild_csr().unwrap();
289
290 let hop1 = g.dfs(a, 1).unwrap();
291 assert_eq!(hop1, vec![a, b]);
292
293 let hop2 = g.dfs(a, 2).unwrap();
294 assert_eq!(hop2, vec![a, b, c]);
295
296 let hop3 = g.dfs(a, 3).unwrap();
297 assert_eq!(hop3, vec![a, b, c, d]);
298 }
299
300 #[test]
301 fn dfs_does_not_loop_on_cycle() {
302 let (_dir, g) = open_tmp();
303 let a = g.add_node("N", &json!({})).unwrap();
305 let b = g.add_node("N", &json!({})).unwrap();
306 let c = g.add_node("N", &json!({})).unwrap();
307 g.add_edge(a, b, "E", &json!({})).unwrap();
308 g.add_edge(b, c, "E", &json!({})).unwrap();
309 g.add_edge(c, a, "E", &json!({})).unwrap();
310 g.rebuild_csr().unwrap();
311
312 let result = g.dfs(a, 10).unwrap();
313 assert_eq!(result.len(), 3);
314 assert_eq!(result[0], a);
315 assert_eq!(result[1], b);
316 assert_eq!(result[2], c);
317 }
318
319 #[test]
320 fn cycle_detection() {
321 let (_dir, g) = open_tmp();
322
323 assert!(!g.detect_cycle().unwrap());
325
326 let a = g.add_node("N", &json!({})).unwrap();
328 let b = g.add_node("N", &json!({})).unwrap();
329 let c = g.add_node("N", &json!({})).unwrap();
330 g.add_edge(a, b, "E", &json!({})).unwrap();
331 g.add_edge(b, c, "E", &json!({})).unwrap();
332 assert!(!g.detect_cycle().unwrap());
333
334 let d = g.add_node("N", &json!({})).unwrap();
336 g.add_edge(d, d, "E", &json!({})).unwrap();
337 assert!(g.detect_cycle().unwrap());
338 }
339
340 #[test]
341 fn cycle_detection_multi_hop() {
342 let (_dir, g) = open_tmp();
343 let a = g.add_node("N", &json!({})).unwrap();
344 let b = g.add_node("N", &json!({})).unwrap();
345 let c = g.add_node("N", &json!({})).unwrap();
346 g.add_edge(a, b, "E", &json!({})).unwrap();
347 g.add_edge(b, c, "E", &json!({})).unwrap();
348 g.add_edge(c, a, "E", &json!({})).unwrap();
349 g.rebuild_csr().unwrap();
350
351 assert!(g.detect_cycle().unwrap());
352 }
353
354 #[test]
355 fn all_neighbors_retrieval() {
356 let (_dir, g) = open_tmp();
357 let a = g.add_node("N", &json!({})).unwrap();
358 let b = g.add_node("N", &json!({})).unwrap();
359 let c = g.add_node("N", &json!({})).unwrap();
360
361 let e1 = g.add_edge(a, b, "OUT", &json!({})).unwrap();
362 let e2 = g.add_edge(c, a, "IN", &json!({})).unwrap();
363
364 g.rebuild_csr().unwrap();
365
366 let mut neighbors = g.all_neighbors(a).unwrap();
367 neighbors.sort_by_key(|ne| ne.node);
368
369 assert_eq!(neighbors.len(), 2);
370
371 assert_eq!(neighbors[0].node, b);
373 assert_eq!(neighbors[0].edge, e1);
374 assert!(neighbors[0].outgoing); assert_eq!(neighbors[1].node, c);
378 assert_eq!(neighbors[1].edge, e2);
379 assert!(!neighbors[1].outgoing); }
381
382 #[test]
387 fn all_paths_linear_and_multiple() {
388 let (_dir, g) = open_tmp();
389 let a = g.add_node("N", &json!({})).unwrap();
390 let b = g.add_node("N", &json!({})).unwrap();
391 let c = g.add_node("N", &json!({})).unwrap();
392
393 g.add_edge(a, b, "E", &json!({})).unwrap();
395 g.add_edge(b, c, "E", &json!({})).unwrap();
396 g.rebuild_csr().unwrap();
397
398 let paths = g.all_paths(a, c).unwrap();
399 assert_eq!(paths, vec![vec![a, b, c]]);
400
401 g.add_edge(a, c, "E", &json!({})).unwrap();
403 g.rebuild_csr().unwrap();
404
405 let mut paths = g.all_paths(a, c).unwrap();
406 paths.sort_by_key(|p| p.len());
407 assert_eq!(paths.len(), 2);
408 assert_eq!(paths[0], vec![a, c]);
409 assert_eq!(paths[1], vec![a, b, c]);
410 }
411
412 #[test]
413 fn all_paths_cyclic_avoids_infinite_loop() {
414 let (_dir, g) = open_tmp();
415 let a = g.add_node("N", &json!({})).unwrap();
416 let b = g.add_node("N", &json!({})).unwrap();
417 let c = g.add_node("N", &json!({})).unwrap();
418 g.add_edge(a, b, "E", &json!({})).unwrap();
420 g.add_edge(b, c, "E", &json!({})).unwrap();
421 g.add_edge(b, a, "E", &json!({})).unwrap();
422 g.rebuild_csr().unwrap();
423
424 let paths = g.all_paths(a, c).unwrap();
425 assert_eq!(paths, vec![vec![a, b, c]]);
427 }
428
429 #[test]
430 fn all_shortest_paths_multiple() {
431 let (_dir, g) = open_tmp();
432 let a = g.add_node("N", &json!({})).unwrap();
433 let b = g.add_node("N", &json!({})).unwrap();
434 let c = g.add_node("N", &json!({})).unwrap();
435 let d = g.add_node("N", &json!({})).unwrap();
436
437 g.add_edge(a, b, "E", &json!({})).unwrap();
439 g.add_edge(b, d, "E", &json!({})).unwrap();
440 g.add_edge(a, c, "E", &json!({})).unwrap();
441 g.add_edge(c, d, "E", &json!({})).unwrap();
442 g.rebuild_csr().unwrap();
443
444 let mut paths = g.all_shortest_paths(a, d).unwrap();
445 paths.sort();
446
447 let mut expected = vec![vec![a, b, d], vec![a, c, d]];
448 expected.sort();
449
450 assert_eq!(paths, expected);
451 }
452
453 #[test]
454 fn all_shortest_paths_unreachable_returns_empty() {
455 let (_dir, g) = open_tmp();
456 let a = g.add_node("N", &json!({})).unwrap();
457 let b = g.add_node("N", &json!({})).unwrap();
458 assert!(g.all_shortest_paths(a, b).unwrap().is_empty());
459 }
460
461 #[test]
462 fn longest_path_selection() {
463 let (_dir, g) = open_tmp();
464 let a = g.add_node("N", &json!({})).unwrap();
465 let b = g.add_node("N", &json!({})).unwrap();
466 let c = g.add_node("N", &json!({})).unwrap();
467
468 g.add_edge(a, b, "E", &json!({})).unwrap();
470 g.add_edge(b, c, "E", &json!({})).unwrap();
471 g.add_edge(a, c, "E", &json!({})).unwrap();
472 g.rebuild_csr().unwrap();
473
474 let path = g.longest_path(a, c).unwrap().unwrap();
475 assert_eq!(path, vec![a, b, c]);
476 }
477
478 #[test]
479 fn longest_path_unreachable_returns_none() {
480 let (_dir, g) = open_tmp();
481 let a = g.add_node("N", &json!({})).unwrap();
482 let b = g.add_node("N", &json!({})).unwrap();
483 assert!(g.longest_path(a, b).unwrap().is_none());
484 }
485
486 #[test]
491 fn dijkstra_shortest_path_same_node() {
492 let (_dir, g) = open_tmp();
493 let a = g.add_node("N", &json!({})).unwrap();
494 let wp = g.shortest_path_dijkstra(a, a).unwrap().unwrap();
495 assert_eq!(wp.nodes, vec![a]);
496 assert_eq!(wp.total_weight, 0.0);
497 }
498
499 #[test]
500 fn dijkstra_shortest_path_linear_and_weighted_decision() {
501 let (_dir, g) = open_tmp();
502 let a = g.add_node("N", &json!({})).unwrap();
503 let b = g.add_node("N", &json!({})).unwrap();
504 let c = g.add_node("N", &json!({})).unwrap();
505
506 g.add_edge(a, b, "E", &json!({ "cost": 1.5 })).unwrap();
508 g.add_edge(b, c, "E", &json!({ "cost": 2.0 })).unwrap();
509
510 g.add_edge(a, c, "E", &json!({ "cost": 10.0 })).unwrap();
512
513 g.rebuild_csr().unwrap();
514
515 let wp = g.shortest_path_dijkstra(a, c).unwrap().unwrap();
517 assert_eq!(wp.nodes, vec![a, b, c]);
518 assert_eq!(wp.total_weight, 3.5);
519 }
520
521 #[test]
522 fn dijkstra_shortest_path_defaults_to_one() {
523 let (_dir, g) = open_tmp();
524 let a = g.add_node("N", &json!({})).unwrap();
525 let b = g.add_node("N", &json!({})).unwrap();
526 let c = g.add_node("N", &json!({})).unwrap();
527
528 g.add_edge(a, b, "E", &json!({})).unwrap();
530 g.add_edge(b, c, "E", &json!({ "cost": "invalid" }))
532 .unwrap();
533
534 g.rebuild_csr().unwrap();
535
536 let wp = g.shortest_path_dijkstra(a, c).unwrap().unwrap();
537 assert_eq!(wp.nodes, vec![a, b, c]);
538 assert_eq!(wp.total_weight, 2.0);
539 }
540
541 #[test]
542 fn dijkstra_shortest_path_unreachable_returns_none() {
543 let (_dir, g) = open_tmp();
544 let a = g.add_node("N", &json!({})).unwrap();
545 let b = g.add_node("N", &json!({})).unwrap();
546 assert!(g.shortest_path_dijkstra(a, b).unwrap().is_none());
547 }
548
549 #[test]
554 fn spanning_forest_empty() {
555 let (_dir, g) = open_tmp();
556 let forest = g.spanning_forest("weight", false).unwrap();
557 assert!(forest.is_empty());
558 }
559
560 #[test]
561 fn spanning_forest_min_max_cyclic() {
562 let (_dir, g) = open_tmp();
563 let a = g.add_node("N", &json!({})).unwrap();
564 let b = g.add_node("N", &json!({})).unwrap();
565 let c = g.add_node("N", &json!({})).unwrap();
566
567 let e_ab = g.add_edge(a, b, "E", &json!({ "cost": 1.0 })).unwrap();
572 let e_bc = g.add_edge(b, c, "E", &json!({ "cost": 2.0 })).unwrap();
573 let e_ac = g.add_edge(a, c, "E", &json!({ "cost": 3.0 })).unwrap();
574
575 let mut min_forest = g.spanning_forest("cost", false).unwrap();
577 min_forest.sort();
578 let mut expected_min = vec![e_ab, e_bc];
579 expected_min.sort();
580 assert_eq!(min_forest, expected_min);
581
582 let mut max_forest = g.spanning_forest("cost", true).unwrap();
584 max_forest.sort();
585 let mut expected_max = vec![e_bc, e_ac];
586 expected_max.sort();
587 assert_eq!(max_forest, expected_max);
588 }
589
590 #[test]
591 fn spanning_forest_disconnected() {
592 let (_dir, g) = open_tmp();
593
594 let a = g.add_node("N", &json!({})).unwrap();
596 let b = g.add_node("N", &json!({})).unwrap();
597 let e_ab = g.add_edge(a, b, "E", &json!({ "cost": 5.0 })).unwrap();
598
599 let c = g.add_node("N", &json!({})).unwrap();
601 let d = g.add_node("N", &json!({})).unwrap();
602 let e_cd = g.add_edge(c, d, "E", &json!({ "cost": 10.0 })).unwrap();
603
604 g.rebuild_csr().unwrap();
605
606 let mut forest = g.spanning_forest("cost", false).unwrap();
607 forest.sort();
608
609 let mut expected = vec![e_ab, e_cd];
610 expected.sort();
611
612 assert_eq!(forest, expected);
613 }
614
615 #[test]
620 fn label_propagation_empty() {
621 let (_dir, g) = open_tmp();
622 let labels = g.label_propagation(10).unwrap();
623 assert!(labels.is_empty());
624 }
625
626 #[test]
627 fn label_propagation_singletons() {
628 let (_dir, g) = open_tmp();
629 let a = g.add_node("N", &json!({})).unwrap();
630 let b = g.add_node("N", &json!({})).unwrap();
631
632 let labels = g.label_propagation(10).unwrap();
633 assert_eq!(labels.len(), 2);
634 assert_eq!(labels[&a], a);
635 assert_eq!(labels[&b], b);
636 }
637
638 #[test]
639 fn label_propagation_cliques() {
640 let (_dir, g) = open_tmp();
641
642 let a = g.add_node("N", &json!({})).unwrap();
644 let b = g.add_node("N", &json!({})).unwrap();
645 let c = g.add_node("N", &json!({})).unwrap();
646 g.add_edge(a, b, "E", &json!({})).unwrap();
647 g.add_edge(b, c, "E", &json!({})).unwrap();
648 g.add_edge(c, a, "E", &json!({})).unwrap();
649
650 let d = g.add_node("N", &json!({})).unwrap();
652 let e = g.add_node("N", &json!({})).unwrap();
653 let f = g.add_node("N", &json!({})).unwrap();
654 g.add_edge(d, e, "E", &json!({})).unwrap();
655 g.add_edge(e, f, "E", &json!({})).unwrap();
656 g.add_edge(f, d, "E", &json!({})).unwrap();
657
658 g.rebuild_csr().unwrap();
659
660 let labels = g.label_propagation(100).unwrap();
661 assert_eq!(labels.len(), 6);
662
663 let comm1 = labels[&a];
665 assert_eq!(labels[&b], comm1);
666 assert_eq!(labels[&c], comm1);
667
668 let comm2 = labels[&d];
670 assert_eq!(labels[&e], comm2);
671 assert_eq!(labels[&f], comm2);
672
673 assert_ne!(comm1, comm2);
675 }
676
677 #[test]
682 fn harmonic_centrality_empty() {
683 let (_dir, g) = open_tmp();
684 let scores = g.harmonic_centrality().unwrap();
685 assert!(scores.is_empty());
686 }
687
688 #[test]
689 fn harmonic_centrality_singletons() {
690 let (_dir, g) = open_tmp();
691 let a = g.add_node("N", &json!({})).unwrap();
692 let b = g.add_node("N", &json!({})).unwrap();
693
694 let scores = g.harmonic_centrality().unwrap();
695 assert_eq!(scores.len(), 2);
696 assert_eq!(scores[&a], 0.0);
697 assert_eq!(scores[&b], 0.0);
698 }
699
700 #[test]
701 fn harmonic_centrality_linear_chain() {
702 let (_dir, g) = open_tmp();
703 let a = g.add_node("N", &json!({})).unwrap();
705 let b = g.add_node("N", &json!({})).unwrap();
706 let c = g.add_node("N", &json!({})).unwrap();
707
708 g.add_edge(a, b, "E", &json!({})).unwrap();
709 g.add_edge(b, c, "E", &json!({})).unwrap();
710
711 g.rebuild_csr().unwrap();
712
713 let scores = g.harmonic_centrality().unwrap();
714 assert_eq!(scores.len(), 3);
715
716 assert!((scores[&a] - 1.5).abs() < 1e-6);
718 assert!((scores[&b] - 1.0).abs() < 1e-6);
720 assert!((scores[&c] - 0.0).abs() < 1e-6);
722 }
723
724 #[test]
725 fn harmonic_centrality_triangle_clique() {
726 let (_dir, g) = open_tmp();
727 let a = g.add_node("N", &json!({})).unwrap();
729 let b = g.add_node("N", &json!({})).unwrap();
730 let c = g.add_node("N", &json!({})).unwrap();
731
732 g.add_edge(a, b, "E", &json!({})).unwrap();
733 g.add_edge(b, c, "E", &json!({})).unwrap();
734 g.add_edge(c, a, "E", &json!({})).unwrap();
735
736 g.rebuild_csr().unwrap();
737
738 let scores = g.harmonic_centrality().unwrap();
739 assert_eq!(scores.len(), 3);
740
741 for &score in scores.values() {
744 assert!((score - 1.5).abs() < 1e-6);
745 }
746 }
747
748 #[test]
749 fn harmonic_centrality_disconnected() {
750 let (_dir, g) = open_tmp();
751 let a = g.add_node("N", &json!({})).unwrap();
753 let b = g.add_node("N", &json!({})).unwrap();
754 g.add_edge(a, b, "E", &json!({})).unwrap();
755
756 let c = g.add_node("N", &json!({})).unwrap();
758
759 g.rebuild_csr().unwrap();
760
761 let scores = g.harmonic_centrality().unwrap();
762 assert_eq!(scores.len(), 3);
763 assert!((scores[&a] - 1.0).abs() < 1e-6);
764 assert!((scores[&b] - 0.0).abs() < 1e-6);
765 assert!((scores[&c] - 0.0).abs() < 1e-6);
766 }
767
768 #[test]
773 fn betweenness_centrality_empty() {
774 let (_dir, g) = open_tmp();
775 let scores = g.betweenness_centrality().unwrap();
776 assert!(scores.is_empty());
777 }
778
779 #[test]
780 fn betweenness_centrality_singletons() {
781 let (_dir, g) = open_tmp();
782 let a = g.add_node("N", &json!({})).unwrap();
783 let b = g.add_node("N", &json!({})).unwrap();
784
785 let scores = g.betweenness_centrality().unwrap();
786 assert_eq!(scores.len(), 2);
787 assert_eq!(scores[&a], 0.0);
788 assert_eq!(scores[&b], 0.0);
789 }
790
791 #[test]
792 fn betweenness_centrality_linear_chain() {
793 let (_dir, g) = open_tmp();
794 let a = g.add_node("N", &json!({})).unwrap();
796 let b = g.add_node("N", &json!({})).unwrap();
797 let c = g.add_node("N", &json!({})).unwrap();
798
799 g.add_edge(a, b, "E", &json!({})).unwrap();
800 g.add_edge(b, c, "E", &json!({})).unwrap();
801
802 g.rebuild_csr().unwrap();
803
804 let scores = g.betweenness_centrality().unwrap();
805 assert_eq!(scores.len(), 3);
806 assert_eq!(scores[&a], 0.0);
807 assert_eq!(scores[&b], 1.0);
808 assert_eq!(scores[&c], 0.0);
809 }
810
811 #[test]
812 fn betweenness_centrality_diamond_graph() {
813 let (_dir, g) = open_tmp();
814 let a = g.add_node("N", &json!({})).unwrap();
820 let b = g.add_node("N", &json!({})).unwrap();
821 let c = g.add_node("N", &json!({})).unwrap();
822 let d = g.add_node("N", &json!({})).unwrap();
823
824 g.add_edge(a, b, "E", &json!({})).unwrap();
825 g.add_edge(a, c, "E", &json!({})).unwrap();
826 g.add_edge(b, d, "E", &json!({})).unwrap();
827 g.add_edge(c, d, "E", &json!({})).unwrap();
828
829 g.rebuild_csr().unwrap();
830
831 let scores = g.betweenness_centrality().unwrap();
832 assert_eq!(scores.len(), 4);
833 assert_eq!(scores[&a], 0.0);
834 assert_eq!(scores[&d], 0.0);
835 assert!((scores[&b] - 0.5).abs() < 1e-6);
836 assert!((scores[&c] - 0.5).abs() < 1e-6);
837 }
838
839 #[test]
840 fn betweenness_centrality_disconnected() {
841 let (_dir, g) = open_tmp();
842 let a = g.add_node("N", &json!({})).unwrap();
844 let b = g.add_node("N", &json!({})).unwrap();
845 let c = g.add_node("N", &json!({})).unwrap();
846 g.add_edge(a, b, "E", &json!({})).unwrap();
847 g.add_edge(b, c, "E", &json!({})).unwrap();
848
849 let d = g.add_node("N", &json!({})).unwrap();
851
852 g.rebuild_csr().unwrap();
853
854 let scores = g.betweenness_centrality().unwrap();
855 assert_eq!(scores.len(), 4);
856 assert_eq!(scores[&a], 0.0);
857 assert_eq!(scores[&b], 1.0);
858 assert_eq!(scores[&c], 0.0);
859 assert_eq!(scores[&d], 0.0);
860 }
861
862 #[test]
867 fn strongly_connected_components_empty() {
868 let (_dir, g) = open_tmp();
869 let comps = g.strongly_connected_components().unwrap();
870 assert!(comps.is_empty());
871 }
872
873 #[test]
874 fn strongly_connected_components_singletons() {
875 let (_dir, g) = open_tmp();
876 let a = g.add_node("N", &json!({})).unwrap();
877 let b = g.add_node("N", &json!({})).unwrap();
878
879 let comps = g.strongly_connected_components().unwrap();
880 assert_eq!(comps.len(), 2);
881 assert_ne!(comps[&a], comps[&b]);
882 }
883
884 #[test]
885 fn strongly_connected_components_linear_chain() {
886 let (_dir, g) = open_tmp();
887 let a = g.add_node("N", &json!({})).unwrap();
889 let b = g.add_node("N", &json!({})).unwrap();
890 let c = g.add_node("N", &json!({})).unwrap();
891
892 g.add_edge(a, b, "E", &json!({})).unwrap();
893 g.add_edge(b, c, "E", &json!({})).unwrap();
894
895 g.rebuild_csr().unwrap();
896
897 let comps = g.strongly_connected_components().unwrap();
898 assert_eq!(comps.len(), 3);
899 assert_ne!(comps[&a], comps[&b]);
900 assert_ne!(comps[&b], comps[&c]);
901 assert_ne!(comps[&a], comps[&c]);
902 }
903
904 #[test]
905 fn strongly_connected_components_loop() {
906 let (_dir, g) = open_tmp();
907 let a = g.add_node("N", &json!({})).unwrap();
909 let b = g.add_node("N", &json!({})).unwrap();
910 let c = g.add_node("N", &json!({})).unwrap();
911
912 g.add_edge(a, b, "E", &json!({})).unwrap();
913 g.add_edge(b, c, "E", &json!({})).unwrap();
914 g.add_edge(c, a, "E", &json!({})).unwrap();
915
916 g.rebuild_csr().unwrap();
917
918 let comps = g.strongly_connected_components().unwrap();
919 assert_eq!(comps.len(), 3);
920 assert_eq!(comps[&a], comps[&b]);
921 assert_eq!(comps[&b], comps[&c]);
922 }
923
924 #[test]
925 fn strongly_connected_components_disconnected_clusters() {
926 let (_dir, g) = open_tmp();
927 let a = g.add_node("N", &json!({})).unwrap();
929 let b = g.add_node("N", &json!({})).unwrap();
930 g.add_edge(a, b, "E", &json!({})).unwrap();
931 g.add_edge(b, a, "E", &json!({})).unwrap();
932
933 let c = g.add_node("N", &json!({})).unwrap();
935 let d = g.add_node("N", &json!({})).unwrap();
936 g.add_edge(c, d, "E", &json!({})).unwrap();
937 g.add_edge(d, c, "E", &json!({})).unwrap();
938
939 g.rebuild_csr().unwrap();
940
941 let comps = g.strongly_connected_components().unwrap();
942 assert_eq!(comps.len(), 4);
943 assert_eq!(comps[&a], comps[&b]);
944 assert_eq!(comps[&c], comps[&d]);
945 assert_ne!(comps[&a], comps[&c]);
946 }
947
948 #[test]
953 fn degree_centrality_empty() {
954 let (_dir, g) = open_tmp();
955 let scores = g.degree_centrality(DegreeDirection::Both).unwrap();
956 assert!(scores.is_empty());
957 }
958
959 #[test]
960 fn degree_centrality_singletons() {
961 let (_dir, g) = open_tmp();
962 let a = g.add_node("N", &json!({})).unwrap();
963 let b = g.add_node("N", &json!({})).unwrap();
964
965 for dir in &[
966 DegreeDirection::In,
967 DegreeDirection::Out,
968 DegreeDirection::Both,
969 ] {
970 let scores = g.degree_centrality(*dir).unwrap();
971 assert_eq!(scores.len(), 2);
972 assert_eq!(scores[&a], 0);
973 assert_eq!(scores[&b], 0);
974 }
975 }
976
977 #[test]
978 fn degree_centrality_linear_chain() {
979 let (_dir, g) = open_tmp();
980 let a = g.add_node("N", &json!({})).unwrap();
982 let b = g.add_node("N", &json!({})).unwrap();
983 let c = g.add_node("N", &json!({})).unwrap();
984
985 g.add_edge(a, b, "E", &json!({})).unwrap();
986 g.add_edge(b, c, "E", &json!({})).unwrap();
987
988 g.rebuild_csr().unwrap();
989
990 let out_scores = g.degree_centrality(DegreeDirection::Out).unwrap();
992 assert_eq!(out_scores[&a], 1);
993 assert_eq!(out_scores[&b], 1);
994 assert_eq!(out_scores[&c], 0);
995
996 let in_scores = g.degree_centrality(DegreeDirection::In).unwrap();
998 assert_eq!(in_scores[&a], 0);
999 assert_eq!(in_scores[&b], 1);
1000 assert_eq!(in_scores[&c], 1);
1001
1002 let both_scores = g.degree_centrality(DegreeDirection::Both).unwrap();
1004 assert_eq!(both_scores[&a], 1);
1005 assert_eq!(both_scores[&b], 2);
1006 assert_eq!(both_scores[&c], 1);
1007 }
1008
1009 #[test]
1010 fn degree_centrality_disconnected() {
1011 let (_dir, g) = open_tmp();
1012 let a = g.add_node("N", &json!({})).unwrap();
1014 let b = g.add_node("N", &json!({})).unwrap();
1015 g.add_edge(a, b, "E", &json!({})).unwrap();
1016
1017 let c = g.add_node("N", &json!({})).unwrap();
1019
1020 g.rebuild_csr().unwrap();
1021
1022 let both_scores = g.degree_centrality(DegreeDirection::Both).unwrap();
1023 assert_eq!(both_scores[&a], 1);
1024 assert_eq!(both_scores[&b], 1);
1025 assert_eq!(both_scores[&c], 0);
1026 }
1027
1028 #[test]
1033 fn maximum_flow_trivial() {
1034 let (_dir, g) = open_tmp();
1035 let a = g.add_node("N", &json!({})).unwrap();
1036 let flow = g.maximum_flow(a, a, "cap").unwrap();
1037 assert_eq!(flow, 0.0);
1038 }
1039
1040 #[test]
1041 fn maximum_flow_single_path_bottleneck() {
1042 let (_dir, g) = open_tmp();
1043 let a = g.add_node("N", &json!({})).unwrap();
1045 let b = g.add_node("N", &json!({})).unwrap();
1046 let c = g.add_node("N", &json!({})).unwrap();
1047
1048 g.add_edge(a, b, "E", &json!({ "capacity": 10.0 })).unwrap();
1049 g.add_edge(b, c, "E", &json!({ "capacity": 5.0 })).unwrap();
1050
1051 g.rebuild_csr().unwrap();
1052
1053 let flow = g.maximum_flow(a, c, "capacity").unwrap();
1054 assert!((flow - 5.0).abs() < 1e-6);
1055 }
1056
1057 #[test]
1058 fn maximum_flow_diamond_parallel() {
1059 let (_dir, g) = open_tmp();
1060 let a = g.add_node("N", &json!({})).unwrap();
1066 let b = g.add_node("N", &json!({})).unwrap();
1067 let c = g.add_node("N", &json!({})).unwrap();
1068 let d = g.add_node("N", &json!({})).unwrap();
1069
1070 g.add_edge(a, b, "E", &json!({ "cap": 10.0 })).unwrap();
1071 g.add_edge(b, d, "E", &json!({ "cap": 10.0 })).unwrap();
1072 g.add_edge(a, c, "E", &json!({ "cap": 5.0 })).unwrap();
1073 g.add_edge(c, d, "E", &json!({ "cap": 5.0 })).unwrap();
1074
1075 g.rebuild_csr().unwrap();
1076
1077 let flow = g.maximum_flow(a, d, "cap").unwrap();
1078 assert!((flow - 15.0).abs() < 1e-6);
1079 }
1080
1081 #[test]
1082 fn maximum_flow_redirection() {
1083 let (_dir, g) = open_tmp();
1084 let a = g.add_node("N", &json!({})).unwrap();
1093 let b = g.add_node("N", &json!({})).unwrap();
1094 let c = g.add_node("N", &json!({})).unwrap();
1095 let d = g.add_node("N", &json!({})).unwrap();
1096
1097 g.add_edge(a, b, "E", &json!({ "cap": 3.0 })).unwrap();
1098 g.add_edge(a, c, "E", &json!({ "cap": 2.0 })).unwrap();
1099 g.add_edge(b, c, "E", &json!({ "cap": 1.0 })).unwrap();
1100 g.add_edge(b, d, "E", &json!({ "cap": 2.0 })).unwrap();
1101 g.add_edge(c, d, "E", &json!({ "cap": 3.0 })).unwrap();
1102
1103 g.rebuild_csr().unwrap();
1104
1105 let flow = g.maximum_flow(a, d, "cap").unwrap();
1106 assert!((flow - 5.0).abs() < 1e-6);
1107 }
1108
1109 #[test]
1110 fn maximum_flow_disconnected() {
1111 let (_dir, g) = open_tmp();
1112 let a = g.add_node("N", &json!({})).unwrap();
1113 let b = g.add_node("N", &json!({})).unwrap();
1114 let flow = g.maximum_flow(a, b, "cap").unwrap();
1115 assert_eq!(flow, 0.0);
1116 }
1117
1118 #[test]
1123 fn shortest_path_top_k_trivial() {
1124 let (_dir, g) = open_tmp();
1125 let a = g.add_node("N", &json!({})).unwrap();
1126 let paths = g.shortest_path_top_k(a, a, 3, "weight").unwrap();
1127 assert_eq!(paths.len(), 1);
1128 assert_eq!(paths[0].nodes, vec![a]);
1129 assert_eq!(paths[0].total_weight, 0.0);
1130 }
1131
1132 #[test]
1133 fn shortest_path_top_k_linear_chain() {
1134 let (_dir, g) = open_tmp();
1135 let a = g.add_node("N", &json!({})).unwrap();
1137 let b = g.add_node("N", &json!({})).unwrap();
1138 let c = g.add_node("N", &json!({})).unwrap();
1139
1140 g.add_edge(a, b, "E", &json!({ "cost": 2.0 })).unwrap();
1141 g.add_edge(b, c, "E", &json!({ "cost": 3.0 })).unwrap();
1142
1143 g.rebuild_csr().unwrap();
1144
1145 let paths = g.shortest_path_top_k(a, c, 3, "cost").unwrap();
1146 assert_eq!(paths.len(), 1);
1147 assert_eq!(paths[0].nodes, vec![a, b, c]);
1148 assert!((paths[0].total_weight - 5.0).abs() < 1e-6);
1149 }
1150
1151 #[test]
1152 fn shortest_path_top_k_diamond() {
1153 let (_dir, g) = open_tmp();
1154 let a = g.add_node("N", &json!({})).unwrap();
1160 let b = g.add_node("N", &json!({})).unwrap();
1161 let c = g.add_node("N", &json!({})).unwrap();
1162 let d = g.add_node("N", &json!({})).unwrap();
1163
1164 g.add_edge(a, b, "E", &json!({ "cost": 1.0 })).unwrap();
1165 g.add_edge(b, d, "E", &json!({ "cost": 1.0 })).unwrap();
1166 g.add_edge(a, c, "E", &json!({ "cost": 2.0 })).unwrap();
1167 g.add_edge(c, d, "E", &json!({ "cost": 2.0 })).unwrap();
1168
1169 g.rebuild_csr().unwrap();
1170
1171 let paths = g.shortest_path_top_k(a, d, 3, "cost").unwrap();
1172 assert_eq!(paths.len(), 2);
1173
1174 assert_eq!(paths[0].nodes, vec![a, b, d]);
1176 assert!((paths[0].total_weight - 2.0).abs() < 1e-6);
1177
1178 assert_eq!(paths[1].nodes, vec![a, c, d]);
1180 assert!((paths[1].total_weight - 4.0).abs() < 1e-6);
1181 }
1182
1183 #[test]
1184 fn shortest_path_top_k_cyclic() {
1185 let (_dir, g) = open_tmp();
1186 let a = g.add_node("N", &json!({})).unwrap();
1192 let b = g.add_node("N", &json!({})).unwrap();
1193 let c = g.add_node("N", &json!({})).unwrap();
1194 let d = g.add_node("N", &json!({})).unwrap();
1195
1196 g.add_edge(a, b, "E", &json!({ "cost": 1.0 })).unwrap();
1197 g.add_edge(b, c, "E", &json!({ "cost": 1.0 })).unwrap();
1198 g.add_edge(c, d, "E", &json!({ "cost": 1.0 })).unwrap();
1199 g.add_edge(a, c, "E", &json!({ "cost": 3.0 })).unwrap();
1200 g.add_edge(b, a, "E", &json!({ "cost": 1.0 })).unwrap();
1201
1202 g.rebuild_csr().unwrap();
1203
1204 let paths = g.shortest_path_top_k(a, d, 4, "cost").unwrap();
1205 assert_eq!(paths.len(), 2);
1206
1207 assert_eq!(paths[0].nodes, vec![a, b, c, d]);
1209 assert!((paths[0].total_weight - 3.0).abs() < 1e-6);
1210
1211 assert_eq!(paths[1].nodes, vec![a, c, d]);
1213 assert!((paths[1].total_weight - 4.0).abs() < 1e-6);
1214 }
1215
1216 #[test]
1217 fn shortest_path_top_k_disconnected() {
1218 let (_dir, g) = open_tmp();
1219 let a = g.add_node("N", &json!({})).unwrap();
1220 let b = g.add_node("N", &json!({})).unwrap();
1221 let paths = g.shortest_path_top_k(a, b, 3, "cost").unwrap();
1222 assert!(paths.is_empty());
1223 }
1224
1225 #[test]
1230 fn shortest_path_same_node() {
1231 let (_dir, g) = open_tmp();
1232 let a = g.add_node("N", &json!({})).unwrap();
1233 let path = g.shortest_path(a, a).unwrap();
1234 assert_eq!(path, Some(vec![a]));
1235 }
1236
1237 #[test]
1238 fn shortest_path_direct_edge() {
1239 let (_dir, g) = open_tmp();
1240 let a = g.add_node("N", &json!({})).unwrap();
1241 let b = g.add_node("N", &json!({})).unwrap();
1242 g.add_edge(a, b, "E", &json!({})).unwrap();
1243 g.rebuild_csr().unwrap();
1244 let path = g.shortest_path(a, b).unwrap().unwrap();
1245 assert_eq!(path, vec![a, b]);
1246 }
1247
1248 #[test]
1249 fn shortest_path_multi_hop() {
1250 let (_dir, g) = open_tmp();
1251 let a = g.add_node("N", &json!({})).unwrap();
1252 let b = g.add_node("N", &json!({})).unwrap();
1253 let c = g.add_node("N", &json!({})).unwrap();
1254 g.add_edge(a, b, "E", &json!({})).unwrap();
1255 g.add_edge(b, c, "E", &json!({})).unwrap();
1256 g.rebuild_csr().unwrap();
1257 let path = g.shortest_path(a, c).unwrap().unwrap();
1258 assert_eq!(path, vec![a, b, c]);
1259 }
1260
1261 #[test]
1262 fn shortest_path_returns_shortest_not_any_path() {
1263 let (_dir, g) = open_tmp();
1264 let a = g.add_node("N", &json!({})).unwrap();
1265 let b = g.add_node("N", &json!({})).unwrap();
1266 let c = g.add_node("N", &json!({})).unwrap();
1267 g.add_edge(a, b, "E", &json!({})).unwrap();
1269 g.add_edge(b, c, "E", &json!({})).unwrap();
1270 g.add_edge(a, c, "E", &json!({})).unwrap();
1271 g.rebuild_csr().unwrap();
1272 let path = g.shortest_path(a, c).unwrap().unwrap();
1273 assert_eq!(path.len(), 2); }
1275
1276 #[test]
1277 fn shortest_path_unreachable_returns_none() {
1278 let (_dir, g) = open_tmp();
1279 let a = g.add_node("N", &json!({})).unwrap();
1280 let b = g.add_node("N", &json!({})).unwrap();
1281 g.rebuild_csr().unwrap();
1282 assert!(g.shortest_path(a, b).unwrap().is_none());
1283 }
1284
1285 #[test]
1290 fn page_rank_all_nodes_present() {
1291 let (_dir, g) = open_tmp();
1292 let a = g.add_node("N", &json!({})).unwrap();
1293 let b = g.add_node("N", &json!({})).unwrap();
1294 let c = g.add_node("N", &json!({})).unwrap();
1295 g.add_edge(a, b, "E", &json!({})).unwrap();
1296 g.add_edge(b, c, "E", &json!({})).unwrap();
1297 g.add_edge(c, a, "E", &json!({})).unwrap();
1298 g.rebuild_csr().unwrap();
1299
1300 let pr = g.page_rank(20, 0.85).unwrap();
1301 assert_eq!(pr.len(), 3);
1302 assert!(pr.contains_key(&a));
1303 for &score in pr.values() {
1305 assert!((score - 1.0 / 3.0).abs() < 1e-3, "rank = {score}");
1306 }
1307 }
1308
1309 #[test]
1310 fn page_rank_empty_graph_returns_empty() {
1311 let (_dir, g) = open_tmp();
1312 g.rebuild_csr().unwrap();
1313 assert!(g.page_rank(10, 0.85).unwrap().is_empty());
1314 }
1315
1316 #[test]
1321 fn connected_components_single_component() {
1322 let (_dir, g) = open_tmp();
1323 let a = g.add_node("N", &json!({})).unwrap();
1324 let b = g.add_node("N", &json!({})).unwrap();
1325 let c = g.add_node("N", &json!({})).unwrap();
1326 g.add_edge(a, b, "E", &json!({})).unwrap();
1327 g.add_edge(b, c, "E", &json!({})).unwrap();
1328
1329 let cc = g.connected_components().unwrap();
1330 assert_eq!(cc.len(), 3);
1331 assert_eq!(cc[&a], cc[&b]);
1332 assert_eq!(cc[&b], cc[&c]);
1333 }
1334
1335 #[test]
1336 fn connected_components_two_components() {
1337 let (_dir, g) = open_tmp();
1338 let a = g.add_node("N", &json!({})).unwrap();
1339 let b = g.add_node("N", &json!({})).unwrap();
1340 let c = g.add_node("N", &json!({})).unwrap();
1341 let d = g.add_node("N", &json!({})).unwrap();
1342 g.add_edge(a, b, "E", &json!({})).unwrap();
1343 g.add_edge(c, d, "E", &json!({})).unwrap();
1344
1345 let cc = g.connected_components().unwrap();
1346 assert_eq!(cc.len(), 4);
1347 assert_eq!(cc[&a], cc[&b]);
1348 assert_eq!(cc[&c], cc[&d]);
1349 assert_ne!(cc[&a], cc[&c]);
1350 }
1351
1352 #[test]
1353 fn connected_components_graphblas_path_node_zero_connected() {
1354 let (_dir, g) = open_tmp();
1358 let a = g.add_node("N", &json!({})).unwrap();
1359 let b = g.add_node("N", &json!({})).unwrap();
1360 let c = g.add_node("N", &json!({})).unwrap();
1361 g.add_edge(a, b, "E", &json!({})).unwrap();
1362 g.add_edge(b, c, "E", &json!({})).unwrap();
1363 g.rebuild_csr().unwrap();
1364
1365 let cc = g.connected_components().unwrap();
1366 let distinct: std::collections::HashSet<u64> = cc.values().copied().collect();
1367 assert_eq!(distinct.len(), 1, "all three nodes form one component");
1368 assert_eq!(cc[&a], cc[&b]);
1369 assert_eq!(cc[&b], cc[&c]);
1370 }
1371
1372 #[test]
1373 fn connected_components_graphblas_path_keeps_components_separate() {
1374 let (_dir, g) = open_tmp();
1379 let a = g.add_node("N", &json!({})).unwrap();
1380 let b = g.add_node("N", &json!({})).unwrap();
1381 let c = g.add_node("N", &json!({})).unwrap();
1382 let d = g.add_node("N", &json!({})).unwrap();
1383 g.add_edge(a, b, "E", &json!({})).unwrap();
1384 g.add_edge(c, d, "E", &json!({})).unwrap();
1385 g.rebuild_csr().unwrap();
1386
1387 let cc = g.connected_components().unwrap();
1388 assert_eq!(cc.len(), 4);
1389 assert_eq!(cc[&a], cc[&b]);
1390 assert_eq!(cc[&c], cc[&d]);
1391 assert_ne!(cc[&a], cc[&c], "disjoint edges must be separate components");
1392 }
1393
1394 #[test]
1395 fn connected_components_weakly_connected_via_reverse_edge() {
1396 let (_dir, g) = open_tmp();
1397 let a = g.add_node("N", &json!({})).unwrap();
1399 let b = g.add_node("N", &json!({})).unwrap();
1400 let c = g.add_node("N", &json!({})).unwrap();
1401 g.add_edge(a, b, "E", &json!({})).unwrap();
1402 g.add_edge(c, b, "E", &json!({})).unwrap();
1404
1405 let cc = g.connected_components().unwrap();
1406 assert_eq!(cc[&a], cc[&b]);
1407 assert_eq!(cc[&b], cc[&c]);
1408 }
1409
1410 #[test]
1411 fn label_name_roundtrip() {
1412 let (_dir, g) = open_tmp();
1413 let id = g.add_node("Person", &json!({})).unwrap();
1414 let rec = g.get_node(id).unwrap().unwrap();
1415 assert_eq!(
1416 g.label_name(rec.primary_label().unwrap())
1417 .unwrap()
1418 .as_deref(),
1419 Some("Person")
1420 );
1421 }
1422
1423 #[test]
1424 fn type_name_roundtrip() {
1425 let (_dir, g) = open_tmp();
1426 let a = g.add_node("N", &json!({})).unwrap();
1427 let b = g.add_node("N", &json!({})).unwrap();
1428 let eid = g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1429 let rec = g.get_edge(eid).unwrap().unwrap();
1430 assert_eq!(
1431 g.type_name(rec.edge_type).unwrap().as_deref(),
1432 Some("KNOWS")
1433 );
1434 }
1435
1436 #[test]
1437 fn label_name_unknown_id_returns_none() {
1438 let (_dir, g) = open_tmp();
1439 assert!(g.label_name(9999).unwrap().is_none());
1440 }
1441
1442 #[test]
1443 fn graphblas_bfs_page_rank_sssp() {
1444 let (_dir, g) = open_tmp();
1445 let a = g.add_node("Person", &json!({})).unwrap();
1446 let b = g.add_node("Person", &json!({})).unwrap();
1447 let c = g.add_node("Person", &json!({})).unwrap();
1448 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1449 g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
1450 g.rebuild_csr().unwrap();
1451
1452 let bfs1 = g.bfs_graphblas(a, 1).unwrap();
1454 assert!(bfs1.contains(&a));
1455 assert!(bfs1.contains(&b));
1456 assert!(!bfs1.contains(&c));
1457
1458 let bfs2 = g.bfs_graphblas(a, 2).unwrap();
1460 assert!(bfs2.contains(&a));
1461 assert!(bfs2.contains(&b));
1462 assert!(bfs2.contains(&c));
1463
1464 let pr = g.page_rank_graphblas(10, 0.85).unwrap();
1466 assert_eq!(pr.len(), 3);
1467 for &id in &[a, b, c] {
1468 assert!(pr.contains_key(&id));
1469 assert!(*pr.get(&id).unwrap() > 0.0);
1470 }
1471
1472 let path = g
1474 .shortest_path_graphblas(a, c)
1475 .unwrap()
1476 .expect("path a→c must exist");
1477 assert_eq!(path, vec![a, b, c]);
1478
1479 let trivial = g.shortest_path_graphblas(a, a).unwrap().unwrap();
1481 assert_eq!(trivial, vec![a]);
1482
1483 assert!(g.shortest_path_graphblas(c, a).unwrap().is_none());
1485 }
1486}
1487
1488#[cfg(test)]
1489mod prop_tests {
1490 use proptest::prelude::*;
1491 use tempfile::TempDir;
1492
1493 use super::*;
1494
1495 #[test]
1503 fn node_ids_are_monotonically_increasing() {
1504 let _dir = TempDir::new().unwrap();
1505 let g = Graph::open(_dir.path(), 1).unwrap();
1506 let config = ProptestConfig {
1507 fork: false,
1508 cases: 200,
1509 ..Default::default()
1510 };
1511 proptest!(config, |(label in "[A-Z]{1,4}")| {
1512 let a = g.add_node(&label, &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
1513 let b = g.add_node(&label, &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
1514 prop_assert!(a < b);
1515 });
1516 }
1517
1518 #[test]
1520 fn edge_ids_are_monotonically_increasing() {
1521 let _dir = TempDir::new().unwrap();
1522 let g = Graph::open(_dir.path(), 1).unwrap();
1523 let src = g.add_node("N", &()).unwrap();
1524 let dst = g.add_node("N", &()).unwrap();
1525 let config = ProptestConfig {
1526 fork: false,
1527 cases: 200,
1528 ..Default::default()
1529 };
1530 proptest!(config, |(_dummy in 0u8..=255)| {
1531 let a = g.add_edge(src, dst, "E", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
1532 let b = g.add_edge(src, dst, "E", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
1533 prop_assert!(a < b);
1534 });
1535 }
1536
1537 #[test]
1539 fn node_ids_are_unique() {
1540 let _dir = TempDir::new().unwrap();
1541 let g = Graph::open(_dir.path(), 1).unwrap();
1542 let config = ProptestConfig {
1543 fork: false,
1544 cases: 200,
1545 ..Default::default()
1546 };
1547 proptest!(config, |(_dummy in 0u8..=255)| {
1548 let a = g.add_node("N", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
1549 let b = g.add_node("N", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
1550 prop_assert_ne!(a, b);
1551 });
1552 }
1553
1554 #[test]
1557 fn adjacency_round_trip() {
1558 let _dir = TempDir::new().unwrap();
1559 let g = Graph::open(_dir.path(), 1).unwrap();
1560 let src = g.add_node("N", &()).unwrap();
1561 let dst = g.add_node("N", &()).unwrap();
1562 let config = ProptestConfig {
1563 fork: false,
1564 cases: 200,
1565 ..Default::default()
1566 };
1567 proptest!(config, |(_dummy in 0u8..=255)| {
1568 let before_out = g.out_neighbors(src).unwrap().len();
1569 let before_in = g.in_neighbors(dst).unwrap().len();
1570 let eid = g.add_edge(src, dst, "E", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
1571 let out: Vec<_> = g.out_neighbors(src).unwrap();
1572 let inc: Vec<_> = g.in_neighbors(dst).unwrap();
1573 prop_assert_eq!(out.len(), before_out + 1);
1574 prop_assert_eq!(inc.len(), before_in + 1);
1575 prop_assert!(out.iter().any(|ne| ne.edge == eid));
1576 prop_assert!(inc.iter().any(|ne| ne.edge == eid));
1577 });
1578 }
1579
1580 #[test]
1583 fn label_index_exact_membership() {
1584 let _dir = TempDir::new().unwrap();
1585 let g = Graph::open(_dir.path(), 1).unwrap();
1586 let config = ProptestConfig {
1587 fork: false,
1588 cases: 200,
1589 ..Default::default()
1590 };
1591 proptest!(config, |(insert_other in proptest::bool::ANY)| {
1592 let before = g.nodes_by_label("Target").unwrap().len();
1593 let id = g.add_node("Target", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
1594 if insert_other {
1595 g.add_node("Other", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
1596 }
1597 let after = g.nodes_by_label("Target").unwrap();
1598 prop_assert_eq!(after.len(), before + 1);
1599 prop_assert!(after.contains(&id));
1600 });
1601 }
1602
1603 #[test]
1606 fn type_index_exact_membership() {
1607 let _dir = TempDir::new().unwrap();
1608 let g = Graph::open(_dir.path(), 1).unwrap();
1609 let a = g.add_node("N", &()).unwrap();
1610 let b = g.add_node("N", &()).unwrap();
1611 let config = ProptestConfig {
1612 fork: false,
1613 cases: 200,
1614 ..Default::default()
1615 };
1616 proptest!(config, |(insert_other in proptest::bool::ANY)| {
1617 let before = g.edges_by_type("Target").unwrap().len();
1618 let eid = g.add_edge(a, b, "Target", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
1619 if insert_other {
1620 g.add_edge(a, b, "Other", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
1621 }
1622 let after = g.edges_by_type("Target").unwrap();
1623 prop_assert_eq!(after.len(), before + 1);
1624 prop_assert!(after.contains(&eid));
1625 });
1626 }
1627}
1628
1629#[cfg(test)]
1630mod differential_tests {
1631 use std::collections::HashMap;
1632
1633 use proptest::prelude::*;
1634 use serde_json::json;
1635 use tempfile::TempDir;
1636
1637 use super::*;
1638
1639 fn open_tmp() -> (TempDir, Graph) {
1640 let dir = TempDir::new().unwrap();
1641 let g = Graph::open(dir.path(), 1).unwrap();
1642 (dir, g)
1643 }
1644
1645 fn reference_wcc(n: usize, edges: &[(usize, usize)]) -> Vec<usize> {
1649 fn find(parent: &mut [usize], mut x: usize) -> usize {
1650 while parent[x] != x {
1651 parent[x] = parent[parent[x]];
1652 x = parent[x];
1653 }
1654 x
1655 }
1656 let mut parent: Vec<usize> = (0..n).collect();
1657 for &(a, b) in edges {
1658 let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
1659 if ra != rb {
1660 parent[ra] = rb;
1661 }
1662 }
1663 (0..n).map(|i| find(&mut parent, i)).collect()
1664 }
1665
1666 fn same_partition<A: Eq, B: Eq>(xs: &[A], ys: &[B]) -> bool {
1671 let n = xs.len();
1672 (0..n).all(|i| (0..n).all(|j| (xs[i] == xs[j]) == (ys[i] == ys[j])))
1673 }
1674
1675 proptest! {
1676 #![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })]
1677
1678 #[test]
1679 fn connected_components_matches_union_find(
1680 n in 1usize..12,
1681 edges in prop::collection::vec((0usize..12, 0usize..12), 0..24),
1682 ) {
1683 let edges: Vec<(usize, usize)> =
1685 edges.into_iter().filter(|&(a, b)| a < n && b < n).collect();
1686 let (_dir, g) = open_tmp();
1687 let ids: Vec<NodeId> = (0..n)
1688 .map(|_| g.add_node("N", &json!({})).unwrap())
1689 .collect();
1690 for &(a, b) in &edges {
1691 g.add_edge(ids[a], ids[b], "E", &json!({})).unwrap();
1692 }
1693 g.rebuild_csr().unwrap();
1694
1695 let got: HashMap<NodeId, u64> = g.connected_components().unwrap();
1696 let impl_labels: Vec<u64> = ids.iter().map(|id| got[id]).collect();
1697 let ref_labels = reference_wcc(n, &edges);
1698
1699 prop_assert!(
1700 same_partition(&impl_labels, &ref_labels),
1701 "WCC partition mismatch: impl={:?} ref={:?} edges={:?}",
1702 impl_labels, ref_labels, edges
1703 );
1704 }
1705 }
1706}