use std::collections::HashSet;
use super::graph::AdjGraph;
use super::{analyze, detect_communities, hardening, leiden, stable_ids};
use crate::core::property_graph::{CodeGraph, Edge, EdgeKind, Node};
fn build_test_graph() -> CodeGraph {
let graph = CodeGraph::open_in_memory().unwrap();
let na = graph.upsert_node(&Node::file("src/core/a.rs")).unwrap();
let nb = graph.upsert_node(&Node::file("src/core/b.rs")).unwrap();
let nc = graph.upsert_node(&Node::file("src/core/c.rs")).unwrap();
let nd = graph.upsert_node(&Node::file("src/tools/d.rs")).unwrap();
let ne = graph.upsert_node(&Node::file("src/tools/e.rs")).unwrap();
graph
.upsert_edge(&Edge::new(na, nb, EdgeKind::Imports))
.unwrap();
graph
.upsert_edge(&Edge::new(nb, nc, EdgeKind::Imports))
.unwrap();
graph
.upsert_edge(&Edge::new(na, nc, EdgeKind::Calls))
.unwrap();
graph
.upsert_edge(&Edge::new(nd, ne, EdgeKind::Imports))
.unwrap();
graph
.upsert_edge(&Edge::new(ne, nd, EdgeKind::Calls))
.unwrap();
graph
.upsert_edge(&Edge::new(nc, nd, EdgeKind::Imports))
.unwrap();
graph
}
#[test]
fn detects_communities() {
let g = build_test_graph();
let result = detect_communities(g.connection());
assert!(!result.communities.is_empty());
assert_eq!(result.node_count, 5);
assert!(result.edge_count > 0);
}
#[test]
fn modularity_non_negative() {
let g = build_test_graph();
let result = detect_communities(g.connection());
assert!(result.modularity >= 0.0);
}
#[test]
fn community_files_cover_all_nodes() {
let g = build_test_graph();
let result = detect_communities(g.connection());
let total_files: usize = result.communities.iter().map(|c| c.files.len()).sum();
assert_eq!(total_files, 5);
}
#[test]
fn empty_graph() {
let g = CodeGraph::open_in_memory().unwrap();
let result = detect_communities(g.connection());
assert!(result.communities.is_empty());
assert_eq!(result.modularity, 0.0);
assert_eq!(result.node_count, 0);
}
#[test]
fn communities_are_connected() {
let g = build_test_graph();
let graph = AdjGraph::from_property_graph(g.connection());
let result = detect_communities(g.connection());
for comm in &result.communities {
if comm.files.len() <= 1 {
continue;
}
let indices: Vec<usize> = comm
.files
.iter()
.filter_map(|f| graph.node_to_idx.get(f).copied())
.collect();
let components = leiden::find_connected_components(&graph, &indices);
assert_eq!(
components.len(),
1,
"community {} should be connected",
comm.id
);
}
}
fn names(n: usize) -> Vec<String> {
(0..n).map(|i| format!("f{i}")).collect()
}
fn two_triangles(n: usize) -> AdjGraph {
let mut edges = vec![
(0, 1, "imports"),
(1, 2, "imports"),
(0, 2, "imports"),
(3, 4, "imports"),
(4, 5, "imports"),
(3, 5, "imports"),
(2, 3, "imports"), ];
if n > 6 {
edges.push((5, 6, "imports")); }
AdjGraph::from_test_edges(names(n), &edges)
}
#[test]
fn partition_is_deterministic() {
let g = two_triangles(6);
let (a1, r1) = analyze(&g, None);
let (a2, r2) = analyze(&g, None);
assert_eq!(a1, a2, "assignment must be identical across runs");
assert_eq!(r1.assignment(), r2.assignment());
assert!(r1.communities.len() >= 2, "triangles must not be merged");
}
#[test]
fn ids_stable_across_rebuild() {
let g1 = two_triangles(6);
let (_, r1) = analyze(&g1, None);
let map1 = r1.assignment();
let g2 = two_triangles(7);
let (_, r2) = analyze(&g2, Some(&map1));
let map2 = r2.assignment();
assert_eq!(
map1["f0"], map2["f0"],
"cluster A keeps its id across rebuild"
);
assert_eq!(
map1["f3"], map2["f3"],
"cluster B keeps its id even though it grew"
);
assert_eq!(map2["f3"], map2["f6"], "new node joins cluster B");
}
fn hub_bridged_graph() -> AdjGraph {
let mut edges: Vec<(usize, usize, &str)> = Vec::new();
for i in 0..8 {
edges.push((i, (i + 1) % 8, "imports"));
}
edges.push((0, 2, "imports"));
edges.push((4, 6, "imports"));
for i in 8..16 {
let next = if i == 15 { 8 } else { i + 1 };
edges.push((i, next, "imports"));
}
edges.push((8, 10, "imports"));
edges.push((12, 14, "imports"));
for &t in &[0, 1, 2, 3, 8, 9, 10, 11] {
edges.push((16, t, "imports"));
}
AdjGraph::from_test_edges(names(17), &edges)
}
#[test]
fn hub_exclusion_prevents_collapse() {
let g = hub_bridged_graph();
let (assignment, result) = analyze(&g, None);
assert!(assignment.iter().all(|&c| c != usize::MAX));
let covered: usize = result.communities.iter().map(|c| c.files.len()).sum();
assert_eq!(covered, 17);
let map = result.assignment();
assert!(
result.communities.len() >= 2,
"hub must not collapse the two clusters into one"
);
assert_ne!(
map["f0"], map["f8"],
"cluster A and B stay in different communities"
);
}
#[test]
fn resplit_breaks_oversized_community() {
let mut edges: Vec<(usize, usize, &str)> = Vec::new();
for grp in 0..2 {
let base = grp * 6;
for i in base..base + 6 {
for j in (i + 1)..base + 6 {
edges.push((i, j, "imports"));
}
}
}
edges.push((5, 6, "imports")); let g = AdjGraph::from_test_edges(names(12), &edges);
let mut assignment = vec![0usize; 12];
hardening::split_oversized_and_incohesive(&g, &mut assignment);
let distinct: HashSet<usize> = assignment.iter().copied().collect();
assert!(
distinct.len() >= 2,
"oversized community should be split, got {} communities",
distinct.len()
);
}
#[test]
fn remap_assigns_fresh_ids_without_overlap() {
let g = two_triangles(6);
let (canonical, _) = analyze(&g, None);
let mut prev = std::collections::HashMap::new();
prev.insert("unrelated_a".to_string(), 0usize);
prev.insert("unrelated_b".to_string(), 1usize);
let remapped = stable_ids::remap_to_previous(&g, &canonical, &prev);
assert_eq!(remapped.len(), canonical.len());
let before: HashSet<usize> = canonical.iter().copied().collect();
let after: HashSet<usize> = remapped.iter().copied().collect();
assert_eq!(before.len(), after.len());
}