use codelore_lib::analyses::modularity_violations::run_modularity_violations;
use codelore_lib::analyses::unstable_interface::run_unstable_interface;
use codelore_lib::facts::FactsDb;
use codelore_lib::repo::GixRepo;
use codelore_lib::test_support::permissive_coupling_opts;
use std::collections::HashSet;
use std::path::Path;
use std::process::Command;
fn git(dir: &Path, args: &[&str]) {
let status = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.status()
.expect("spawn git");
assert!(status.success(), "git {args:?} failed");
}
fn commit(dir: &Path, iso_date: &str, msg: &str) {
git(dir, &["add", "."]);
let status = Command::new("git")
.arg("-C")
.arg(dir)
.args(["commit", "-m", msg, "--quiet"])
.env("GIT_AUTHOR_DATE", iso_date)
.env("GIT_COMMITTER_DATE", iso_date)
.status()
.expect("spawn git commit");
assert!(status.success(), "git commit {msg} failed");
}
fn write(root: &Path, rel: &str, content: &str) {
let p = root.join(rel);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, content).unwrap();
}
fn build_hub_repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let p = dir.path();
git(p, &["init", "-b", "main", "--quiet"]);
git(p, &["config", "user.email", "hub@example.com"]);
git(p, &["config", "user.name", "Hub"]);
write(
p,
"Cargo.toml",
"[package]\nname=\"fix\"\nversion=\"0.1.0\"\nedition=\"2021\"\n",
);
write(
p,
"src/lib.rs",
"pub mod core_mod;\npub mod a;\npub mod b;\npub mod c;\npub mod helper;\npub mod util;\n",
);
write(p, "src/core_mod.rs", "pub fn core_fn() {}\n");
commit(p, "2026-01-01T10:00:00Z", "skeleton + hub");
write(
p,
"src/a.rs",
"use crate::core_mod;\npub fn a() { core_mod::core_fn(); }\n",
);
write(p, "src/core_mod.rs", "pub fn core_fn() { let _ = 1; }\n");
commit(p, "2026-01-02T10:00:00Z", "add a + touch hub");
write(p, "src/b.rs", "use crate::core_mod;\npub fn b() {}\n");
write(p, "src/core_mod.rs", "pub fn core_fn() { let _ = 2; }\n");
commit(p, "2026-01-03T10:00:00Z", "add b + touch hub");
write(p, "src/c.rs", "use crate::core_mod;\npub fn c() {}\n");
commit(p, "2026-01-04T10:00:00Z", "add c alone");
write(p, "src/helper.rs", "pub fn helper() {}\n");
write(p, "src/util.rs", "pub fn util() {}\n");
commit(p, "2026-01-05T10:00:00Z", "helper + util together");
dir
}
fn ingested(dir: &Path) -> (FactsDb, codelore_lib::Options) {
let repo = GixRepo::open(dir).expect("open hub repo");
let db = FactsDb::new_in_memory().expect("in-memory db");
let opts = permissive_coupling_opts(dir.to_path_buf());
db.ingest(&repo, &opts).expect("ingest hub repo");
(db, opts)
}
#[test]
fn modularity_violations_flags_implicit_coupling_only() {
let dir = build_hub_repo();
let (db, opts) = ingested(dir.path());
let rows = run_modularity_violations(&db, &opts).expect("run modularity-violations");
let pairs: HashSet<(String, String)> = rows
.iter()
.map(|r| (r.entity_a.clone(), r.entity_b.clone()))
.collect();
assert!(
pairs.contains(&("src/helper.rs".into(), "src/util.rs".into())),
"helper/util co-change without an import edge must be flagged; got {pairs:?}"
);
assert!(
!pairs.contains(&("src/a.rs".into(), "src/core_mod.rs".into())),
"a→core_mod is a real import edge; must NOT be a modularity violation; got {pairs:?}"
);
assert!(
!pairs.contains(&("src/b.rs".into(), "src/core_mod.rs".into())),
"b→core_mod is a real import edge; must NOT be a modularity violation; got {pairs:?}"
);
}
#[test]
fn unstable_interface_surfaces_the_churning_hub() {
let dir = build_hub_repo();
let (db, opts) = ingested(dir.path());
let rows = run_unstable_interface(&db, &opts).expect("run unstable-interface");
let core = rows
.iter()
.find(|r| r.path == "src/core_mod.rs")
.unwrap_or_else(|| {
panic!("core_mod should surface as an unstable interface; got {rows:?}")
});
assert_eq!(core.fan_in, 3, "core_mod is imported by a, b, c");
assert_eq!(
core.coupled_dependents, 2,
"only a and b co-change with the hub (c is imported-but-not-coupled)"
);
assert!((core.instability_score - f64::from(core.revisions) * 2.0).abs() < 1e-9);
for w in rows.windows(2) {
assert!(
w[0].instability_score >= w[1].instability_score,
"rows must be sorted by instability_score descending"
);
}
}