use codelore_lib::Options;
use codelore_lib::analyses::centrality::run_centrality;
use codelore_lib::analyses::coupling::run_coupling;
use codelore_lib::facts::FactsDb;
use codelore_lib::repo::GixRepo;
use codelore_lib::test_support::permissive_coupling_opts;
use std::collections::{HashMap, HashSet};
#[test]
fn centrality_metrics_agree_with_coupling_oracle() {
let fixture = codelore_lib::test_support::differential_repo::build();
let repo = GixRepo::open(fixture.dir.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = permissive_coupling_opts(fixture.dir.path().to_path_buf());
db.ingest(&repo, &opts).expect("ingest");
let pairs = run_coupling(&db, &opts).expect("coupling oracle");
assert!(
!pairs.is_empty(),
"differential fixture should yield ≥1 coupling pair under permissive options"
);
let rows = run_centrality(&db, &opts).expect("run centrality");
assert!(
!rows.is_empty(),
"a non-empty coupling graph must produce ≥1 centrality row"
);
let oracle_paths: HashSet<&str> = pairs
.iter()
.flat_map(|p| [p.entity_a.as_str(), p.entity_b.as_str()])
.collect();
let row_paths: HashSet<&str> = rows.iter().map(|r| r.path.as_str()).collect();
assert_eq!(
oracle_paths, row_paths,
"centrality node set must equal the set of files in coupling pairs"
);
let mut expected_degree: HashMap<&str, u32> = HashMap::new();
for p in &pairs {
*expected_degree.entry(p.entity_a.as_str()).or_default() += 1;
*expected_degree.entry(p.entity_b.as_str()).or_default() += 1;
}
for row in &rows {
assert_eq!(
row.degree,
expected_degree[row.path.as_str()],
"degree mismatch for {} vs coupling-pair count",
row.path
);
}
let pr_sum: f64 = rows.iter().map(|r| r.pagerank).sum();
assert!(
(pr_sum - 1.0).abs() < 1e-6,
"PageRank should sum to 1, got {pr_sum}"
);
let ev_norm_sq: f64 = rows.iter().map(|r| r.eigenvector * r.eigenvector).sum();
assert!(
(ev_norm_sq - 1.0).abs() < 1e-6,
"eigenvector L2 norm² should be 1, got {ev_norm_sq}"
);
for w in rows.windows(2) {
assert!(
w[0].pagerank >= w[1].pagerank,
"rows must be sorted by PageRank descending: {} ({}) < {} ({})",
w[0].path,
w[0].pagerank,
w[1].path,
w[1].pagerank,
);
}
for row in &rows {
assert!(
row.weighted_degree.is_finite() && row.weighted_degree >= 0.0,
"weighted_degree out of range for {}: {}",
row.path,
row.weighted_degree
);
assert!(
row.pagerank.is_finite() && row.pagerank >= 0.0,
"pagerank out of range for {}: {}",
row.path,
row.pagerank
);
assert!(
row.eigenvector.is_finite() && row.eigenvector >= 0.0,
"eigenvector out of range for {}: {}",
row.path,
row.eigenvector
);
}
}
#[test]
fn centrality_empty_when_no_significant_coupling() {
let fixture = codelore_lib::test_support::differential_repo::build();
let repo = GixRepo::open(fixture.dir.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
fisher_significance: 0.0,
..permissive_coupling_opts(fixture.dir.path().to_path_buf())
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_centrality(&db, &opts).expect("run centrality");
assert!(
rows.is_empty(),
"no Fisher-significant pairs should yield an empty centrality result, got {} rows",
rows.len()
);
}