use std::fs;
use std::path::Path;
use std::process::Command;
use tempfile::tempdir;
fn cli_cmd(td_path: &Path) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_entropyx"));
cmd.env("ENTROPYX_CACHE_DIR", td_path);
cmd
}
fn run_git(cwd: &Path, args: &[&str]) {
let status = Command::new("git")
.args(args)
.current_dir(cwd)
.status()
.expect("spawn git");
assert!(status.success(), "git {args:?} failed");
}
fn commit_as(cwd: &Path, email: &str, time: i64, subject: &str) {
run_git(cwd, &["add", "-A"]);
let status = Command::new("git")
.args([
"-c",
"user.name=T",
"-c",
&format!("user.email={email}"),
"commit",
"-q",
"-m",
subject,
])
.env("GIT_AUTHOR_DATE", format!("@{time} +0000"))
.env("GIT_COMMITTER_DATE", format!("@{time} +0000"))
.current_dir(cwd)
.status()
.expect("spawn git");
assert!(status.success(), "commit failed");
}
#[test]
fn calibrate_command_produces_valid_scoreweights() {
let td = tempdir().expect("tempdir");
let root = td.path();
run_git(root, &["init", "--quiet"]);
fs::write(root.join("hot.rs"), "pub fn a() {}\n").unwrap();
fs::write(root.join("warm.rs"), "pub fn b() {}\n").unwrap();
fs::write(root.join("stable.rs"), "pub fn c() {}\n").unwrap();
commit_as(root, "alice@ex.com", 100, "init");
fs::write(
root.join("hot.rs"),
"pub fn a() {}\npub fn a2() {}\npub fn a3() {}\n",
)
.unwrap();
fs::write(root.join("warm.rs"), "pub fn b() {}\npub fn b2() {}\n").unwrap();
commit_as(root, "bob@ex.com", 200, "expand hot + warm");
fs::write(
root.join("hot.rs"),
"pub fn a() {}\npub fn a2() {}\npub fn a3() {}\npub fn a4() {}\n",
)
.unwrap();
commit_as(root, "carol@ex.com", 400, "even more hot");
let scan_out = cli_cmd(td.path())
.args(["scan"])
.arg(root)
.output()
.expect("scan");
assert!(
scan_out.status.success(),
"scan failed: {}",
String::from_utf8_lossy(&scan_out.stderr),
);
let summary_path = td.path().join("summary.json");
fs::write(&summary_path, &scan_out.stdout).unwrap();
let labels = serde_json::json!({
"hot.rs": 0.9,
"warm.rs": 0.5,
"stable.rs": 0.1,
});
let labels_path = td.path().join("labels.json");
fs::write(&labels_path, serde_json::to_string_pretty(&labels).unwrap()).unwrap();
let cal_out = cli_cmd(td.path())
.args(["calibrate"])
.arg("--summary")
.arg(&summary_path)
.arg("--labels")
.arg(&labels_path)
.output()
.expect("calibrate");
assert!(
cal_out.status.success(),
"calibrate failed: {}",
String::from_utf8_lossy(&cal_out.stderr),
);
let weights: serde_json::Value = serde_json::from_slice(&cal_out.stdout).expect("weights JSON");
let positives: f64 = [
"theta_d", "theta_h", "theta_v", "theta_c", "theta_b", "theta_s",
]
.iter()
.map(|k| weights[k].as_f64().unwrap())
.sum();
assert!(
(positives - 1.0).abs() < 1e-9,
"positives sum = {positives}",
);
let theta_t = weights["theta_t"].as_f64().unwrap();
assert!(
(0.0..=1.0).contains(&theta_t),
"theta_t = {theta_t} out of range",
);
let weights_path = td.path().join("weights.json");
fs::write(&weights_path, &cal_out.stdout).unwrap();
let scan2_out = cli_cmd(td.path())
.args(["scan"])
.arg(root)
.arg("--weights")
.arg(&weights_path)
.output()
.expect("scan with weights");
assert!(
scan2_out.status.success(),
"weighted scan failed: {}",
String::from_utf8_lossy(&scan2_out.stderr),
);
let summary2: serde_json::Value =
serde_json::from_slice(&scan2_out.stdout).expect("weighted summary");
for row in summary2["files"].as_array().unwrap() {
let composite = row["values"][7].as_f64().unwrap();
assert!(
composite.is_finite(),
"weighted composite {composite} not finite"
);
}
}
#[test]
fn scan_weights_flag_changes_composite() {
let td = tempdir().expect("tempdir");
let root = td.path();
run_git(root, &["init", "--quiet"]);
fs::write(root.join("f.rs"), "pub fn a() {}\n").unwrap();
commit_as(root, "a@ex.com", 100, "init");
fs::write(root.join("f.rs"), "pub fn a() {}\npub fn b() {}\n").unwrap();
commit_as(root, "a@ex.com", 200, "add b");
let default_out = cli_cmd(td.path())
.args(["scan"])
.arg(root)
.output()
.unwrap();
assert!(default_out.status.success());
let default: serde_json::Value = serde_json::from_slice(&default_out.stdout).unwrap();
let default_composite = default["files"][0]["values"][7].as_f64().unwrap();
let weights = serde_json::json!({
"theta_d": 0.0,
"theta_h": 0.0,
"theta_v": 0.0,
"theta_c": 0.0,
"theta_b": 0.0,
"theta_s": 1.0,
"theta_t": 0.0,
});
let weights_path = td.path().join("w.json");
fs::write(&weights_path, serde_json::to_string(&weights).unwrap()).unwrap();
let weighted_out = cli_cmd(td.path())
.args(["scan"])
.arg(root)
.arg("--weights")
.arg(&weights_path)
.output()
.unwrap();
assert!(weighted_out.status.success());
let weighted: serde_json::Value = serde_json::from_slice(&weighted_out.stdout).unwrap();
let weighted_composite = weighted["files"][0]["values"][7].as_f64().unwrap();
assert_eq!(
weighted_composite, 1.0,
"all-S weights should yield composite=1.0"
);
assert!(
(weighted_composite - default_composite).abs() > 0.1,
"weighted ({weighted_composite}) should differ meaningfully from default ({default_composite})",
);
}
#[test]
fn scan_weights_rejects_malformed_json() {
let td = tempdir().expect("tempdir");
let root = td.path();
run_git(root, &["init", "--quiet"]);
fs::write(root.join("a.rs"), "fn x(){}").unwrap();
commit_as(root, "a@ex.com", 100, "init");
let bogus = td.path().join("bad.json");
fs::write(&bogus, "{ not valid json").unwrap();
let out = cli_cmd(td.path())
.args(["scan"])
.arg(root)
.arg("--weights")
.arg(&bogus)
.output()
.unwrap();
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("invalid weights JSON"),
"stderr: {}",
String::from_utf8_lossy(&out.stderr),
);
}
#[test]
fn calibrate_empty_overlap_falls_back_to_defaults() {
let td = tempdir().expect("tempdir");
let root = td.path();
run_git(root, &["init", "--quiet"]);
fs::write(root.join("foo.rs"), "pub fn a(){}").unwrap();
commit_as(root, "a@ex.com", 100, "init");
let scan_out = cli_cmd(td.path())
.args(["scan"])
.arg(root)
.output()
.unwrap();
let summary_path = td.path().join("summary.json");
fs::write(&summary_path, &scan_out.stdout).unwrap();
let labels = serde_json::json!({ "nonexistent/file.rs": 0.9 });
let labels_path = td.path().join("labels.json");
fs::write(&labels_path, serde_json::to_string(&labels).unwrap()).unwrap();
let cal_out = cli_cmd(td.path())
.args(["calibrate"])
.arg("--summary")
.arg(&summary_path)
.arg("--labels")
.arg(&labels_path)
.output()
.unwrap();
assert!(cal_out.status.success(), "calibrate should still succeed");
let w: serde_json::Value = serde_json::from_slice(&cal_out.stdout).unwrap();
assert!(
(w["theta_d"].as_f64().unwrap() - 0.15).abs() < 1e-9,
"empty overlap → DEFAULT_WEIGHTS (theta_d=0.15)",
);
}