use mitts::cli::coverage_tools::resolve_llvm_tool;
use serde_json::Value;
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use tempfile::TempDir;
struct Fixture {
_dir: TempDir,
root: PathBuf,
cache: PathBuf,
}
fn fixture() -> Fixture {
let dir = TempDir::new().unwrap();
let root = dir.path().join("app");
let write = |path: PathBuf, body: &str| {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, body).unwrap();
};
write(
root.join("Cargo.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\
[dependencies]\nvendored = { path = \"vendored/vendored\" }\n\
outsider = { path = \"../outsider\" }\n",
);
write(
root.join("src/lib.rs"),
"pub fn covered() -> u32 { vendored::helper() + outsider::helper() + 1 }\n\
pub fn uncovered() -> u32 { 2 }\n\n\
#[cfg(test)]\nmod tests {\n #[test]\n fn covers() { assert_eq!(super::covered(), 3); }\n}\n",
);
for (path, name) in [
(root.join("vendored/vendored"), "vendored"),
(dir.path().join("outsider"), "outsider"),
] {
write(
path.join("Cargo.toml"),
&format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"),
);
write(
path.join("src/lib.rs"),
"pub fn helper() -> u32 { 1 }\npub fn dead_code_one() -> u32 { 8 }\npub fn dead_code_two() -> u32 { 9 }\n",
);
}
write(
root.join("vendored/vendored/Cargo.lock"),
"version = 3\n\n[[package]]\nname = \"vendored\"\nversion = \"0.1.0\"\n",
);
let cache = dir.path().join("cache");
Fixture {
_dir: dir,
root,
cache,
}
}
fn llvm_tools_available() -> bool {
resolve_llvm_tool("llvm-profdata").is_some() && resolve_llvm_tool("llvm-cov").is_some()
}
impl Fixture {
fn run(&self, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_mitts"))
.current_dir(&self.root)
.arg("llvm-cov")
.args(args)
.args(["--offline", "--cache-dir"])
.arg(&self.cache)
.env("CARGO_HOME", self.root.join("cargo-home"))
.output()
.unwrap()
}
}
fn text(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).to_string()
}
fn lcov_files(lcov: &str) -> Vec<String> {
lcov.lines()
.filter_map(|line| line.strip_prefix("SF:"))
.map(str::to_string)
.collect()
}
fn json_files(report: &Value) -> BTreeSet<String> {
report["data"][0]["files"]
.as_array()
.map(|files| {
files
.iter()
.map(|file| file["filename"].as_str().unwrap().to_string())
.collect()
})
.unwrap_or_default()
}
fn line_percent(report: &Value) -> f64 {
report["data"][0]["totals"]["lines"]["percent"]
.as_f64()
.expect("totals.lines.percent")
}
fn read_json(path: &Path) -> Value {
serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap()
}
#[test]
fn report_paths_are_unique_and_identify_the_file() {
if !llvm_tools_available() {
eprintln!("skipped: llvm-tools are not installed");
return;
}
let fixture = fixture();
let lcov_path = fixture.root.join("out/coverage.lcov");
let output = fixture.run(&["--lcov", "--output-path", lcov_path.to_str().unwrap()]);
assert!(output.status.success(), "{}", text(&output.stderr));
let files = lcov_files(&fs::read_to_string(&lcov_path).unwrap());
for expected in [
"src/lib.rs",
"vendored/vendored/src/lib.rs",
"external/outsider-0.1.0/src/lib.rs",
] {
assert!(
files.iter().any(|file| file == expected),
"{expected} missing from {files:?}"
);
}
let unique: BTreeSet<_> = files.iter().collect();
assert_eq!(unique.len(), files.len(), "paths collide: {files:?}");
for file in &files {
assert!(
!file.contains("/.cargo/") && !file.contains("/sources/") && !file.contains("/rustc/"),
"dependency or toolchain source in the report: {file}"
);
}
}
#[test]
fn ignore_regex_applies_to_every_format_and_to_the_threshold() {
if !llvm_tools_available() {
eprintln!("skipped: llvm-tools are not installed");
return;
}
let fixture = fixture();
let all = fixture.root.join("out/all.json");
let filtered = fixture.root.join("out/filtered.json");
let regex = "(vendored/|external/)";
let output = fixture.run(&[
"--json",
"--summary-only",
"--output-path",
all.to_str().unwrap(),
]);
assert!(output.status.success(), "{}", text(&output.stderr));
let output = fixture.run(&[
"--json",
"--summary-only",
"--ignore-filename-regex",
regex,
"--output-path",
filtered.to_str().unwrap(),
]);
assert!(output.status.success(), "{}", text(&output.stderr));
let all_report = read_json(&all);
let filtered_report = read_json(&filtered);
assert!(
json_files(&all_report).len() >= 3,
"{:?}",
json_files(&all_report)
);
assert_eq!(
json_files(&filtered_report),
BTreeSet::from(["src/lib.rs".to_string()]),
"only the measured crate is left"
);
let filtered_percent = line_percent(&filtered_report);
assert!(
(line_percent(&all_report) - filtered_percent).abs() > 0.01,
"the dependencies change the total, so the filter matters"
);
assert!(
filtered_percent > 1.0 && filtered_percent < 99.0,
"{filtered_percent}"
);
let below = format!("{:.2}", filtered_percent - 0.5);
let output = fixture.run(&[
"--ignore-filename-regex",
regex,
"--fail-under-lines",
&below,
]);
assert!(output.status.success(), "{}", text(&output.stderr));
let above = format!("{:.2}", filtered_percent + 0.5);
let output = fixture.run(&[
"--ignore-filename-regex",
regex,
"--fail-under-lines",
&above,
]);
assert!(!output.status.success(), "threshold {above} must fail");
let lcov = fixture.root.join("out/filtered.lcov");
let output = fixture.run(&[
"--lcov",
"--ignore-filename-regex",
regex,
"--output-path",
lcov.to_str().unwrap(),
]);
assert!(output.status.success(), "{}", text(&output.stderr));
assert_eq!(
lcov_files(&fs::read_to_string(&lcov).unwrap()),
["src/lib.rs"]
);
}
#[test]
fn output_path_writes_the_report_to_the_file_and_not_to_stdout() {
if !llvm_tools_available() {
eprintln!("skipped: llvm-tools are not installed");
return;
}
let fixture = fixture();
let report = fixture.root.join("target/nested/coverage-report.json");
let output = fixture.run(&[
"--workspace",
"--json",
"--summary-only",
"--output-path",
report.to_str().unwrap(),
]);
assert!(output.status.success(), "{}", text(&output.stderr));
let stdout = text(&output.stdout);
assert!(
!stdout.contains("\"data\""),
"the JSON report must not reach stdout: {stdout}"
);
let parsed = read_json(&report);
assert!(
parsed["data"][0]["totals"]["lines"]["count"]
.as_u64()
.unwrap()
> 0
);
assert!(
parsed["data"][0]["files"][0].get("summary").is_some(),
"--summary-only exports per-file summaries"
);
let text_report = fixture.root.join("out/report.txt");
let output = fixture.run(&["--text", "--output-path", text_report.to_str().unwrap()]);
assert!(output.status.success(), "{}", text(&output.stderr));
let body = fs::read_to_string(&text_report).unwrap();
assert!(
body.contains("TOTAL") && !body.contains('\u{1b}'),
"plain table: {body}"
);
}
#[test]
fn output_path_needs_exactly_one_single_file_format() {
let fixture = fixture();
let target = fixture.root.join("out/x");
let target = target.to_str().unwrap();
for args in [
vec!["--output-path", target],
vec!["--html", "--output-path", target],
vec!["--json", "--lcov", "--output-path", target],
] {
let output = fixture.run(&args);
assert_eq!(
output.status.code(),
Some(2),
"{args:?}: {}",
text(&output.stderr)
);
assert!(text(&output.stderr).contains("--output-path"), "{args:?}");
}
}