use super::*;
use crate::datatypes::values::Value;
use crate::graph::schema::{DirGraph, EdgeData, InternedKey, NodeData};
use crate::graph::storage::GraphWrite;
use std::collections::HashMap;
fn build_chain_graph() -> (DirGraph, Vec<petgraph::graph::NodeIndex>) {
let mut graph = DirGraph::new();
let mut indices = Vec::new();
for i in 0..5 {
let node = NodeData::new(
Value::Int64(i),
Value::String(format!("Node_{}", i)),
"Chain".to_string(),
HashMap::new(),
&mut graph.interner,
);
let idx = graph.graph.add_node(node);
graph
.type_indices
.entry_or_default("Chain".to_string())
.push(idx);
indices.push(idx);
}
for i in 0..4 {
let edge = EdgeData::new("NEXT".to_string(), HashMap::new(), &mut graph.interner);
graph.graph.add_edge(indices[i], indices[i + 1], edge);
}
(graph, indices)
}
fn build_triangle_graph() -> (DirGraph, Vec<petgraph::graph::NodeIndex>) {
let mut graph = DirGraph::new();
let mut indices = Vec::new();
for i in 0..3 {
let node = NodeData::new(
Value::Int64(i),
Value::String(format!("N_{}", i)),
"Node".to_string(),
HashMap::new(),
&mut graph.interner,
);
let idx = graph.graph.add_node(node);
graph
.type_indices
.entry_or_default("Node".to_string())
.push(idx);
indices.push(idx);
}
let pairs = [(0, 1), (1, 2), (2, 0)];
for (from, to) in pairs {
let edge = EdgeData::new("LINK".to_string(), HashMap::new(), &mut graph.interner);
graph.graph.add_edge(indices[from], indices[to], edge);
}
(graph, indices)
}
fn build_disconnected_graph() -> (DirGraph, Vec<petgraph::graph::NodeIndex>) {
let mut graph = DirGraph::new();
let mut indices = Vec::new();
for i in 0..4 {
let node = NodeData::new(
Value::Int64(i),
Value::String(format!("N_{}", i)),
"Node".to_string(),
HashMap::new(),
&mut graph.interner,
);
let idx = graph.graph.add_node(node);
graph
.type_indices
.entry_or_default("Node".to_string())
.push(idx);
indices.push(idx);
}
let edge_ab = EdgeData::new("LINK".to_string(), HashMap::new(), &mut graph.interner);
graph.graph.add_edge(indices[0], indices[1], edge_ab);
let edge_cd = EdgeData::new("LINK".to_string(), HashMap::new(), &mut graph.interner);
graph.graph.add_edge(indices[2], indices[3], edge_cd);
(graph, indices)
}
#[test]
fn test_shortest_path_adjacent() {
let (graph, indices) = build_chain_graph();
let result = shortest_path(&graph, indices[0], indices[1], None, None, None);
assert!(result.is_some());
let path = result.unwrap();
assert_eq!(path.cost, 1);
assert_eq!(path.path.len(), 2);
}
#[test]
fn test_shortest_path_multi_hop() {
let (graph, indices) = build_chain_graph();
let result = shortest_path(&graph, indices[0], indices[4], None, None, None);
assert!(result.is_some());
let path = result.unwrap();
assert_eq!(path.cost, 4);
assert_eq!(path.path.len(), 5);
}
#[test]
fn test_shortest_path_same_node() {
let (graph, indices) = build_chain_graph();
let result = shortest_path(&graph, indices[0], indices[0], None, None, None);
assert!(result.is_some());
let path = result.unwrap();
assert_eq!(path.cost, 0);
assert_eq!(path.path.len(), 1);
}
#[test]
fn test_shortest_path_not_found() {
let (graph, indices) = build_disconnected_graph();
let result = shortest_path(&graph, indices[0], indices[2], None, None, None);
assert!(result.is_none());
}
#[test]
fn test_shortest_path_reverse_direction() {
let (graph, indices) = build_chain_graph();
let result = shortest_path(&graph, indices[4], indices[0], None, None, None);
assert!(result.is_some());
assert_eq!(result.unwrap().cost, 4);
}
#[test]
fn test_all_paths_basic() {
let (graph, indices) = build_chain_graph();
let paths = all_paths(&graph, indices[0], indices[2], 5, None, None, None, None);
assert!(!paths.is_empty());
assert!(paths.iter().any(|p| p.len() == 3));
}
#[test]
fn test_all_paths_limited_hops() {
let (graph, indices) = build_chain_graph();
let paths = all_paths(&graph, indices[0], indices[2], 1, None, None, None, None);
assert!(paths.is_empty()); }
#[test]
fn test_all_paths_triangle() {
let (graph, indices) = build_triangle_graph();
let paths = all_paths(&graph, indices[0], indices[2], 3, None, None, None, None);
assert!(!paths.is_empty());
}
#[test]
fn test_all_paths_max_results() {
let (graph, indices) = build_triangle_graph();
let paths = all_paths(&graph, indices[0], indices[2], 3, Some(1), None, None, None);
assert_eq!(paths.len(), 1);
}
#[test]
fn test_all_paths_max_results_none_unlimited() {
let (graph, indices) = build_triangle_graph();
let limited = all_paths(&graph, indices[0], indices[2], 3, Some(1), None, None, None);
let unlimited = all_paths(&graph, indices[0], indices[2], 3, None, None, None, None);
assert!(unlimited.len() >= limited.len());
}
#[test]
fn test_shortest_path_connection_type_filter() {
let mut graph = DirGraph::new();
let mut indices = Vec::new();
for i in 0..3 {
let node = NodeData::new(
Value::Int64(i),
Value::String(format!("Node_{}", i)),
"Test".to_string(),
HashMap::new(),
&mut graph.interner,
);
let idx = graph.graph.add_node(node);
graph
.type_indices
.entry_or_default("Test".to_string())
.push(idx);
indices.push(idx);
}
let edge1 = EdgeData::new("NEXT".to_string(), HashMap::new(), &mut graph.interner);
graph.graph.add_edge(indices[0], indices[1], edge1);
let edge2 = EdgeData::new("NEXT".to_string(), HashMap::new(), &mut graph.interner);
graph.graph.add_edge(indices[1], indices[2], edge2);
let edge3 = EdgeData::new("SKIP".to_string(), HashMap::new(), &mut graph.interner);
graph.graph.add_edge(indices[0], indices[2], edge3);
let result = shortest_path(&graph, indices[0], indices[2], None, None, None);
assert_eq!(result.unwrap().cost, 1);
let next_only = vec!["NEXT".to_string()];
let result = shortest_path(&graph, indices[0], indices[2], Some(&next_only), None, None);
assert_eq!(result.unwrap().cost, 2);
let skip_only = vec!["SKIP".to_string()];
let result = shortest_path(&graph, indices[0], indices[2], Some(&skip_only), None, None);
assert_eq!(result.unwrap().cost, 1);
}
#[test]
fn test_weakly_connected_components_connected() {
let (graph, _) = build_chain_graph();
let components = weakly_connected_components(&graph, None).unwrap();
assert_eq!(components.len(), 1);
assert_eq!(components[0].len(), 5);
}
#[test]
fn test_weakly_connected_components_disconnected() {
let (graph, _) = build_disconnected_graph();
let components = weakly_connected_components(&graph, None).unwrap();
assert_eq!(components.len(), 2);
assert_eq!(components[0].len(), 2);
assert_eq!(components[1].len(), 2);
}
#[test]
fn test_weakly_connected_components_empty() {
let graph = DirGraph::new();
let components = weakly_connected_components(&graph, None).unwrap();
assert!(components.is_empty());
}
fn build_two_type_graph() -> DirGraph {
let mut graph = DirGraph::new();
let mut persons = Vec::new();
for i in 0..4 {
let node = NodeData::new(
Value::Int64(i),
Value::String(format!("P{i}")),
"Person".to_string(),
HashMap::new(),
&mut graph.interner,
);
let idx = graph.graph.add_node(node);
graph
.type_indices
.entry_or_default("Person".to_string())
.push(idx);
persons.push(idx);
}
let company = NodeData::new(
Value::Int64(100),
Value::String("C0".to_string()),
"Company".to_string(),
HashMap::new(),
&mut graph.interner,
);
let c0 = graph.graph.add_node(company);
graph
.type_indices
.entry_or_default("Company".to_string())
.push(c0);
let knows =
|g: &mut DirGraph| EdgeData::new("KNOWS".to_string(), HashMap::new(), &mut g.interner);
let e = knows(&mut graph);
graph.graph.add_edge(persons[0], persons[1], e);
let e = knows(&mut graph);
graph.graph.add_edge(persons[2], persons[3], e);
let e = EdgeData::new("WORKS_AT".to_string(), HashMap::new(), &mut graph.interner);
graph.graph.add_edge(persons[0], c0, e);
let e = EdgeData::new("WORKS_AT".to_string(), HashMap::new(), &mut graph.interner);
graph.graph.add_edge(persons[2], c0, e);
graph
}
#[test]
fn test_wcc_unscoped_bridges_via_other_edge_type() {
let graph = build_two_type_graph();
let components = weakly_connected_components(&graph, None).unwrap();
assert_eq!(components.len(), 1);
assert_eq!(components[0].len(), 5);
}
#[test]
fn test_wcc_scoped_to_node_type_and_relationship() {
let graph = build_two_type_graph();
let node_types = ["Person".to_string()];
let rel_types = [InternedKey::from_str("KNOWS")];
let components =
weakly_connected_components_scoped(&graph, Some(&node_types), Some(&rel_types), None)
.unwrap();
assert_eq!(components.len(), 2);
assert_eq!(components[0].len(), 2);
assert_eq!(components[1].len(), 2);
}
#[test]
fn test_coreness_triangle_all_two() {
let (graph, _) = build_triangle_graph();
let mut scores = coreness_scoped(&graph, None, None, None).unwrap();
scores.sort_by_key(|(n, _)| n.index());
assert_eq!(scores.len(), 3);
assert!(
scores.iter().all(|(_, c)| *c == 2),
"triangle coreness should all be 2: {scores:?}"
);
}
#[test]
fn test_coreness_chain_all_one() {
let (graph, _) = build_chain_graph();
let scores = coreness_scoped(&graph, None, None, None).unwrap();
assert_eq!(scores.len(), 5);
assert!(
scores.iter().all(|(_, c)| *c == 1),
"path coreness should all be 1: {scores:?}"
);
}
#[test]
fn test_clustering_triangle_all_one() {
let (graph, _) = build_triangle_graph();
let scores = clustering_coefficient_scoped(&graph, None, None, None).unwrap();
assert_eq!(scores.len(), 3);
assert!(
scores.iter().all(|(_, c)| (*c - 1.0).abs() < 1e-9),
"triangle clustering coefficient should all be 1.0: {scores:?}"
);
}
#[test]
fn test_clustering_chain_all_zero() {
let (graph, _) = build_chain_graph();
let scores = clustering_coefficient_scoped(&graph, None, None, None).unwrap();
assert!(
scores.iter().all(|(_, c)| *c == 0.0),
"path clustering should all be 0: {scores:?}"
);
}
#[test]
fn test_coreness_scoped_to_relationship() {
let graph = build_two_type_graph();
let node_types = ["Person".to_string()];
let rel_types = [InternedKey::from_str("KNOWS")];
let scores = coreness_scoped(&graph, Some(&node_types), Some(&rel_types), None).unwrap();
assert_eq!(scores.len(), 4); assert!(scores.iter().all(|(_, c)| *c == 1));
}
#[test]
fn test_wcc_scoped_relationship_only_induces_subgraph() {
let graph = build_two_type_graph();
let rel_types = [InternedKey::from_str("KNOWS")];
let components =
weakly_connected_components_scoped(&graph, None, Some(&rel_types), None).unwrap();
assert_eq!(components.len(), 2);
assert_eq!(components.iter().map(|c| c.len()).sum::<usize>(), 4);
}
#[test]
fn test_are_connected_true() {
let (graph, indices) = build_chain_graph();
assert!(are_connected(&graph, indices[0], indices[4]));
}
#[test]
fn test_are_connected_false() {
let (graph, indices) = build_disconnected_graph();
assert!(!are_connected(&graph, indices[0], indices[2]));
}
#[test]
fn test_node_degree() {
let (graph, indices) = build_chain_graph();
assert_eq!(node_degree(&graph, indices[0]), 1);
assert_eq!(node_degree(&graph, indices[2]), 2);
assert_eq!(node_degree(&graph, indices[4]), 1);
}
#[test]
fn test_betweenness_centrality_chain() {
let (graph, indices) = build_chain_graph();
let results = betweenness_centrality(&graph, false, None, None, None, None).unwrap();
assert_eq!(results.len(), 5);
let middle_score = results
.iter()
.find(|r| r.node_idx == indices[2])
.unwrap()
.score;
let end_score = results
.iter()
.find(|r| r.node_idx == indices[0])
.unwrap()
.score;
assert!(middle_score > end_score);
}
#[test]
fn test_betweenness_centrality_with_sampling() {
let (graph, indices) = build_chain_graph();
let results = betweenness_centrality(&graph, false, Some(3), None, None, None).unwrap();
assert_eq!(results.len(), 5);
let middle_score = results
.iter()
.find(|r| r.node_idx == indices[2])
.unwrap()
.score;
assert!(
middle_score > 0.0,
"Middle node should have non-zero betweenness with sampling"
);
}
#[test]
fn test_degree_centrality() {
let (graph, indices) = build_chain_graph();
let results = degree_centrality(&graph, false, None, None, None).unwrap();
assert_eq!(results.len(), 5);
let middle = results.iter().find(|r| r.node_idx == indices[2]).unwrap();
let end = results.iter().find(|r| r.node_idx == indices[0]).unwrap();
assert_eq!(middle.score, 2.0);
assert_eq!(end.score, 1.0);
}
#[test]
fn test_pagerank_basic() {
let (graph, _) = build_triangle_graph();
let results = pagerank(&graph, 0.85, 100, 1e-6, None, None, None).unwrap();
assert_eq!(results.len(), 3);
let scores: Vec<f64> = results.iter().map(|r| r.score).collect();
let diff = (scores[0] - scores[2]).abs();
assert!(
diff < 0.01,
"Triangle nodes should have similar PageRank: {:?}",
scores
);
}
#[test]
fn test_closeness_centrality_chain() {
let (graph, indices) = build_chain_graph();
let results = closeness_centrality(&graph, false, None, None, None, None).unwrap();
assert_eq!(results.len(), 5);
let middle = results
.iter()
.find(|r| r.node_idx == indices[2])
.unwrap()
.score;
let end = results
.iter()
.find(|r| r.node_idx == indices[0])
.unwrap()
.score;
assert!(middle > end);
}
#[test]
fn test_centrality_scope_restricts_nodes_and_edges() {
let (graph, indices) = build_chain_graph();
let scope: std::collections::HashSet<_> =
[indices[1], indices[2], indices[3]].into_iter().collect();
let deg = degree_centrality(&graph, false, None, Some(&scope), None).unwrap();
assert_eq!(deg.len(), 3, "only scoped nodes returned");
let score_of = |idx| deg.iter().find(|r| r.node_idx == idx).unwrap().score;
assert_eq!(score_of(indices[2]), 2.0);
assert_eq!(score_of(indices[1]), 1.0);
assert_eq!(score_of(indices[3]), 1.0);
let pr = pagerank(&graph, 0.85, 100, 1e-6, None, Some(&scope), None).unwrap();
let pr_nodes: std::collections::HashSet<_> = pr.iter().map(|r| r.node_idx).collect();
assert_eq!(pr_nodes, scope);
}
#[test]
fn test_pagerank_empty_graph() {
let graph = DirGraph::new();
let results = pagerank(&graph, 0.85, 100, 1e-6, None, None, None).unwrap();
assert!(results.is_empty());
}
#[test]
fn test_get_node_info() {
let (graph, indices) = build_chain_graph();
let info = get_node_info(&graph, indices[0]);
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.node_type, "Chain");
assert_eq!(info.title, "Node_0");
}
#[test]
fn test_get_path_connections() {
let (graph, indices) = build_chain_graph();
let path = vec![indices[0], indices[1], indices[2]];
let connections = get_path_connections(&graph, &path);
assert_eq!(connections.len(), 2);
assert_eq!(connections[0], Some("NEXT".to_string()));
assert_eq!(connections[1], Some("NEXT".to_string()));
}
fn build_two_triangle_bridge() -> (DirGraph, Vec<petgraph::graph::NodeIndex>) {
let mut graph = DirGraph::new();
let mut indices = Vec::new();
for i in 0..6 {
let node = NodeData::new(
Value::Int64(i),
Value::String(format!("N_{}", i)),
"Node".to_string(),
HashMap::new(),
&mut graph.interner,
);
let idx = graph.graph.add_node(node);
graph
.type_indices
.entry_or_default("Node".to_string())
.push(idx);
indices.push(idx);
}
let pairs = [(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5), (2, 3)];
for (from, to) in pairs {
let edge = EdgeData::new("LINK".to_string(), HashMap::new(), &mut graph.interner);
graph.graph.add_edge(indices[from], indices[to], edge);
}
(graph, indices)
}
fn community_of(result: &CommunityResult, idx: petgraph::graph::NodeIndex) -> usize {
result
.assignments
.iter()
.find(|a| a.node_idx == idx)
.map(|a| a.community_id)
.expect("node assigned")
}
#[test]
fn test_louvain_multilevel_two_communities() {
let (graph, ix) = build_two_triangle_bridge();
let r = louvain_communities(&graph, None, 1.0, None, None, None).unwrap();
assert_eq!(r.num_communities, 2, "two triangles → two communities");
assert!(
r.modularity > 0.0,
"positive modularity, got {}",
r.modularity
);
assert_eq!(community_of(&r, ix[0]), community_of(&r, ix[1]));
assert_eq!(community_of(&r, ix[0]), community_of(&r, ix[2]));
assert_eq!(community_of(&r, ix[3]), community_of(&r, ix[4]));
assert_eq!(community_of(&r, ix[3]), community_of(&r, ix[5]));
assert_ne!(community_of(&r, ix[0]), community_of(&r, ix[3]));
}
#[test]
fn test_louvain_exposes_hierarchy_levels() {
let (graph, _) = build_two_triangle_bridge();
let r = louvain_communities(&graph, None, 1.0, None, None, None).unwrap();
assert!(!r.levels.is_empty(), "hierarchy levels present");
assert_eq!(r.levels.last().unwrap().len(), r.assignments.len());
for level in &r.levels {
assert_eq!(level.len(), 6);
}
}
#[test]
fn test_louvain_deterministic() {
let (graph, _) = build_two_triangle_bridge();
let a = louvain_communities(&graph, None, 1.0, None, None, None).unwrap();
let b = louvain_communities(&graph, None, 1.0, None, None, None).unwrap();
assert_eq!(a.num_communities, b.num_communities);
let ca: Vec<usize> = a.assignments.iter().map(|x| x.community_id).collect();
let cb: Vec<usize> = b.assignments.iter().map(|x| x.community_id).collect();
assert_eq!(ca, cb, "deterministic across runs");
}
#[test]
fn test_louvain_empty_and_isolated() {
let g = DirGraph::new();
let r = louvain_communities(&g, None, 1.0, None, None, None).unwrap();
assert_eq!(r.num_communities, 0);
assert!(r.levels.is_empty());
let mut g3 = DirGraph::new();
for i in 0..3 {
let node = NodeData::new(
Value::Int64(i),
Value::String(format!("I_{}", i)),
"Node".to_string(),
HashMap::new(),
&mut g3.interner,
);
let idx = g3.graph.add_node(node);
g3.type_indices
.entry_or_default("Node".to_string())
.push(idx);
}
let r3 = louvain_communities(&g3, None, 1.0, None, None, None).unwrap();
assert_eq!(r3.num_communities, 3);
assert_eq!(r3.modularity, 0.0);
}
fn assert_all_communities_connected(graph: &DirGraph, result: &CommunityResult) {
use std::collections::{HashMap, HashSet, VecDeque};
let mut adj: HashMap<petgraph::graph::NodeIndex, Vec<petgraph::graph::NodeIndex>> =
HashMap::new();
for e in graph.graph.edge_references() {
adj.entry(e.source()).or_default().push(e.target());
adj.entry(e.target()).or_default().push(e.source());
}
let mut groups: HashMap<usize, Vec<petgraph::graph::NodeIndex>> = HashMap::new();
for a in &result.assignments {
groups.entry(a.community_id).or_default().push(a.node_idx);
}
for (cid, members) in &groups {
if members.len() <= 1 {
continue;
}
let set: HashSet<_> = members.iter().copied().collect();
let mut seen: HashSet<_> = HashSet::new();
let mut q = VecDeque::new();
q.push_back(members[0]);
seen.insert(members[0]);
while let Some(u) = q.pop_front() {
if let Some(ns) = adj.get(&u) {
for &v in ns {
if set.contains(&v) && seen.insert(v) {
q.push_back(v);
}
}
}
}
assert_eq!(
seen.len(),
members.len(),
"community {cid} is disconnected (Leiden must not produce that)"
);
}
}
#[test]
fn test_leiden_two_communities() {
let (graph, ix) = build_two_triangle_bridge();
let r = leiden_communities(&graph, None, 1.0, None, None, None).unwrap();
assert_eq!(r.num_communities, 2);
assert!(r.modularity > 0.0);
assert_eq!(community_of(&r, ix[0]), community_of(&r, ix[1]));
assert_eq!(community_of(&r, ix[3]), community_of(&r, ix[5]));
assert_ne!(community_of(&r, ix[0]), community_of(&r, ix[3]));
}
#[test]
fn test_leiden_communities_well_connected() {
let (graph, _) = build_two_triangle_bridge();
let r = leiden_communities(&graph, None, 1.0, None, None, None).unwrap();
assert_all_communities_connected(&graph, &r);
let (chain, _) = build_chain_graph();
let rc = leiden_communities(&chain, None, 1.0, None, None, None).unwrap();
assert_all_communities_connected(&chain, &rc);
}
#[test]
fn test_leiden_deterministic() {
let (graph, _) = build_two_triangle_bridge();
let a = leiden_communities(&graph, None, 1.0, None, None, None).unwrap();
let b = leiden_communities(&graph, None, 1.0, None, None, None).unwrap();
let ca: Vec<usize> = a.assignments.iter().map(|x| x.community_id).collect();
let cb: Vec<usize> = b.assignments.iter().map(|x| x.community_id).collect();
assert_eq!(ca, cb);
}
#[test]
fn test_leiden_hierarchy_and_modularity_vs_louvain() {
let (graph, _) = build_two_triangle_bridge();
let lei = leiden_communities(&graph, None, 1.0, None, None, None).unwrap();
let lou = louvain_communities(&graph, None, 1.0, None, None, None).unwrap();
assert!(!lei.levels.is_empty());
assert_eq!(lei.levels.last().unwrap().len(), lei.assignments.len());
assert!(
lei.modularity >= lou.modularity - 1e-9,
"leiden {} should be >= louvain {}",
lei.modularity,
lou.modularity
);
}
#[test]
fn test_leiden_empty_and_isolated() {
let g = DirGraph::new();
let r = leiden_communities(&g, None, 1.0, None, None, None).unwrap();
assert_eq!(r.num_communities, 0);
assert!(r.levels.is_empty());
}