use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
use anyhow::{bail, Context, Result};
use serde::Deserialize;
const TEST_OMIT: &str = "*_test.py";
const SUPPORT_OMIT: &str = "*conftest.py";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Thresholds {
pub fail_under: u8,
pub branch: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CoverageReport {
pub totals: Totals,
#[serde(default)]
pub files: BTreeMap<String, FileCoverage>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct FileCoverage {
#[serde(default)]
pub executed_lines: Vec<u64>,
#[serde(default)]
pub missing_lines: Vec<u64>,
#[serde(default)]
pub excluded_lines: Vec<u64>,
#[serde(default)]
pub missing_branches: Vec<Vec<i64>>,
#[serde(default)]
pub executed_branches: Vec<Vec<i64>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Totals {
pub percent_covered: f64,
#[serde(default)]
pub num_branches: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Outcome {
Pass,
Fail(String),
}
pub fn parse_report(json: &str) -> Result<CoverageReport> {
serde_json::from_str(json).context("parsing coverage.py JSON report")
}
pub fn evaluate(report: &CoverageReport, thresholds: Thresholds) -> Outcome {
if thresholds.branch && report.totals.num_branches == 0 {
return Outcome::Fail(
"branch coverage is required but the report measured no branches".to_string(),
);
}
let actual = report.totals.percent_covered;
let required = f64::from(thresholds.fail_under);
if actual + 1e-9 >= required {
Outcome::Pass
} else {
Outcome::Fail(format!(
"coverage {actual:.2}% is below the required {}%",
thresholds.fail_under
))
}
}
pub fn measure(root: &Path, thresholds: Thresholds, omit: &[String]) -> Result<Outcome> {
let report = run_coverage(root, omit, false)?;
Ok(evaluate(&report, thresholds))
}
pub fn measure_patch_report(root: &Path, omit: &[String]) -> Result<CoverageReport> {
run_coverage(root, omit, true)
}
struct DataFile(PathBuf);
impl DataFile {
fn new() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let name = format!(
"testing-conventions-{}-{}.coverage",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed),
);
DataFile(std::env::temp_dir().join(name))
}
}
impl Drop for DataFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
fn run_coverage(root: &Path, omit: &[String], include_all_sources: bool) -> Result<CoverageReport> {
let data = DataFile::new();
let omit = build_omit(omit);
let mut command = Command::new("coverage");
command
.current_dir(root)
.args(["run", "--branch"])
.arg(format!("--omit={omit}"));
if include_all_sources {
command.arg("--source=.");
}
let run = command
.args(["-m", "pytest", "-q", "-p", "no:cacheprovider", "."])
.env("COVERAGE_FILE", &data.0)
.env("PYTHONDONTWRITEBYTECODE", "1")
.output()
.context("running `coverage run -m pytest` (is coverage.py installed?)")?;
if !run.status.success() {
bail!(
"the unit suite did not run cleanly under coverage in `{}`:\n{}{}",
root.display(),
String::from_utf8_lossy(&run.stdout),
String::from_utf8_lossy(&run.stderr),
);
}
let json = Command::new("coverage")
.current_dir(root)
.args(["json", "-o", "-"])
.env("COVERAGE_FILE", &data.0)
.output()
.context("running `coverage json`")?;
if !json.status.success() {
bail!(
"`coverage json` failed:\n{}",
String::from_utf8_lossy(&json.stderr),
);
}
parse_report(&String::from_utf8_lossy(&json.stdout))
}
fn build_omit(omit: &[String]) -> String {
[TEST_OMIT.to_string(), SUPPORT_OMIT.to_string()]
.into_iter()
.chain(omit.iter().cloned())
.collect::<Vec<_>>()
.join(",")
}
const TS_INCLUDE: &str = "**/*.{ts,tsx,mts,cts}";
const TS_TEST_EXCLUDE: &str = "**/*.test.*";
const TS_DECL_EXCLUDE: &str = "**/*.d.{ts,mts,cts}";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TypeScriptThresholds {
pub lines: u8,
pub branches: u8,
pub functions: u8,
pub statements: u8,
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct VitestReport {
pub total: VitestTotals,
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct VitestTotals {
pub lines: VitestMetric,
pub branches: VitestMetric,
pub functions: VitestMetric,
pub statements: VitestMetric,
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct VitestMetric {
#[serde(deserialize_with = "deserialize_pct")]
pub pct: Option<f64>,
pub total: u64,
}
fn deserialize_pct<'de, D>(deserializer: D) -> std::result::Result<Option<f64>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct PctVisitor;
impl serde::de::Visitor<'_> for PctVisitor {
type Value = Option<f64>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a coverage percent number or the string \"Unknown\"")
}
fn visit_f64<E>(self, value: f64) -> std::result::Result<Self::Value, E> {
Ok(Some(value))
}
fn visit_u64<E>(self, value: u64) -> std::result::Result<Self::Value, E> {
Ok(Some(value as f64))
}
fn visit_str<E>(self, _value: &str) -> std::result::Result<Self::Value, E> {
Ok(None)
}
}
deserializer.deserialize_any(PctVisitor)
}
pub fn parse_vitest_report(json: &str) -> Result<VitestReport> {
serde_json::from_str(json).context("parsing vitest coverage-summary JSON report")
}
pub fn evaluate_typescript(report: &VitestReport, thresholds: TypeScriptThresholds) -> Outcome {
let total = &report.total;
if total.lines.total == 0 {
return Outcome::Fail(
"the unit suite measured no code — check the path and that the suite runs".to_string(),
);
}
let checks = [
("lines", total.lines, thresholds.lines),
("branches", total.branches, thresholds.branches),
("functions", total.functions, thresholds.functions),
("statements", total.statements, thresholds.statements),
];
let mut shortfalls = Vec::new();
for (name, metric, required) in checks {
let actual = metric.pct.unwrap_or(100.0);
if actual + 1e-9 < f64::from(required) {
shortfalls.push(format!("{name} {actual:.2}% < {required}%"));
}
}
if shortfalls.is_empty() {
Outcome::Pass
} else {
Outcome::Fail(format!(
"coverage below thresholds: {}",
shortfalls.join(", ")
))
}
}
pub fn measure_typescript(
root: &Path,
thresholds: TypeScriptThresholds,
exclude: &[String],
) -> Result<Outcome> {
let report = run_vitest(root, exclude)?;
Ok(evaluate_typescript(&report, thresholds))
}
struct ReportDir(PathBuf);
impl ReportDir {
fn new() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let name = format!(
"testing-conventions-vitest-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed),
);
ReportDir(std::env::temp_dir().join(name))
}
}
impl Drop for ReportDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn run_vitest(root: &Path, exclude: &[String]) -> Result<VitestReport> {
let json = run_vitest_coverage(root, exclude, "json-summary", "coverage-summary.json")?;
parse_vitest_report(&json)
}
fn run_vitest_coverage(
root: &Path,
exclude: &[String],
reporter: &str,
report_file: &str,
) -> Result<String> {
let reports = ReportDir::new();
let mut command = Command::new("npx");
command
.current_dir(root)
.args(["--yes", "vitest", "run", "--no-cache"])
.args(["--coverage.enabled", "--coverage.provider=v8"])
.arg(format!("--coverage.reporter={reporter}"))
.arg("--coverage.all=true")
.arg(format!(
"--coverage.reportsDirectory={}",
reports.0.display()
))
.arg(format!("--coverage.include={TS_INCLUDE}"))
.arg(format!("--coverage.exclude={TS_TEST_EXCLUDE}"))
.arg(format!("--coverage.exclude={TS_DECL_EXCLUDE}"));
for path in exclude {
command.arg(format!("--coverage.exclude={path}"));
}
let run = command.env("CI", "1").output().context(
"running `npx vitest run --coverage` (are vitest and @vitest/coverage-v8 installed?)",
)?;
if !run.status.success() {
bail!(
"the unit suite did not run cleanly under vitest in `{}`:\n{}{}",
root.display(),
String::from_utf8_lossy(&run.stdout),
String::from_utf8_lossy(&run.stderr),
);
}
let path = reports.0.join(report_file);
std::fs::read_to_string(&path).with_context(|| {
format!(
"reading vitest coverage report `{}` (did the run produce a {reporter} report?)",
path.display()
)
})
}
#[derive(Debug, Clone, Deserialize)]
struct IstanbulFile {
#[serde(rename = "statementMap", default)]
statement_map: BTreeMap<String, IstanbulSpan>,
#[serde(default)]
s: BTreeMap<String, u64>,
#[serde(rename = "branchMap", default)]
branch_map: BTreeMap<String, IstanbulBranch>,
#[serde(default)]
b: BTreeMap<String, Vec<u64>>,
#[serde(rename = "fnMap", default)]
fn_map: BTreeMap<String, IstanbulFn>,
#[serde(default)]
f: BTreeMap<String, u64>,
}
#[derive(Debug, Clone, Deserialize)]
struct IstanbulSpan {
start: IstanbulPos,
end: IstanbulPos,
}
#[derive(Debug, Clone, Deserialize)]
struct IstanbulPos {
line: u64,
}
#[derive(Debug, Clone, Deserialize)]
struct IstanbulBranch {
loc: IstanbulSpan,
}
#[derive(Debug, Clone, Deserialize)]
struct IstanbulFn {
decl: IstanbulSpan,
}
#[derive(Debug, Clone, Default)]
pub struct TsPatchCoverage {
pub statements: Vec<(u64, u64, bool)>,
pub branch_arms: Vec<(u64, bool)>,
pub functions: Vec<(u64, bool)>,
}
pub fn measure_patch_typescript_detail(
root: &Path,
exclude: &[String],
) -> Result<BTreeMap<String, TsPatchCoverage>> {
let json = run_vitest_coverage(root, exclude, "json", "coverage-final.json")?;
istanbul_patch_detail(&json)
}
fn istanbul_patch_detail(json: &str) -> Result<BTreeMap<String, TsPatchCoverage>> {
let files: BTreeMap<String, IstanbulFile> = serde_json::from_str(json)
.context("parsing vitest coverage-final (Istanbul) JSON report")?;
let mut out = BTreeMap::new();
for (path, file) in files {
let mut detail = TsPatchCoverage::default();
for (id, span) in &file.statement_map {
let covered = file.s.get(id).is_some_and(|&count| count > 0);
detail
.statements
.push((span.start.line, span.end.line, covered));
}
for (id, branch) in &file.branch_map {
let line = branch.loc.start.line;
if let Some(counts) = file.b.get(id) {
for &count in counts {
detail.branch_arms.push((line, count > 0));
}
}
}
for (id, function) in &file.fn_map {
let covered = file.f.get(id).is_some_and(|&count| count > 0);
detail.functions.push((function.decl.start.line, covered));
}
out.insert(path, detail);
}
Ok(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RustThresholds {
pub regions: u8,
pub lines: u8,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LlvmCovReport {
pub data: Vec<LlvmCovData>,
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct LlvmCovData {
pub totals: LlvmCovTotals,
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct LlvmCovTotals {
pub regions: LlvmCovMetric,
pub lines: LlvmCovMetric,
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct LlvmCovMetric {
pub count: u64,
pub covered: u64,
pub percent: f64,
}
pub fn parse_llvm_cov_report(json: &str) -> Result<LlvmCovReport> {
serde_json::from_str(json).context("parsing cargo llvm-cov JSON report")
}
pub fn evaluate_rust(report: &LlvmCovReport, thresholds: RustThresholds) -> Outcome {
let Some(totals) = report.data.first().map(|entry| &entry.totals) else {
return Outcome::Fail("the cargo llvm-cov report contained no data".to_string());
};
if totals.regions.count == 0 {
return Outcome::Fail(
"the unit suite measured no code — check the path and that the suite runs".to_string(),
);
}
let checks = [
("regions", totals.regions.percent, thresholds.regions),
("lines", totals.lines.percent, thresholds.lines),
];
let mut shortfalls = Vec::new();
for (name, actual, required) in checks {
if actual + 1e-9 < f64::from(required) {
shortfalls.push(format!("{name} {actual:.2}% < {required}%"));
}
}
if shortfalls.is_empty() {
Outcome::Pass
} else {
Outcome::Fail(format!(
"coverage below thresholds: {}",
shortfalls.join(", ")
))
}
}
pub fn measure_rust(root: &Path, thresholds: RustThresholds, ignore: &[String]) -> Result<Outcome> {
let report = run_llvm_cov(root, ignore)?;
Ok(evaluate_rust(&report, thresholds))
}
struct TargetDir(PathBuf);
impl TargetDir {
fn new() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let name = format!(
"testing-conventions-llvm-cov-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed),
);
TargetDir(std::env::temp_dir().join(name))
}
}
impl Drop for TargetDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn run_llvm_cov(root: &Path, ignore: &[String]) -> Result<LlvmCovReport> {
parse_llvm_cov_report(&run_cargo_llvm_cov(
root,
ignore,
&["--json", "--summary-only"],
)?)
}
fn run_cargo_llvm_cov(root: &Path, ignore: &[String], format: &[&str]) -> Result<String> {
let target = TargetDir::new();
let mut command = Command::new("cargo");
command
.current_dir(root)
.arg("llvm-cov")
.args(format)
.env("CARGO_TARGET_DIR", &target.0);
if let Some(regex) = ignore_filename_regex(ignore) {
command.arg("--ignore-filename-regex").arg(regex);
}
for var in [
"RUSTFLAGS",
"CARGO_ENCODED_RUSTFLAGS",
"RUSTDOCFLAGS",
"CARGO_ENCODED_RUSTDOCFLAGS",
"LLVM_PROFILE_FILE",
"CARGO_LLVM_COV",
"CARGO_LLVM_COV_SHOW_ENV",
"CARGO_LLVM_COV_TARGET_DIR",
"CARGO_LLVM_COV_BUILD_DIR",
"RUSTC_WRAPPER",
"RUSTC_WORKSPACE_WRAPPER",
"__CARGO_LLVM_COV_RUSTC_WRAPPER",
"__CARGO_LLVM_COV_RUSTC_WRAPPER_RUSTFLAGS",
"__CARGO_LLVM_COV_RUSTC_WRAPPER_CRATE_NAMES",
] {
command.env_remove(var);
}
let output = command
.output()
.context("running `cargo llvm-cov` (is cargo-llvm-cov installed?)")?;
if !output.status.success() {
bail!(
"the unit suite did not run cleanly under cargo llvm-cov in `{}`:\n{}{}",
root.display(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[derive(Debug, Clone, Default)]
pub struct RustPatchCoverage {
pub regions: Vec<(u64, u64, bool)>,
}
#[derive(Debug, Clone, Deserialize)]
struct LlvmCovExport {
data: Vec<LlvmCovExportData>,
}
#[derive(Debug, Clone, Deserialize)]
struct LlvmCovExportData {
files: Vec<LlvmCovExportFile>,
functions: Vec<LlvmCovFunction>,
}
#[derive(Debug, Clone, Deserialize)]
struct LlvmCovExportFile {
filename: String,
}
#[derive(Debug, Clone, Deserialize)]
struct LlvmCovFunction {
filenames: Vec<String>,
regions: Vec<Vec<i64>>,
}
pub fn measure_patch_rust_detail(
root: &Path,
ignore: &[String],
) -> Result<BTreeMap<String, RustPatchCoverage>> {
llvm_cov_patch_detail(&run_cargo_llvm_cov(root, ignore, &["--json"])?)
}
fn llvm_cov_patch_detail(json: &str) -> Result<BTreeMap<String, RustPatchCoverage>> {
let export: LlvmCovExport =
serde_json::from_str(json).context("parsing cargo llvm-cov JSON export")?;
let mut out: BTreeMap<String, RustPatchCoverage> = BTreeMap::new();
for data in &export.data {
let measured: BTreeSet<&str> = data.files.iter().map(|f| f.filename.as_str()).collect();
for function in &data.functions {
for region in &function.regions {
if region.len() < 8 {
continue;
}
if region[7] != 0 {
continue;
}
let file_id = region[5];
let Ok(file_id) = usize::try_from(file_id) else {
continue;
};
let Some(file) = function.filenames.get(file_id) else {
continue;
};
if !measured.contains(file.as_str()) {
continue;
}
let start = region[0].max(0) as u64;
let end = region[2].max(0) as u64;
let covered = region[4] > 0;
out.entry(file.clone())
.or_default()
.regions
.push((start, end, covered));
}
}
}
Ok(out)
}
fn ignore_filename_regex(ignore: &[String]) -> Option<String> {
if ignore.is_empty() {
return None;
}
Some(
ignore
.iter()
.map(|path| regex_escape(path))
.collect::<Vec<_>>()
.join("|"),
)
}
fn regex_escape(s: &str) -> String {
const META: &str = r"\.+*?()|[]{}^$";
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if META.contains(c) {
out.push('\\');
}
out.push(c);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn report(percent_covered: f64, num_branches: u64) -> CoverageReport {
CoverageReport {
totals: Totals {
percent_covered,
num_branches,
},
files: BTreeMap::new(),
}
}
#[test]
fn passes_when_total_meets_the_floor() {
assert_eq!(
evaluate(
&report(100.0, 12),
Thresholds {
fail_under: 100,
branch: true
}
),
Outcome::Pass
);
}
#[test]
fn fails_when_total_is_below_the_floor() {
assert!(matches!(
evaluate(
&report(80.0, 12),
Thresholds {
fail_under: 100,
branch: true
}
),
Outcome::Fail(_)
));
}
#[test]
fn fails_when_branch_required_but_unmeasured() {
assert!(matches!(
evaluate(
&report(100.0, 0),
Thresholds {
fail_under: 90,
branch: true
}
),
Outcome::Fail(_)
));
}
#[test]
fn parses_a_coverage_py_report() {
let json = r#"{"totals":{"percent_covered":91.5,"num_branches":8,"covered_lines":91}}"#;
let report = parse_report(json).expect("valid coverage.py json");
assert_eq!(report.totals.percent_covered, 91.5);
assert_eq!(report.totals.num_branches, 8);
}
#[test]
fn parses_the_per_file_block_for_patch_coverage() {
let json = r#"{
"files": {
"widget.py": {
"executed_lines": [1, 2, 3, 4, 6],
"summary": {"percent_covered": 85.0},
"missing_lines": [5],
"excluded_lines": [],
"missing_branches": [[4, 5]]
}
},
"totals": {"percent_covered": 85.0, "num_branches": 4}
}"#;
let report = parse_report(json).expect("valid coverage.py json with files");
let widget = report.files.get("widget.py").expect("widget.py is present");
assert_eq!(widget.missing_lines, vec![5]);
assert_eq!(widget.missing_branches, vec![vec![4, 5]]);
assert_eq!(report.totals.percent_covered, 85.0);
}
#[test]
fn a_report_without_a_files_block_parses_with_an_empty_map() {
let report = parse_report(r#"{"totals":{"percent_covered":100.0,"num_branches":2}}"#)
.expect("valid coverage.py json");
assert!(report.files.is_empty());
}
#[test]
fn omit_is_the_test_and_support_globs_when_nothing_is_exempt() {
assert_eq!(build_omit(&[]), "*_test.py,*conftest.py");
}
#[test]
fn omit_folds_in_the_exempt_paths_after_the_test_glob() {
let exempt = vec!["pkg/gen.py".to_string(), "shim.py".to_string()];
assert_eq!(
build_omit(&exempt),
"*_test.py,*conftest.py,pkg/gen.py,shim.py"
);
}
fn metric(pct: f64) -> VitestMetric {
VitestMetric {
pct: Some(pct),
total: 10,
}
}
fn ts_report(lines: f64, branches: f64, functions: f64, statements: f64) -> VitestReport {
VitestReport {
total: VitestTotals {
lines: metric(lines),
branches: metric(branches),
functions: metric(functions),
statements: metric(statements),
},
}
}
const TS_FULL: TypeScriptThresholds = TypeScriptThresholds {
lines: 100,
branches: 100,
functions: 100,
statements: 100,
};
const TS_MID: TypeScriptThresholds = TypeScriptThresholds {
lines: 80,
branches: 75,
functions: 80,
statements: 80,
};
#[test]
fn typescript_passes_when_every_metric_meets_its_floor() {
assert_eq!(
evaluate_typescript(&ts_report(100.0, 100.0, 100.0, 100.0), TS_FULL),
Outcome::Pass
);
}
#[test]
fn typescript_fails_on_the_one_metric_below_its_floor() {
let outcome = evaluate_typescript(&ts_report(100.0, 66.66, 100.0, 100.0), TS_MID);
assert!(
matches!(&outcome, Outcome::Fail(message) if message.contains("branches") && !message.contains("lines")),
"got: {outcome:?}"
);
}
#[test]
fn typescript_fail_message_names_every_metric_below() {
let outcome = evaluate_typescript(&ts_report(70.0, 70.0, 70.0, 70.0), TS_MID);
assert!(
matches!(&outcome, Outcome::Fail(message)
if message.contains("lines")
&& message.contains("branches")
&& message.contains("functions")
&& message.contains("statements")),
"got: {outcome:?}"
);
}
#[test]
fn typescript_tolerates_float_noise_at_the_floor() {
assert_eq!(
evaluate_typescript(&ts_report(99.999_999_999, 100.0, 100.0, 100.0), TS_FULL),
Outcome::Pass
);
}
#[test]
fn typescript_empty_denominator_metric_is_vacuously_satisfied() {
let report = VitestReport {
total: VitestTotals {
lines: metric(100.0),
branches: VitestMetric {
pct: None,
total: 0,
},
functions: metric(100.0),
statements: metric(100.0),
},
};
assert_eq!(evaluate_typescript(&report, TS_FULL), Outcome::Pass);
}
#[test]
fn typescript_fails_a_vacuous_run_that_measured_no_code() {
let nothing = VitestMetric {
pct: None,
total: 0,
};
let report = VitestReport {
total: VitestTotals {
lines: nothing,
branches: nothing,
functions: nothing,
statements: nothing,
},
};
let outcome = evaluate_typescript(&report, TS_MID);
assert!(
matches!(&outcome, Outcome::Fail(message) if message.contains("measured no code")),
"got: {outcome:?}"
);
}
#[test]
fn parses_a_vitest_summary_report() {
let json = r#"{
"total": {
"lines": {"total": 5, "covered": 4, "skipped": 0, "pct": 80},
"statements": {"total": 5, "covered": 4, "skipped": 0, "pct": 80},
"functions": {"total": 2, "covered": 2, "skipped": 0, "pct": 100},
"branches": {"total": 3, "covered": 2, "skipped": 0, "pct": 66.66},
"branchesTrue": {"total": 0, "covered": 0, "skipped": 0, "pct": "Unknown"}
},
"/abs/widget.ts": {
"lines": {"total": 5, "covered": 4, "skipped": 0, "pct": 80}
}
}"#;
let report = parse_vitest_report(json).expect("valid vitest json-summary");
assert_eq!(report.total.lines.pct, Some(80.0));
assert_eq!(report.total.branches.pct, Some(66.66));
assert_eq!(report.total.functions.total, 2);
}
#[test]
fn parses_an_unknown_pct_as_unmeasured() {
let json = r#"{"total": {
"lines": {"total": 0, "covered": 0, "skipped": 0, "pct": "Unknown"},
"statements": {"total": 0, "covered": 0, "skipped": 0, "pct": "Unknown"},
"functions": {"total": 0, "covered": 0, "skipped": 0, "pct": "Unknown"},
"branches": {"total": 0, "covered": 0, "skipped": 0, "pct": "Unknown"}
}}"#;
let report = parse_vitest_report(json).expect("valid vitest json-summary");
assert_eq!(report.total.lines.pct, None);
assert_eq!(report.total.lines.total, 0);
}
#[test]
fn a_pct_that_is_neither_number_nor_string_is_a_parse_error() {
let json = r#"{"total":{
"lines": {"total": 1, "covered": 1, "skipped": 0, "pct": true},
"statements": {"total": 1, "covered": 1, "skipped": 0, "pct": 100},
"functions": {"total": 1, "covered": 1, "skipped": 0, "pct": 100},
"branches": {"total": 1, "covered": 1, "skipped": 0, "pct": 100}
}}"#;
assert!(parse_vitest_report(json).is_err());
}
fn rust_metric(percent: f64) -> LlvmCovMetric {
LlvmCovMetric {
count: 10,
covered: 10,
percent,
}
}
fn rust_report(regions: f64, lines: f64) -> LlvmCovReport {
LlvmCovReport {
data: vec![LlvmCovData {
totals: LlvmCovTotals {
regions: rust_metric(regions),
lines: rust_metric(lines),
},
}],
}
}
const RUST_FULL: RustThresholds = RustThresholds {
regions: 100,
lines: 100,
};
const RUST_MID: RustThresholds = RustThresholds {
regions: 80,
lines: 85,
};
#[test]
fn rust_passes_when_both_metrics_meet_their_floor() {
assert_eq!(
evaluate_rust(&rust_report(100.0, 100.0), RUST_FULL),
Outcome::Pass
);
}
#[test]
fn rust_fails_on_the_one_metric_below_its_floor() {
let outcome = evaluate_rust(&rust_report(70.0, 100.0), RUST_MID);
assert!(
matches!(&outcome, Outcome::Fail(message) if message.contains("regions") && !message.contains("lines")),
"got: {outcome:?}"
);
}
#[test]
fn rust_fail_message_names_every_metric_below() {
let outcome = evaluate_rust(&rust_report(50.0, 50.0), RUST_MID);
assert!(
matches!(&outcome, Outcome::Fail(message)
if message.contains("regions") && message.contains("lines")),
"got: {outcome:?}"
);
}
#[test]
fn rust_tolerates_float_noise_at_the_floor() {
assert_eq!(
evaluate_rust(&rust_report(99.999_999_999, 100.0), RUST_FULL),
Outcome::Pass
);
}
#[test]
fn rust_fails_a_vacuous_run_that_measured_no_code() {
let nothing = LlvmCovMetric {
count: 0,
covered: 0,
percent: 0.0,
};
let report = LlvmCovReport {
data: vec![LlvmCovData {
totals: LlvmCovTotals {
regions: nothing,
lines: nothing,
},
}],
};
let outcome = evaluate_rust(&report, RUST_MID);
assert!(
matches!(&outcome, Outcome::Fail(message) if message.contains("measured no code")),
"got: {outcome:?}"
);
}
#[test]
fn rust_fails_an_export_with_no_data() {
let report = LlvmCovReport { data: vec![] };
assert!(matches!(evaluate_rust(&report, RUST_MID), Outcome::Fail(_)));
}
#[test]
fn parses_a_cargo_llvm_cov_report() {
let json = r#"{
"data": [{"totals": {
"regions": {"count": 12, "covered": 9, "notcovered": 3, "percent": 75.0},
"lines": {"count": 20, "covered": 18, "percent": 90.0},
"functions": {"count": 3, "covered": 3, "percent": 100.0}
}}],
"type": "llvm.coverage.json.export",
"version": "2.0.1"
}"#;
let report = parse_llvm_cov_report(json).expect("valid llvm-cov json");
assert_eq!(report.data[0].totals.regions.percent, 75.0);
assert_eq!(report.data[0].totals.lines.count, 20);
}
#[test]
fn llvm_cov_patch_detail_reads_code_regions_per_file() {
let json = r#"{
"data": [{
"files": [{"filename": "/abs/grade.rs"}],
"functions": [{
"filenames": ["/abs/grade.rs"],
"regions": [
[6, 5, 6, 26, 1, 0, 0, 0],
[10, 9, 10, 17, 0, 0, 0, 0]
]
}],
"totals": {}
}],
"type": "llvm.coverage.json.export",
"version": "3.0.1"
}"#;
let out = llvm_cov_patch_detail(json).expect("valid llvm-cov export");
assert_eq!(
out["/abs/grade.rs"].regions,
vec![(6, 6, true), (10, 10, false)]
);
}
#[test]
fn llvm_cov_patch_detail_skips_non_code_regions() {
let json = r#"{
"data": [{
"files": [{"filename": "/abs/a.rs"}],
"functions": [{
"filenames": ["/abs/a.rs"],
"regions": [
[1, 1, 1, 10, 2, 0, 0, 0],
[2, 1, 2, 10, 0, 0, 0, 1],
[3, 1, 3, 10, 0, 0, 0, 2]
]
}]
}]
}"#;
let out = llvm_cov_patch_detail(json).expect("valid llvm-cov export");
assert_eq!(out["/abs/a.rs"].regions, vec![(1, 1, true)]);
}
#[test]
fn llvm_cov_patch_detail_groups_regions_by_filename_id() {
let json = r#"{
"data": [{
"files": [{"filename": "/abs/a.rs"}, {"filename": "/abs/b.rs"}],
"functions": [{
"filenames": ["/abs/a.rs", "/abs/b.rs"],
"regions": [
[1, 1, 1, 5, 1, 0, 0, 0],
[9, 1, 9, 5, 0, 1, 1, 0]
]
}]
}]
}"#;
let out = llvm_cov_patch_detail(json).expect("valid llvm-cov export");
assert_eq!(out["/abs/a.rs"].regions, vec![(1, 1, true)]);
assert_eq!(out["/abs/b.rs"].regions, vec![(9, 9, false)]);
}
#[test]
fn llvm_cov_patch_detail_skips_a_malformed_short_region() {
let json = r#"{
"data": [{
"files": [{"filename": "/abs/a.rs"}],
"functions": [{
"filenames": ["/abs/a.rs"],
"regions": [
[4, 1, 4],
[5, 1, 5, 9, 1, 0, 0, 0]
]
}]
}]
}"#;
let out = llvm_cov_patch_detail(json).expect("valid llvm-cov export");
assert_eq!(out["/abs/a.rs"].regions, vec![(5, 5, true)]);
}
#[test]
fn llvm_cov_patch_detail_spans_a_multiline_region() {
let json = r#"{
"data": [{
"files": [{"filename": "/abs/a.rs"}],
"functions": [{
"filenames": ["/abs/a.rs"],
"regions": [[3, 5, 5, 6, 0, 0, 0, 0]]
}]
}]
}"#;
let out = llvm_cov_patch_detail(json).expect("valid llvm-cov export");
assert_eq!(out["/abs/a.rs"].regions, vec![(3, 5, false)]);
}
#[test]
fn llvm_cov_patch_detail_drops_a_file_absent_from_the_files_allowlist() {
let json = r#"{
"data": [{
"files": [{"filename": "/abs/kept.rs"}],
"functions": [{
"filenames": ["/abs/kept.rs", "/abs/ignored.rs"],
"regions": [
[1, 1, 1, 9, 1, 0, 0, 0],
[2, 1, 2, 9, 0, 1, 0, 0]
]
}]
}]
}"#;
let out = llvm_cov_patch_detail(json).expect("valid llvm-cov export");
assert_eq!(out["/abs/kept.rs"].regions, vec![(1, 1, true)]);
assert!(!out.contains_key("/abs/ignored.rs"));
}
#[test]
fn llvm_cov_patch_detail_malformed_json_is_an_error() {
assert!(llvm_cov_patch_detail("{ not json").is_err());
}
#[test]
fn rust_ignore_regex_is_none_when_nothing_is_exempt() {
assert_eq!(ignore_filename_regex(&[]), None);
}
#[test]
fn rust_ignore_regex_escapes_and_joins_exempt_paths() {
let exempt = vec!["src/shim.rs".to_string(), "src/gen.rs".to_string()];
assert_eq!(
ignore_filename_regex(&exempt).as_deref(),
Some(r"src/shim\.rs|src/gen\.rs")
);
}
}