use std::collections::BTreeSet;
use std::path::Path;
use std::process::Command;
use codelore_lib::Options;
use codelore_lib::analyses::revisions::run_revisions;
use codelore_lib::facts::FactsDb;
use codelore_lib::repo::GixRepo;
fn code_maat_path() -> Option<String> {
std::env::var("CODE_MAAT_PATH")
.ok()
.filter(|p| Path::new(p).join("project.clj").exists())
}
fn dump_git2_log(repo_path: &Path) -> String {
let output = Command::new("git")
.arg("-C")
.arg(repo_path)
.args([
"log",
"--pretty=format:--%h--%ad--%aN",
"--date=short",
"--numstat",
"--no-renames",
])
.output()
.expect("git log");
assert!(output.status.success(), "git log failed");
String::from_utf8(output.stdout).expect("git log utf-8")
}
fn run_code_maat(code_maat: &str, log_path: &Path, analysis: &str) -> String {
let output = Command::new("lein")
.arg("run")
.args([
"--",
"-l",
log_path.to_str().unwrap(),
"-c",
"git2",
"-a",
analysis,
])
.current_dir(code_maat)
.output()
.expect("lein run");
assert!(
output.status.success(),
"code-maat {analysis} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).expect("code-maat utf-8")
}
fn normalize(csv: &str) -> BTreeSet<String> {
csv.lines()
.skip(1)
.map(|l| l.trim_end().to_string())
.filter(|l| !l.is_empty())
.collect()
}
#[test]
#[ignore = "needs CODE_MAAT_PATH env var pointing at a code-maat checkout"]
fn revisions_matches_code_maat_on_tiny_repo() {
let Some(code_maat) = code_maat_path() else {
eprintln!("CODE_MAAT_PATH unset or invalid — skipping");
return;
};
let tiny = codelore_lib::test_support::tiny_repo::build();
let log_path = tiny.dir.path().join("tiny.log");
std::fs::write(&log_path, dump_git2_log(tiny.dir.path())).expect("write log");
let cmaat_csv = run_code_maat(&code_maat, &log_path, "revisions");
let repo = GixRepo::open(tiny.dir.path()).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: tiny.dir.path().to_path_buf(),
min_revs: 1,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_revisions(&db, &opts).expect("revisions");
let mut buf = Vec::new();
codelore_lib::output::csv::write_revisions_csv(&rows, &mut buf).expect("csv");
let codelore_csv = String::from_utf8(buf).expect("utf-8");
let cmaat = normalize(&cmaat_csv);
let codelore = normalize(&codelore_csv);
assert_eq!(
codelore, cmaat,
"revisions CSV diverged.\ncode-maat:\n{cmaat_csv}\ncodelore:\n{codelore_csv}"
);
}
#[test]
#[ignore = "needs CODE_MAAT_PATH env var pointing at a code-maat checkout"]
fn summary_matches_code_maat_on_tiny_repo() {
let Some(code_maat) = code_maat_path() else {
eprintln!("CODE_MAAT_PATH unset or invalid — skipping");
return;
};
let tiny = codelore_lib::test_support::tiny_repo::build();
let log_path = tiny.dir.path().join("tiny.log");
std::fs::write(&log_path, dump_git2_log(tiny.dir.path())).expect("write log");
let cmaat_csv = run_code_maat(&code_maat, &log_path, "summary");
let repo = GixRepo::open(tiny.dir.path()).expect("gix open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: tiny.dir.path().to_path_buf(),
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = codelore_lib::analyses::summary::run_summary(&db, &opts).expect("summary");
let mut buf = Vec::new();
codelore_lib::output::csv::write_summary_csv(&rows, &mut buf, false).expect("csv");
let codelore_csv = String::from_utf8(buf).expect("utf-8");
let cmaat_map = parse_summary_csv(&cmaat_csv);
let codelore_map = parse_summary_csv(&codelore_csv);
let pairs = [
("number-of-commits", "commits"),
("number-of-authors", "authors"),
];
for (cmaat_key, codelore_key) in pairs {
let cmaat_val = cmaat_map.get(cmaat_key).copied().unwrap_or(-1);
let codelore_val = codelore_map.get(codelore_key).copied().unwrap_or(-1);
assert_eq!(
codelore_val, cmaat_val,
"summary value mismatch for {cmaat_key} / {codelore_key}: code-maat={cmaat_val} codelore={codelore_val}"
);
}
}
fn parse_summary_csv(csv: &str) -> std::collections::HashMap<String, i64> {
csv.lines()
.skip(1)
.filter_map(|l| {
let mut parts = l.splitn(2, ',');
let k = parts.next()?.trim().to_string();
let v: i64 = parts.next()?.trim().parse().ok()?;
Some((k, v))
})
.collect()
}