use std::path::{Path, PathBuf};
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_roteiro");
fn git(dir: &Path, args: &[&str]) {
let status = Command::new("git")
.args([
"-c",
"user.name=Test",
"-c",
"user.email=test@example.com",
"-c",
"commit.gpgsign=false",
"-c",
"init.defaultBranch=main",
])
.args(args)
.current_dir(dir)
.status()
.expect("run git");
assert!(status.success(), "git {args:?} failed");
}
fn roteiro(dir: &Path, args: &[&str]) -> std::process::Output {
Command::new(BIN)
.args(args)
.current_dir(dir)
.env("ROTEIRO_HOME", dir)
.output()
.expect("run roteiro")
}
fn json(dir: &Path, args: &[&str]) -> serde_json::Value {
let out = roteiro(dir, args);
assert!(out.status.success(), "roteiro {args:?} failed: {out:?}");
serde_json::from_slice(&out.stdout).expect("--json is valid JSON")
}
fn write(dir: &Path, rel: &str, content: &str) {
let path = dir.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).expect("mkdir");
std::fs::write(path, content).expect("write");
}
fn fresh_dir() -> PathBuf {
let dir = std::env::temp_dir().join(format!("roteiro-density-cli-{}", std::process::id()));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).expect("mkdir");
dir
}
fn source(markers: usize, lines: usize) -> String {
use std::fmt::Write as _;
let mut s = String::new();
for i in 0..markers {
let _ = writeln!(s, "// TODO item {i}");
}
for i in markers..lines {
let _ = writeln!(s, "pub const N{i}: u32 = {i};");
}
s
}
#[test]
fn density_divides_by_the_line_count_real_extraction_already_records() {
let dir = fresh_dir();
git(&dir, &["init", "-q"]);
write(&dir, "src/big.rs", &source(4, 1000));
write(&dir, "src/small.rs", &source(4, 100));
write(&dir, "vendor/dep.rs", &source(8, 100));
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
let report = json(&dir, &["debt-density", "--json", "--min-lines", "0"]);
let of = |path: &str| -> serde_json::Value {
report["items"]
.as_array()
.expect("items")
.iter()
.find(|i| i["path"] == path)
.unwrap_or_else(|| panic!("`{path}` missing from {report}"))
.clone()
};
assert_eq!(of("src/big.rs")["lines"], 1000, "{report}");
assert_eq!(of("src/small.rs")["lines"], 100);
assert_eq!(
report["unknown_length_files"], 0,
"every file with a marker had a recorded length: {report}"
);
assert_eq!(of("src/big.rs")["markers"], of("src/small.rs")["markers"]);
assert_eq!(of("src/big.rs")["per_kloc"], 4.0);
assert_eq!(of("src/small.rs")["per_kloc"], 40.0);
let ranked: Vec<&str> = report["items"]
.as_array()
.expect("items")
.iter()
.filter_map(|i| i["path"].as_str())
.collect();
assert_eq!(
ranked,
["vendor/dep.rs", "src/small.rs", "src/big.rs"],
"{report}"
);
let by_count = json(
&dir,
&[
"debt-density",
"--json",
"--min-lines",
"0",
"--order",
"markers",
],
);
let ranked: Vec<&str> = by_count["items"]
.as_array()
.expect("items")
.iter()
.filter_map(|i| i["path"].as_str())
.collect();
assert_eq!(
ranked,
["vendor/dep.rs", "src/big.rs", "src/small.rs"],
"{by_count}"
);
write(&dir, "roteiro.toml", "[debt]\nignore = [\"vendor/**\"]\n");
let filtered = json(&dir, &["debt-density", "--json", "--min-lines", "0"]);
assert!(
!filtered["items"]
.as_array()
.expect("items")
.iter()
.any(|i| i["path"] == "vendor/dep.rs"),
"{filtered}"
);
assert_eq!(filtered["files_with_markers"], 2, "{filtered}");
assert_eq!(filtered["total_markers"], 8, "{filtered}");
assert_eq!(
filtered["overall_per_kloc"], 7.27,
"8 markers over 1100 lines: {filtered}"
);
let bad = roteiro(&dir, &["debt-density", "--order", "count"]);
assert!(!bad.status.success(), "{bad:?}");
let err = String::from_utf8_lossy(&bad.stderr);
assert!(err.contains("unknown --order `count`"), "was: {err}");
let text = roteiro(&dir, &["debt-density", "--min-lines", "0"]);
assert!(text.status.success(), "{text:?}");
let out = String::from_utf8_lossy(&text.stdout);
assert!(
out.contains("not source lines of code"),
"the caveat travels with the figures: {out}"
);
assert!(out.contains("baseline:"), "{out}");
std::fs::remove_dir_all(&dir).ok();
}