use std::collections::BTreeMap;
use std::path::PathBuf;
use sdivi_detection::leiden::run_leiden;
use sdivi_detection::partition::{LeidenConfig, LeidenPartition};
use sdivi_detection::warm_start::initial_assignment_from_cache;
use sdivi_graph::dependency_graph::build_dependency_graph;
use sdivi_parsing::feature_record::FeatureRecord;
use sdivi_pipeline::cache::{load_cached_partition, save_cached_partition};
fn make_record(path: &str, imports: &[&str]) -> FeatureRecord {
FeatureRecord {
path: PathBuf::from(path),
language: "rust".to_string(),
imports: imports.iter().map(|s| s.to_string()).collect(),
exports: vec![],
signatures: vec![],
pattern_hints: vec![],
}
}
fn clique_records() -> Vec<FeatureRecord> {
vec![
make_record(
"src/a0.rs",
&["crate::a1", "crate::a2", "crate::a3", "crate::a4"],
),
make_record(
"src/a1.rs",
&["crate::a0", "crate::a2", "crate::a3", "crate::a4"],
),
make_record(
"src/a2.rs",
&["crate::a0", "crate::a1", "crate::a3", "crate::a4"],
),
make_record(
"src/a3.rs",
&["crate::a0", "crate::a1", "crate::a2", "crate::a4"],
),
make_record(
"src/a4.rs",
&[
"crate::a0",
"crate::a1",
"crate::a2",
"crate::a3",
"crate::b0",
],
),
make_record(
"src/b0.rs",
&[
"crate::b1",
"crate::b2",
"crate::b3",
"crate::b4",
"crate::a4",
],
),
make_record(
"src/b1.rs",
&["crate::b0", "crate::b2", "crate::b3", "crate::b4"],
),
make_record(
"src/b2.rs",
&["crate::b0", "crate::b1", "crate::b3", "crate::b4"],
),
make_record(
"src/b3.rs",
&["crate::b0", "crate::b1", "crate::b2", "crate::b4"],
),
make_record(
"src/b4.rs",
&["crate::b0", "crate::b1", "crate::b2", "crate::b3"],
),
]
}
#[test]
fn missing_cache_returns_none() {
let tmp = tempfile::tempdir().unwrap();
let cache_path = tmp.path().join("partition.json");
let result = load_cached_partition(&cache_path);
assert!(result.is_none(), "missing file must return None");
}
#[test]
fn corrupt_cache_returns_none() {
let tmp = tempfile::tempdir().unwrap();
let cache_path = tmp.path().join("partition.json");
std::fs::write(&cache_path, b"not valid json").unwrap();
let result = load_cached_partition(&cache_path);
assert!(result.is_none(), "corrupt JSON must return None");
}
#[test]
fn round_trip_save_load() {
let tmp = tempfile::tempdir().unwrap();
let cache_path = tmp.path().join("cache/partition.json");
let original = LeidenPartition {
assignments: BTreeMap::from([(0, 0), (1, 0), (2, 1)]),
stability: BTreeMap::from([(0, 0.75), (1, 1.0)]),
modularity: 0.35,
seed: 42,
};
save_cached_partition(&original, &cache_path).expect("save must succeed");
let loaded = load_cached_partition(&cache_path).expect("load must succeed");
assert_eq!(original, loaded, "round-trip must be identical");
}
#[test]
fn initial_assignment_singletons_for_absent_nodes() {
let cached = LeidenPartition {
assignments: BTreeMap::from([(0, 0), (1, 0)]),
stability: BTreeMap::from([(0, 0.8)]),
modularity: 0.4,
seed: 42,
};
let init = initial_assignment_from_cache(Some(&cached), 5);
assert_eq!(init.len(), 5);
assert_eq!(init[0], init[1], "nodes 0 and 1 should share a community");
assert_ne!(init[2], init[3]);
assert_ne!(init[2], init[4]);
assert_ne!(init[3], init[4]);
assert_ne!(init[2], init[0]);
}
#[test]
fn warm_start_quality_within_one_percent_of_cold_start() {
let records = clique_records();
let dg = build_dependency_graph(&records);
let cfg = LeidenConfig {
seed: 42,
..LeidenConfig::default()
};
let cold = run_leiden(&dg, &cfg, None);
let tmp = tempfile::tempdir().unwrap();
let cache_path = tmp.path().join("partition.json");
save_cached_partition(&cold, &cache_path).unwrap();
let cached = load_cached_partition(&cache_path).unwrap();
let warm = run_leiden(&dg, &cfg, Some(&cached));
let cold_mod = cold.modularity;
let warm_mod = warm.modularity;
let tolerance = 0.01 * cold_mod.abs().max(1e-9);
assert!(
(warm_mod - cold_mod).abs() <= tolerance,
"warm-start modularity {warm_mod:.6} deviates > 1% from cold-start {cold_mod:.6}"
);
}
#[test]
fn warm_start_honours_prior_assignments_in_first_iteration() {
let records = clique_records();
let dg = build_dependency_graph(&records);
let warm_partition = LeidenPartition {
assignments: BTreeMap::from([
(0, 0),
(1, 0),
(2, 0),
(3, 0),
(4, 0),
(5, 1),
(6, 1),
(7, 1),
(8, 1),
(9, 1),
]),
stability: BTreeMap::from([(0, 0.9), (1, 0.9)]),
modularity: 0.45,
seed: 42,
};
let cfg = LeidenConfig {
seed: 42,
..LeidenConfig::default()
};
let result = run_leiden(&dg, &cfg, Some(&warm_partition));
assert!(result.community_count() >= 1);
assert_eq!(result.assignments.len(), 10);
}