use codelore_lib::Options;
use codelore_lib::analyses::code_health::run_code_health;
use codelore_lib::analyses::god_classes::run_god_classes;
use codelore_lib::analyses::hotspots::run_hotspots;
use codelore_lib::analyses::revisions::run_revisions;
use codelore_lib::facts::FactsDb;
use codelore_lib::repo::GixRepo;
fn run_git(path: &std::path::Path, args: &[&str]) {
let out = std::process::Command::new("git")
.args(args)
.current_dir(path)
.output()
.expect("git");
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
fn write(p: std::path::PathBuf, content: &str) {
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, content).unwrap();
}
#[test]
fn grouping_rewrites_paths_to_logical_groups() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
run_git(path, &["init", "-b", "main", "--quiet"]);
run_git(path, &["config", "user.email", "t@e.com"]);
run_git(path, &["config", "user.name", "T"]);
write(
path.join("groups.txt"),
"src/auth => Auth\nsrc/db => DB\n",
);
write(path.join("src/auth/login.rs"), "v1\n");
write(path.join("src/db/migrate.rs"), "v1\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c1", "--quiet"]);
write(path.join("src/auth/session.rs"), "v1\n");
write(path.join("src/db/migrate.rs"), "v2\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c2", "--quiet"]);
let repo = GixRepo::open(path).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 0,
group_file: Some(path.join("groups.txt")),
strict_grouping: false,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let revs = run_revisions(&db, &opts).expect("revisions");
let entities: Vec<&str> = revs.iter().map(|(e, _)| e.as_str()).collect();
assert!(
entities.contains(&"Auth"),
"Auth group must appear: {entities:?}"
);
assert!(
entities.contains(&"DB"),
"DB group must appear: {entities:?}"
);
assert!(
!entities
.iter()
.any(|e| e.contains("login.rs") || e.contains("session.rs")),
"raw auth paths must NOT appear after grouping: {entities:?}"
);
let auth_revs = revs.iter().find(|(e, _)| e == "Auth").unwrap().1;
let db_revs = revs.iter().find(|(e, _)| e == "DB").unwrap().1;
assert_eq!(auth_revs, 2);
assert_eq!(db_revs, 2);
}
#[test]
fn grouping_with_canonical_lineage_sees_grouped_paths() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
run_git(path, &["init", "-b", "main", "--quiet"]);
run_git(path, &["config", "user.email", "t@e.com"]);
run_git(path, &["config", "user.name", "T"]);
write(
path.join("groups.txt"),
"src/auth => Auth\nsrc/db => DB\n",
);
write(path.join("src/auth/login.rs"), "v1\n");
write(path.join("src/db/migrate.rs"), "v1\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c1", "--quiet"]);
write(path.join("src/auth/session.rs"), "v1\n");
write(path.join("src/db/migrate.rs"), "v2\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c2", "--quiet"]);
let repo = GixRepo::open(path).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 0,
group_file: Some(path.join("groups.txt")),
use_canonical_lineage: true,
strict_grouping: false,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let revs = run_revisions(&db, &opts).expect("revisions");
let entities: Vec<&str> = revs.iter().map(|(e, _)| e.as_str()).collect();
assert!(
entities.contains(&"Auth") && entities.contains(&"DB"),
"grouped entities must appear with lineage on: {entities:?}"
);
assert!(
!entities.iter().any(|e| {
e.contains("login.rs") || e.contains("session.rs") || e.contains("migrate.rs")
}),
"stale raw paths must NOT appear — that would mean the lineage guard \
served a pre-grouping view: {entities:?}"
);
}
#[test]
fn grouping_strict_mode_drops_unmapped_paths() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
run_git(path, &["init", "-b", "main", "--quiet"]);
run_git(path, &["config", "user.email", "t@e.com"]);
run_git(path, &["config", "user.name", "T"]);
write(path.join("groups.txt"), "src/auth => Auth\n");
write(path.join("src/auth/login.rs"), "v1\n");
write(path.join("vendor/lib.rs"), "v1\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c1", "--quiet"]);
let repo = GixRepo::open(path).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 0,
group_file: Some(path.join("groups.txt")),
strict_grouping: true,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let revs = run_revisions(&db, &opts).expect("revisions");
let entities: Vec<&str> = revs.iter().map(|(e, _)| e.as_str()).collect();
assert!(entities.contains(&"Auth"));
assert!(
!entities.iter().any(|e| e.contains("vendor")),
"vendor/lib.rs must be dropped in strict mode: {entities:?}"
);
}
#[test]
fn grouping_non_strict_mode_keeps_unmapped_paths_raw() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
run_git(path, &["init", "-b", "main", "--quiet"]);
run_git(path, &["config", "user.email", "t@e.com"]);
run_git(path, &["config", "user.name", "T"]);
write(path.join("groups.txt"), "src/auth => Auth\n");
write(path.join("src/auth/login.rs"), "v1\n");
write(path.join("vendor/lib.rs"), "v1\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c1", "--quiet"]);
let repo = GixRepo::open(path).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 0,
group_file: Some(path.join("groups.txt")),
strict_grouping: false, ..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let revs = run_revisions(&db, &opts).expect("revisions");
let entities: Vec<&str> = revs.iter().map(|(e, _)| e.as_str()).collect();
assert!(entities.contains(&"Auth"));
assert!(
entities.iter().any(|e| e.contains("vendor/lib.rs")),
"vendor/lib.rs must be kept (raw path) in non-strict mode: {entities:?}"
);
}
#[test]
fn grouping_rolls_up_cognitive_into_group_paths() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
run_git(path, &["init", "-b", "main", "--quiet"]);
run_git(path, &["config", "user.email", "t@e.com"]);
run_git(path, &["config", "user.name", "T"]);
write(path.join("groups.txt"), "src/core => Core\n");
let gnarly = "
pub fn trivial() -> u32 { 1 }
pub fn nested(a: bool, b: bool, c: bool, d: bool, e: bool) -> u32 {
if a {
if b {
if c {
if d {
if e { 5 } else { 4 }
} else if c && b { 3 } else { 2 }
} else if b || a { 1 } else { 0 }
} else if a && b && c { 9 } else { 8 }
} else if a || b || c || d || e { 7 } else { 6 }
}
";
write(path.join("src/core/nested.rs"), gnarly);
write(path.join("src/core/other.rs"), "pub fn x() -> u32 { 2 }\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c1", "--quiet"]);
let repo = GixRepo::open(path).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 0,
group_file: Some(path.join("groups.txt")),
strict_grouping: false,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let hotspots = run_hotspots(&db, &opts).expect("hotspots");
let core = hotspots
.iter()
.find(|r| r.path == "Core")
.expect("Core group must appear in hotspots output");
assert!(
core.cognitive > 0.0,
"grouped Core row must report non-zero cognitive after rollup; got {core:?}"
);
let _gods = run_god_classes(&db, &opts).expect("god-classes must run against grouped paths");
}
#[test]
fn grouping_survives_hunks_from_modified_files_non_strict() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
run_git(path, &["init", "-b", "main", "--quiet"]);
run_git(path, &["config", "user.email", "t@e.com"]);
run_git(path, &["config", "user.name", "T"]);
write(path.join("groups.txt"), "src/auth => Auth\n");
write(path.join("src/auth/login.rs"), "v1\n");
write(path.join("notes.md"), "a\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c1", "--quiet"]);
write(path.join("src/auth/login.rs"), "v1\nv2\n");
write(path.join("notes.md"), "a\nb\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c2", "--quiet"]);
let repo = GixRepo::open(path).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 0,
group_file: Some(path.join("groups.txt")),
strict_grouping: false,
..Options::default()
};
db.ingest(&repo, &opts)
.expect("ingest with grouping over modified files must not trip the hunks FK");
let auth_changes = db
.query_one_value("SELECT CAST(COUNT(*) AS TEXT) FROM changes WHERE path = 'Auth'")
.expect("Auth changes count");
assert_ne!(auth_changes, "0", "Auth group must appear in changes");
let notes_changes = db
.query_one_value("SELECT CAST(COUNT(*) AS TEXT) FROM changes WHERE path = 'notes.md'")
.expect("notes.md changes count");
assert_ne!(
notes_changes, "0",
"unmapped notes.md must keep its raw path in changes"
);
let notes_hunks = db
.query_one_value("SELECT CAST(COUNT(*) AS TEXT) FROM hunks WHERE path = 'notes.md'")
.expect("notes.md hunks count");
assert_ne!(
notes_hunks, "0",
"hunks of the unmapped modified file must survive the grouping swap"
);
let other_hunks = db
.query_one_value("SELECT CAST(COUNT(*) AS TEXT) FROM hunks WHERE path <> 'notes.md'")
.expect("non-surviving hunks count");
assert_eq!(
other_hunks, "0",
"grouped-away paths must have no hunk rows after the swap"
);
}
#[test]
fn grouping_strict_mode_with_modified_files() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
run_git(path, &["init", "-b", "main", "--quiet"]);
run_git(path, &["config", "user.email", "t@e.com"]);
run_git(path, &["config", "user.name", "T"]);
write(path.join("groups.txt"), "src/auth => Auth\n");
write(path.join("src/auth/login.rs"), "v1\n");
write(path.join("notes.md"), "a\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c1", "--quiet"]);
write(path.join("src/auth/login.rs"), "v1\nv2\n");
write(path.join("notes.md"), "a\nb\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c2", "--quiet"]);
let repo = GixRepo::open(path).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 0,
group_file: Some(path.join("groups.txt")),
strict_grouping: true,
..Options::default()
};
db.ingest(&repo, &opts)
.expect("strict grouping over modified files must not trip the hunks FK");
let notes_changes = db
.query_one_value("SELECT CAST(COUNT(*) AS TEXT) FROM changes WHERE path = 'notes.md'")
.expect("notes.md changes count");
assert_eq!(
notes_changes, "0",
"strict mode must drop the unmapped notes.md from changes"
);
let hunks_total = db
.query_one_value("SELECT CAST(COUNT(*) AS TEXT) FROM hunks")
.expect("hunks count");
assert_eq!(
hunks_total, "0",
"strict mode leaves no surviving paths, so hunks must be empty"
);
}
#[test]
fn code_health_works_with_grouping() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
run_git(path, &["init", "-b", "main", "--quiet"]);
run_git(path, &["config", "user.email", "t@e.com"]);
run_git(path, &["config", "user.name", "T"]);
write(path.join("groups.txt"), "src/core => Core\n");
let gnarly = "
pub fn trivial() -> u32 { 1 }
pub fn nested(a: bool, b: bool, c: bool, d: bool, e: bool) -> u32 {
if a {
if b {
if c {
if d {
if e { 5 } else { 4 }
} else if c && b { 3 } else { 2 }
} else if b || a { 1 } else { 0 }
} else if a && b && c { 9 } else { 8 }
} else if a || b || c || d || e { 7 } else { 6 }
}
";
write(path.join("src/core/nested.rs"), gnarly);
write(path.join("notes.md"), "a\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c1", "--quiet"]);
let gnarly_v2 = format!("{gnarly}\npub fn added_later() -> u32 {{ 3 }}\n");
write(path.join("src/core/nested.rs"), &gnarly_v2);
write(path.join("notes.md"), "a\nb\n");
run_git(path, &["add", "."]);
run_git(path, &["commit", "-m", "c2", "--quiet"]);
let repo = GixRepo::open(path).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 0,
group_file: Some(path.join("groups.txt")),
strict_grouping: false,
..Options::default()
};
db.ingest(&repo, &opts)
.expect("ingest with grouping over modified files");
let rows = run_code_health(&db, &opts).expect("code-health must run against grouped paths");
let core = rows
.iter()
.find(|r| r.path == "Core")
.expect("Core group must appear in code-health output");
assert!(
core.cognitive > 0.0,
"grouped Core row must report non-zero cognitive after rollup; got {core:?}"
);
}