pub(crate) mod columns;
pub(crate) mod csr;
mod error;
mod graph;
pub(crate) mod histogram;
pub(crate) mod matrices;
mod schema;
pub(crate) mod storage;
pub use error::Error;
pub use graph::{DegreeDirection, Graph, ReadTxn, TriangleCountSpec, WriteTxn};
pub use schema::{
DirectedNeighborEntry, EdgeId, EdgeRecord, LabelId, Language, NeighborEntry, NodeId,
NodeRecord, PropValue, TypeId, WeightedPath,
};
#[cfg(test)]
mod tests {
use serde_json::json;
use tempfile::TempDir;
use super::*;
fn open_tmp() -> (TempDir, Graph) {
let dir = TempDir::new().unwrap();
let g = Graph::open(dir.path(), 1).unwrap();
(dir, g)
}
#[test]
fn insert_and_read_node() {
let (_dir, g) = open_tmp();
let id = g
.add_node("Person", &json!({ "name": "Alice", "age": 30 }))
.unwrap();
let record = g.get_node(id).unwrap().expect("node should exist");
let props: serde_json::Value = rmp_serde::from_slice(&record.props).unwrap();
assert_eq!(props["name"], "Alice");
assert_eq!(props["age"], 30);
}
#[test]
fn insert_and_read_edge() {
let (_dir, g) = open_tmp();
let alice = g.add_node("Person", &json!({ "name": "Alice" })).unwrap();
let bob = g.add_node("Person", &json!({ "name": "Bob" })).unwrap();
let eid = g
.add_edge(alice, bob, "KNOWS", &json!({ "since": 2020 }))
.unwrap();
let edge = g.get_edge(eid).unwrap().expect("edge should exist");
assert_eq!(edge.src, alice);
assert_eq!(edge.dst, bob);
let neighbors = g.out_neighbors(alice).unwrap();
assert_eq!(neighbors.len(), 1);
assert_eq!(neighbors[0].node, bob);
assert_eq!(neighbors[0].edge, eid);
}
#[test]
fn multiple_nodes_get_unique_ids() {
let (_dir, g) = open_tmp();
let ids: Vec<_> = (0..10)
.map(|i| g.add_node("Node", &json!({ "i": i })).unwrap())
.collect();
let unique: std::collections::HashSet<_> = ids.iter().collect();
assert_eq!(unique.len(), 10);
}
#[test]
fn nodes_by_label_returns_correct_set() {
let (_dir, g) = open_tmp();
let a = g.add_node("Person", &json!({})).unwrap();
let b = g.add_node("Person", &json!({})).unwrap();
let _c = g.add_node("Company", &json!({})).unwrap();
let mut persons = g.nodes_by_label("Person").unwrap();
persons.sort_unstable();
assert_eq!(persons, vec![a, b]);
let companies = g.nodes_by_label("Company").unwrap();
assert_eq!(companies.len(), 1);
let missing = g.nodes_by_label("Robot").unwrap();
assert!(missing.is_empty());
}
#[test]
fn edges_by_type_returns_correct_set() {
let (_dir, g) = open_tmp();
let alice = g.add_node("Person", &json!({})).unwrap();
let bob = g.add_node("Person", &json!({})).unwrap();
let corp = g.add_node("Company", &json!({})).unwrap();
let e1 = g.add_edge(alice, bob, "KNOWS", &json!({})).unwrap();
let e2 = g.add_edge(alice, corp, "WORKS_AT", &json!({})).unwrap();
let e3 = g.add_edge(bob, corp, "WORKS_AT", &json!({})).unwrap();
let knows = g.edges_by_type("KNOWS").unwrap();
assert_eq!(knows, vec![e1]);
let mut works = g.edges_by_type("WORKS_AT").unwrap();
works.sort_unstable();
assert_eq!(works, vec![e2, e3]);
let missing = g.edges_by_type("FOLLOWS").unwrap();
assert!(missing.is_empty());
}
#[test]
fn label_idx_consistent_across_reopen() {
let dir = TempDir::new().unwrap();
let id = {
let g = Graph::open(dir.path(), 1).unwrap();
g.add_node("Person", &json!({})).unwrap()
};
let g2 = Graph::open(dir.path(), 1).unwrap();
let persons = g2.nodes_by_label("Person").unwrap();
assert_eq!(persons, vec![id]);
}
#[test]
fn csr_hot_path_returns_correct_neighbors() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let e1 = g.add_edge(a, b, "E", &json!({})).unwrap();
let e2 = g.add_edge(a, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let mut out = g.out_neighbors(a).unwrap();
out.sort_unstable_by_key(|ne| ne.node);
assert_eq!(out.len(), 2);
let edge_ids: Vec<_> = out.iter().map(|ne| ne.edge).collect();
assert!(edge_ids.contains(&e1));
assert!(edge_ids.contains(&e2));
}
#[test]
fn csr_fallback_to_lmdb_for_new_nodes() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let eid = g.add_edge(a, b, "E", &json!({})).unwrap();
let out = g.out_neighbors(a).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].edge, eid);
}
#[test]
fn csr_snapshot_rebuilds_correctly_on_reopen() {
let dir = TempDir::new().unwrap();
let (a, b, eid) = {
let g = Graph::open(dir.path(), 1).unwrap();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let eid = g.add_edge(a, b, "E", &json!({})).unwrap();
(a, b, eid)
};
let g2 = Graph::open(dir.path(), 1).unwrap();
let out = g2.out_neighbors(a).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].node, b);
assert_eq!(out[0].edge, eid);
}
#[test]
fn bfs_hops_zero_returns_start_only() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
let result = g.bfs(a, 0).unwrap();
assert_eq!(result, vec![a]);
}
#[test]
fn bfs_linear_chain_respects_hop_limit() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(c, d, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let mut hop1 = g.bfs(a, 1).unwrap();
hop1.sort_unstable();
assert_eq!(hop1, vec![a, b]);
let mut hop2 = g.bfs(a, 2).unwrap();
hop2.sort_unstable();
assert_eq!(hop2, vec![a, b, c]);
let mut hop3 = g.bfs(a, 3).unwrap();
hop3.sort_unstable();
assert_eq!(hop3, vec![a, b, c, d]);
}
#[test]
fn bfs_does_not_revisit_nodes_in_a_cycle() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(c, a, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let mut result = g.bfs(a, 10).unwrap();
result.sort_unstable();
assert_eq!(result, vec![a, b, c]);
}
#[test]
fn bfs_isolated_node_returns_only_itself() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let _b = g.add_node("N", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let result = g.bfs(a, 5).unwrap();
assert_eq!(result, vec![a]);
}
#[test]
fn bfs_works_via_dynamic_matrix_materialization_without_manual_rebuild() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
let mut result = g.bfs(a, 2).unwrap();
result.sort_unstable();
assert_eq!(result, vec![a, b, c]);
}
#[test]
fn dfs_hops_zero_returns_start_only() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
let result = g.dfs(a, 0).unwrap();
assert_eq!(result, vec![a]);
}
#[test]
fn dfs_linear_chain_pre_order_and_limit() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(c, d, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let hop1 = g.dfs(a, 1).unwrap();
assert_eq!(hop1, vec![a, b]);
let hop2 = g.dfs(a, 2).unwrap();
assert_eq!(hop2, vec![a, b, c]);
let hop3 = g.dfs(a, 3).unwrap();
assert_eq!(hop3, vec![a, b, c, d]);
}
#[test]
fn dfs_does_not_loop_on_cycle() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(c, a, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let result = g.dfs(a, 10).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0], a);
assert_eq!(result[1], b);
assert_eq!(result[2], c);
}
#[test]
fn cycle_detection() {
let (_dir, g) = open_tmp();
assert!(!g.detect_cycle().unwrap());
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
assert!(!g.detect_cycle().unwrap());
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(d, d, "E", &json!({})).unwrap();
assert!(g.detect_cycle().unwrap());
}
#[test]
fn cycle_detection_multi_hop() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(c, a, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
assert!(g.detect_cycle().unwrap());
}
#[test]
fn all_neighbors_retrieval() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let e1 = g.add_edge(a, b, "OUT", &json!({})).unwrap();
let e2 = g.add_edge(c, a, "IN", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let mut neighbors = g.all_neighbors(a).unwrap();
neighbors.sort_by_key(|ne| ne.node);
assert_eq!(neighbors.len(), 2);
assert_eq!(neighbors[0].node, b);
assert_eq!(neighbors[0].edge, e1);
assert!(neighbors[0].outgoing);
assert_eq!(neighbors[1].node, c);
assert_eq!(neighbors[1].edge, e2);
assert!(!neighbors[1].outgoing); }
#[test]
fn all_paths_linear_and_multiple() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let paths = g.all_paths(a, c).unwrap();
assert_eq!(paths, vec![vec![a, b, c]]);
g.add_edge(a, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let mut paths = g.all_paths(a, c).unwrap();
paths.sort_by_key(|p| p.len());
assert_eq!(paths.len(), 2);
assert_eq!(paths[0], vec![a, c]);
assert_eq!(paths[1], vec![a, b, c]);
}
#[test]
fn all_paths_cyclic_avoids_infinite_loop() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(b, a, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let paths = g.all_paths(a, c).unwrap();
assert_eq!(paths, vec![vec![a, b, c]]);
}
#[test]
fn all_shortest_paths_multiple() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, d, "E", &json!({})).unwrap();
g.add_edge(a, c, "E", &json!({})).unwrap();
g.add_edge(c, d, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let mut paths = g.all_shortest_paths(a, d).unwrap();
paths.sort();
let mut expected = vec![vec![a, b, d], vec![a, c, d]];
expected.sort();
assert_eq!(paths, expected);
}
#[test]
fn all_shortest_paths_unreachable_returns_empty() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
assert!(g.all_shortest_paths(a, b).unwrap().is_empty());
}
#[test]
fn longest_path_selection() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(a, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let path = g.longest_path(a, c).unwrap().unwrap();
assert_eq!(path, vec![a, b, c]);
}
#[test]
fn longest_path_unreachable_returns_none() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
assert!(g.longest_path(a, b).unwrap().is_none());
}
#[test]
fn dijkstra_shortest_path_same_node() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let wp = g.shortest_path_dijkstra(a, a).unwrap().unwrap();
assert_eq!(wp.nodes, vec![a]);
assert_eq!(wp.total_weight, 0.0);
}
#[test]
fn dijkstra_shortest_path_linear_and_weighted_decision() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({ "cost": 1.5 })).unwrap();
g.add_edge(b, c, "E", &json!({ "cost": 2.0 })).unwrap();
g.add_edge(a, c, "E", &json!({ "cost": 10.0 })).unwrap();
g.rebuild_csr().unwrap();
let wp = g.shortest_path_dijkstra(a, c).unwrap().unwrap();
assert_eq!(wp.nodes, vec![a, b, c]);
assert_eq!(wp.total_weight, 3.5);
}
#[test]
fn dijkstra_shortest_path_defaults_to_one() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({ "cost": "invalid" }))
.unwrap();
g.rebuild_csr().unwrap();
let wp = g.shortest_path_dijkstra(a, c).unwrap().unwrap();
assert_eq!(wp.nodes, vec![a, b, c]);
assert_eq!(wp.total_weight, 2.0);
}
#[test]
fn dijkstra_shortest_path_unreachable_returns_none() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
assert!(g.shortest_path_dijkstra(a, b).unwrap().is_none());
}
#[test]
fn spanning_forest_empty() {
let (_dir, g) = open_tmp();
let forest = g.spanning_forest("weight", false).unwrap();
assert!(forest.is_empty());
}
#[test]
fn spanning_forest_min_max_cyclic() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let e_ab = g.add_edge(a, b, "E", &json!({ "cost": 1.0 })).unwrap();
let e_bc = g.add_edge(b, c, "E", &json!({ "cost": 2.0 })).unwrap();
let e_ac = g.add_edge(a, c, "E", &json!({ "cost": 3.0 })).unwrap();
let mut min_forest = g.spanning_forest("cost", false).unwrap();
min_forest.sort();
let mut expected_min = vec![e_ab, e_bc];
expected_min.sort();
assert_eq!(min_forest, expected_min);
let mut max_forest = g.spanning_forest("cost", true).unwrap();
max_forest.sort();
let mut expected_max = vec![e_bc, e_ac];
expected_max.sort();
assert_eq!(max_forest, expected_max);
}
#[test]
fn spanning_forest_disconnected() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let e_ab = g.add_edge(a, b, "E", &json!({ "cost": 5.0 })).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
let e_cd = g.add_edge(c, d, "E", &json!({ "cost": 10.0 })).unwrap();
g.rebuild_csr().unwrap();
let mut forest = g.spanning_forest("cost", false).unwrap();
forest.sort();
let mut expected = vec![e_ab, e_cd];
expected.sort();
assert_eq!(forest, expected);
}
#[test]
fn label_propagation_empty() {
let (_dir, g) = open_tmp();
let labels = g.label_propagation(10).unwrap();
assert!(labels.is_empty());
}
#[test]
fn label_propagation_singletons() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let labels = g.label_propagation(10).unwrap();
assert_eq!(labels.len(), 2);
assert_eq!(labels[&a], a);
assert_eq!(labels[&b], b);
}
#[test]
fn label_propagation_cliques() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(c, a, "E", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
let e = g.add_node("N", &json!({})).unwrap();
let f = g.add_node("N", &json!({})).unwrap();
g.add_edge(d, e, "E", &json!({})).unwrap();
g.add_edge(e, f, "E", &json!({})).unwrap();
g.add_edge(f, d, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let labels = g.label_propagation(100).unwrap();
assert_eq!(labels.len(), 6);
let comm1 = labels[&a];
assert_eq!(labels[&b], comm1);
assert_eq!(labels[&c], comm1);
let comm2 = labels[&d];
assert_eq!(labels[&e], comm2);
assert_eq!(labels[&f], comm2);
assert_ne!(comm1, comm2);
}
#[test]
fn harmonic_centrality_empty() {
let (_dir, g) = open_tmp();
let scores = g.harmonic_centrality().unwrap();
assert!(scores.is_empty());
}
#[test]
fn harmonic_centrality_singletons() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let scores = g.harmonic_centrality().unwrap();
assert_eq!(scores.len(), 2);
assert_eq!(scores[&a], 0.0);
assert_eq!(scores[&b], 0.0);
}
#[test]
fn harmonic_centrality_linear_chain() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let scores = g.harmonic_centrality().unwrap();
assert_eq!(scores.len(), 3);
assert!((scores[&a] - 1.5).abs() < 1e-6);
assert!((scores[&b] - 1.0).abs() < 1e-6);
assert!((scores[&c] - 0.0).abs() < 1e-6);
}
#[test]
fn harmonic_centrality_triangle_clique() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(c, a, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let scores = g.harmonic_centrality().unwrap();
assert_eq!(scores.len(), 3);
for &score in scores.values() {
assert!((score - 1.5).abs() < 1e-6);
}
}
#[test]
fn harmonic_centrality_disconnected() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let scores = g.harmonic_centrality().unwrap();
assert_eq!(scores.len(), 3);
assert!((scores[&a] - 1.0).abs() < 1e-6);
assert!((scores[&b] - 0.0).abs() < 1e-6);
assert!((scores[&c] - 0.0).abs() < 1e-6);
}
#[test]
fn betweenness_centrality_empty() {
let (_dir, g) = open_tmp();
let scores = g.betweenness_centrality().unwrap();
assert!(scores.is_empty());
}
#[test]
fn betweenness_centrality_singletons() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let scores = g.betweenness_centrality().unwrap();
assert_eq!(scores.len(), 2);
assert_eq!(scores[&a], 0.0);
assert_eq!(scores[&b], 0.0);
}
#[test]
fn betweenness_centrality_linear_chain() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let scores = g.betweenness_centrality().unwrap();
assert_eq!(scores.len(), 3);
assert_eq!(scores[&a], 0.0);
assert_eq!(scores[&b], 1.0);
assert_eq!(scores[&c], 0.0);
}
#[test]
fn betweenness_centrality_diamond_graph() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(a, c, "E", &json!({})).unwrap();
g.add_edge(b, d, "E", &json!({})).unwrap();
g.add_edge(c, d, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let scores = g.betweenness_centrality().unwrap();
assert_eq!(scores.len(), 4);
assert_eq!(scores[&a], 0.0);
assert_eq!(scores[&d], 0.0);
assert!((scores[&b] - 0.5).abs() < 1e-6);
assert!((scores[&c] - 0.5).abs() < 1e-6);
}
#[test]
fn betweenness_centrality_disconnected() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let scores = g.betweenness_centrality().unwrap();
assert_eq!(scores.len(), 4);
assert_eq!(scores[&a], 0.0);
assert_eq!(scores[&b], 1.0);
assert_eq!(scores[&c], 0.0);
assert_eq!(scores[&d], 0.0);
}
#[test]
fn strongly_connected_components_empty() {
let (_dir, g) = open_tmp();
let comps = g.strongly_connected_components().unwrap();
assert!(comps.is_empty());
}
#[test]
fn strongly_connected_components_singletons() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let comps = g.strongly_connected_components().unwrap();
assert_eq!(comps.len(), 2);
assert_ne!(comps[&a], comps[&b]);
}
#[test]
fn strongly_connected_components_linear_chain() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let comps = g.strongly_connected_components().unwrap();
assert_eq!(comps.len(), 3);
assert_ne!(comps[&a], comps[&b]);
assert_ne!(comps[&b], comps[&c]);
assert_ne!(comps[&a], comps[&c]);
}
#[test]
fn strongly_connected_components_loop() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(c, a, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let comps = g.strongly_connected_components().unwrap();
assert_eq!(comps.len(), 3);
assert_eq!(comps[&a], comps[&b]);
assert_eq!(comps[&b], comps[&c]);
}
#[test]
fn strongly_connected_components_disconnected_clusters() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, a, "E", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(c, d, "E", &json!({})).unwrap();
g.add_edge(d, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let comps = g.strongly_connected_components().unwrap();
assert_eq!(comps.len(), 4);
assert_eq!(comps[&a], comps[&b]);
assert_eq!(comps[&c], comps[&d]);
assert_ne!(comps[&a], comps[&c]);
}
#[test]
fn degree_centrality_empty() {
let (_dir, g) = open_tmp();
let scores = g.degree_centrality(DegreeDirection::Both).unwrap();
assert!(scores.is_empty());
}
#[test]
fn degree_centrality_singletons() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
for dir in &[
DegreeDirection::In,
DegreeDirection::Out,
DegreeDirection::Both,
] {
let scores = g.degree_centrality(*dir).unwrap();
assert_eq!(scores.len(), 2);
assert_eq!(scores[&a], 0);
assert_eq!(scores[&b], 0);
}
}
#[test]
fn degree_centrality_linear_chain() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let out_scores = g.degree_centrality(DegreeDirection::Out).unwrap();
assert_eq!(out_scores[&a], 1);
assert_eq!(out_scores[&b], 1);
assert_eq!(out_scores[&c], 0);
let in_scores = g.degree_centrality(DegreeDirection::In).unwrap();
assert_eq!(in_scores[&a], 0);
assert_eq!(in_scores[&b], 1);
assert_eq!(in_scores[&c], 1);
let both_scores = g.degree_centrality(DegreeDirection::Both).unwrap();
assert_eq!(both_scores[&a], 1);
assert_eq!(both_scores[&b], 2);
assert_eq!(both_scores[&c], 1);
}
#[test]
fn degree_centrality_disconnected() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let both_scores = g.degree_centrality(DegreeDirection::Both).unwrap();
assert_eq!(both_scores[&a], 1);
assert_eq!(both_scores[&b], 1);
assert_eq!(both_scores[&c], 0);
}
#[test]
fn maximum_flow_trivial() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let flow = g.maximum_flow(a, a, "cap").unwrap();
assert_eq!(flow, 0.0);
}
#[test]
fn maximum_flow_single_path_bottleneck() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({ "capacity": 10.0 })).unwrap();
g.add_edge(b, c, "E", &json!({ "capacity": 5.0 })).unwrap();
g.rebuild_csr().unwrap();
let flow = g.maximum_flow(a, c, "capacity").unwrap();
assert!((flow - 5.0).abs() < 1e-6);
}
#[test]
fn maximum_flow_diamond_parallel() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({ "cap": 10.0 })).unwrap();
g.add_edge(b, d, "E", &json!({ "cap": 10.0 })).unwrap();
g.add_edge(a, c, "E", &json!({ "cap": 5.0 })).unwrap();
g.add_edge(c, d, "E", &json!({ "cap": 5.0 })).unwrap();
g.rebuild_csr().unwrap();
let flow = g.maximum_flow(a, d, "cap").unwrap();
assert!((flow - 15.0).abs() < 1e-6);
}
#[test]
fn maximum_flow_redirection() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({ "cap": 3.0 })).unwrap();
g.add_edge(a, c, "E", &json!({ "cap": 2.0 })).unwrap();
g.add_edge(b, c, "E", &json!({ "cap": 1.0 })).unwrap();
g.add_edge(b, d, "E", &json!({ "cap": 2.0 })).unwrap();
g.add_edge(c, d, "E", &json!({ "cap": 3.0 })).unwrap();
g.rebuild_csr().unwrap();
let flow = g.maximum_flow(a, d, "cap").unwrap();
assert!((flow - 5.0).abs() < 1e-6);
}
#[test]
fn maximum_flow_disconnected() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let flow = g.maximum_flow(a, b, "cap").unwrap();
assert_eq!(flow, 0.0);
}
#[test]
fn shortest_path_top_k_trivial() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let paths = g.shortest_path_top_k(a, a, 3, "weight").unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].nodes, vec![a]);
assert_eq!(paths[0].total_weight, 0.0);
}
#[test]
fn shortest_path_top_k_linear_chain() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({ "cost": 2.0 })).unwrap();
g.add_edge(b, c, "E", &json!({ "cost": 3.0 })).unwrap();
g.rebuild_csr().unwrap();
let paths = g.shortest_path_top_k(a, c, 3, "cost").unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].nodes, vec![a, b, c]);
assert!((paths[0].total_weight - 5.0).abs() < 1e-6);
}
#[test]
fn shortest_path_top_k_diamond() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({ "cost": 1.0 })).unwrap();
g.add_edge(b, d, "E", &json!({ "cost": 1.0 })).unwrap();
g.add_edge(a, c, "E", &json!({ "cost": 2.0 })).unwrap();
g.add_edge(c, d, "E", &json!({ "cost": 2.0 })).unwrap();
g.rebuild_csr().unwrap();
let paths = g.shortest_path_top_k(a, d, 3, "cost").unwrap();
assert_eq!(paths.len(), 2);
assert_eq!(paths[0].nodes, vec![a, b, d]);
assert!((paths[0].total_weight - 2.0).abs() < 1e-6);
assert_eq!(paths[1].nodes, vec![a, c, d]);
assert!((paths[1].total_weight - 4.0).abs() < 1e-6);
}
#[test]
fn shortest_path_top_k_cyclic() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({ "cost": 1.0 })).unwrap();
g.add_edge(b, c, "E", &json!({ "cost": 1.0 })).unwrap();
g.add_edge(c, d, "E", &json!({ "cost": 1.0 })).unwrap();
g.add_edge(a, c, "E", &json!({ "cost": 3.0 })).unwrap();
g.add_edge(b, a, "E", &json!({ "cost": 1.0 })).unwrap();
g.rebuild_csr().unwrap();
let paths = g.shortest_path_top_k(a, d, 4, "cost").unwrap();
assert_eq!(paths.len(), 2);
assert_eq!(paths[0].nodes, vec![a, b, c, d]);
assert!((paths[0].total_weight - 3.0).abs() < 1e-6);
assert_eq!(paths[1].nodes, vec![a, c, d]);
assert!((paths[1].total_weight - 4.0).abs() < 1e-6);
}
#[test]
fn shortest_path_top_k_disconnected() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let paths = g.shortest_path_top_k(a, b, 3, "cost").unwrap();
assert!(paths.is_empty());
}
#[test]
fn shortest_path_same_node() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let path = g.shortest_path(a, a).unwrap();
assert_eq!(path, Some(vec![a]));
}
#[test]
fn shortest_path_direct_edge() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let path = g.shortest_path(a, b).unwrap().unwrap();
assert_eq!(path, vec![a, b]);
}
#[test]
fn shortest_path_multi_hop() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let path = g.shortest_path(a, c).unwrap().unwrap();
assert_eq!(path, vec![a, b, c]);
}
#[test]
fn shortest_path_returns_shortest_not_any_path() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(a, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let path = g.shortest_path(a, c).unwrap().unwrap();
assert_eq!(path.len(), 2); }
#[test]
fn shortest_path_unreachable_returns_none() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
g.rebuild_csr().unwrap();
assert!(g.shortest_path(a, b).unwrap().is_none());
}
#[test]
fn page_rank_all_nodes_present() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.add_edge(c, a, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let pr = g.page_rank(20, 0.85).unwrap();
assert_eq!(pr.len(), 3);
assert!(pr.contains_key(&a));
for &score in pr.values() {
assert!((score - 1.0 / 3.0).abs() < 1e-3, "rank = {score}");
}
}
#[test]
fn page_rank_empty_graph_returns_empty() {
let (_dir, g) = open_tmp();
g.rebuild_csr().unwrap();
assert!(g.page_rank(10, 0.85).unwrap().is_empty());
}
#[test]
fn connected_components_single_component() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
let cc = g.connected_components().unwrap();
assert_eq!(cc.len(), 3);
assert_eq!(cc[&a], cc[&b]);
assert_eq!(cc[&b], cc[&c]);
}
#[test]
fn connected_components_two_components() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(c, d, "E", &json!({})).unwrap();
let cc = g.connected_components().unwrap();
assert_eq!(cc.len(), 4);
assert_eq!(cc[&a], cc[&b]);
assert_eq!(cc[&c], cc[&d]);
assert_ne!(cc[&a], cc[&c]);
}
#[test]
fn connected_components_graphblas_path_node_zero_connected() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(b, c, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let cc = g.connected_components().unwrap();
let distinct: std::collections::HashSet<u64> = cc.values().copied().collect();
assert_eq!(distinct.len(), 1, "all three nodes form one component");
assert_eq!(cc[&a], cc[&b]);
assert_eq!(cc[&b], cc[&c]);
}
#[test]
fn connected_components_graphblas_path_keeps_components_separate() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
let d = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(c, d, "E", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let cc = g.connected_components().unwrap();
assert_eq!(cc.len(), 4);
assert_eq!(cc[&a], cc[&b]);
assert_eq!(cc[&c], cc[&d]);
assert_ne!(cc[&a], cc[&c], "disjoint edges must be separate components");
}
#[test]
fn connected_components_weakly_connected_via_reverse_edge() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let c = g.add_node("N", &json!({})).unwrap();
g.add_edge(a, b, "E", &json!({})).unwrap();
g.add_edge(c, b, "E", &json!({})).unwrap();
let cc = g.connected_components().unwrap();
assert_eq!(cc[&a], cc[&b]);
assert_eq!(cc[&b], cc[&c]);
}
#[test]
fn label_name_roundtrip() {
let (_dir, g) = open_tmp();
let id = g.add_node("Person", &json!({})).unwrap();
let rec = g.get_node(id).unwrap().unwrap();
assert_eq!(
g.label_name(rec.primary_label().unwrap())
.unwrap()
.as_deref(),
Some("Person")
);
}
#[test]
fn type_name_roundtrip() {
let (_dir, g) = open_tmp();
let a = g.add_node("N", &json!({})).unwrap();
let b = g.add_node("N", &json!({})).unwrap();
let eid = g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
let rec = g.get_edge(eid).unwrap().unwrap();
assert_eq!(
g.type_name(rec.edge_type).unwrap().as_deref(),
Some("KNOWS")
);
}
#[test]
fn label_name_unknown_id_returns_none() {
let (_dir, g) = open_tmp();
assert!(g.label_name(9999).unwrap().is_none());
}
#[test]
fn graphblas_bfs_page_rank_sssp() {
let (_dir, g) = open_tmp();
let a = g.add_node("Person", &json!({})).unwrap();
let b = g.add_node("Person", &json!({})).unwrap();
let c = g.add_node("Person", &json!({})).unwrap();
g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
g.rebuild_csr().unwrap();
let bfs1 = g.bfs_graphblas(a, 1).unwrap();
assert!(bfs1.contains(&a));
assert!(bfs1.contains(&b));
assert!(!bfs1.contains(&c));
let bfs2 = g.bfs_graphblas(a, 2).unwrap();
assert!(bfs2.contains(&a));
assert!(bfs2.contains(&b));
assert!(bfs2.contains(&c));
let pr = g.page_rank_graphblas(10, 0.85).unwrap();
assert_eq!(pr.len(), 3);
for &id in &[a, b, c] {
assert!(pr.contains_key(&id));
assert!(*pr.get(&id).unwrap() > 0.0);
}
let path = g
.shortest_path_graphblas(a, c)
.unwrap()
.expect("path a→c must exist");
assert_eq!(path, vec![a, b, c]);
let trivial = g.shortest_path_graphblas(a, a).unwrap().unwrap();
assert_eq!(trivial, vec![a]);
assert!(g.shortest_path_graphblas(c, a).unwrap().is_none());
}
}
#[cfg(test)]
mod prop_tests {
use proptest::prelude::*;
use tempfile::TempDir;
use super::*;
#[test]
fn node_ids_are_monotonically_increasing() {
let _dir = TempDir::new().unwrap();
let g = Graph::open(_dir.path(), 1).unwrap();
let config = ProptestConfig {
fork: false,
cases: 200,
..Default::default()
};
proptest!(config, |(label in "[A-Z]{1,4}")| {
let a = g.add_node(&label, &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
let b = g.add_node(&label, &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
prop_assert!(a < b);
});
}
#[test]
fn edge_ids_are_monotonically_increasing() {
let _dir = TempDir::new().unwrap();
let g = Graph::open(_dir.path(), 1).unwrap();
let src = g.add_node("N", &()).unwrap();
let dst = g.add_node("N", &()).unwrap();
let config = ProptestConfig {
fork: false,
cases: 200,
..Default::default()
};
proptest!(config, |(_dummy in 0u8..=255)| {
let a = g.add_edge(src, dst, "E", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
let b = g.add_edge(src, dst, "E", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
prop_assert!(a < b);
});
}
#[test]
fn node_ids_are_unique() {
let _dir = TempDir::new().unwrap();
let g = Graph::open(_dir.path(), 1).unwrap();
let config = ProptestConfig {
fork: false,
cases: 200,
..Default::default()
};
proptest!(config, |(_dummy in 0u8..=255)| {
let a = g.add_node("N", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
let b = g.add_node("N", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
prop_assert_ne!(a, b);
});
}
#[test]
fn adjacency_round_trip() {
let _dir = TempDir::new().unwrap();
let g = Graph::open(_dir.path(), 1).unwrap();
let src = g.add_node("N", &()).unwrap();
let dst = g.add_node("N", &()).unwrap();
let config = ProptestConfig {
fork: false,
cases: 200,
..Default::default()
};
proptest!(config, |(_dummy in 0u8..=255)| {
let before_out = g.out_neighbors(src).unwrap().len();
let before_in = g.in_neighbors(dst).unwrap().len();
let eid = g.add_edge(src, dst, "E", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
let out: Vec<_> = g.out_neighbors(src).unwrap();
let inc: Vec<_> = g.in_neighbors(dst).unwrap();
prop_assert_eq!(out.len(), before_out + 1);
prop_assert_eq!(inc.len(), before_in + 1);
prop_assert!(out.iter().any(|ne| ne.edge == eid));
prop_assert!(inc.iter().any(|ne| ne.edge == eid));
});
}
#[test]
fn label_index_exact_membership() {
let _dir = TempDir::new().unwrap();
let g = Graph::open(_dir.path(), 1).unwrap();
let config = ProptestConfig {
fork: false,
cases: 200,
..Default::default()
};
proptest!(config, |(insert_other in proptest::bool::ANY)| {
let before = g.nodes_by_label("Target").unwrap().len();
let id = g.add_node("Target", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
if insert_other {
g.add_node("Other", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
}
let after = g.nodes_by_label("Target").unwrap();
prop_assert_eq!(after.len(), before + 1);
prop_assert!(after.contains(&id));
});
}
#[test]
fn type_index_exact_membership() {
let _dir = TempDir::new().unwrap();
let g = Graph::open(_dir.path(), 1).unwrap();
let a = g.add_node("N", &()).unwrap();
let b = g.add_node("N", &()).unwrap();
let config = ProptestConfig {
fork: false,
cases: 200,
..Default::default()
};
proptest!(config, |(insert_other in proptest::bool::ANY)| {
let before = g.edges_by_type("Target").unwrap().len();
let eid = g.add_edge(a, b, "Target", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
if insert_other {
g.add_edge(a, b, "Other", &()).map_err(|e| TestCaseError::fail(e.to_string()))?;
}
let after = g.edges_by_type("Target").unwrap();
prop_assert_eq!(after.len(), before + 1);
prop_assert!(after.contains(&eid));
});
}
}
#[cfg(test)]
mod differential_tests {
use std::collections::HashMap;
use proptest::prelude::*;
use serde_json::json;
use tempfile::TempDir;
use super::*;
fn open_tmp() -> (TempDir, Graph) {
let dir = TempDir::new().unwrap();
let g = Graph::open(dir.path(), 1).unwrap();
(dir, g)
}
fn reference_wcc(n: usize, edges: &[(usize, usize)]) -> Vec<usize> {
fn find(parent: &mut [usize], mut x: usize) -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]];
x = parent[x];
}
x
}
let mut parent: Vec<usize> = (0..n).collect();
for &(a, b) in edges {
let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
if ra != rb {
parent[ra] = rb;
}
}
(0..n).map(|i| find(&mut parent, i)).collect()
}
fn same_partition<A: Eq, B: Eq>(xs: &[A], ys: &[B]) -> bool {
let n = xs.len();
(0..n).all(|i| (0..n).all(|j| (xs[i] == xs[j]) == (ys[i] == ys[j])))
}
proptest! {
#![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })]
#[test]
fn connected_components_matches_union_find(
n in 1usize..12,
edges in prop::collection::vec((0usize..12, 0usize..12), 0..24),
) {
let edges: Vec<(usize, usize)> =
edges.into_iter().filter(|&(a, b)| a < n && b < n).collect();
let (_dir, g) = open_tmp();
let ids: Vec<NodeId> = (0..n)
.map(|_| g.add_node("N", &json!({})).unwrap())
.collect();
for &(a, b) in &edges {
g.add_edge(ids[a], ids[b], "E", &json!({})).unwrap();
}
g.rebuild_csr().unwrap();
let got: HashMap<NodeId, u64> = g.connected_components().unwrap();
let impl_labels: Vec<u64> = ids.iter().map(|id| got[id]).collect();
let ref_labels = reference_wcc(n, &edges);
prop_assert!(
same_partition(&impl_labels, &ref_labels),
"WCC partition mismatch: impl={:?} ref={:?} edges={:?}",
impl_labels, ref_labels, edges
);
}
}
}