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>>,
}
#[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()
)
})
}
pub fn measure_patch_typescript(
root: &Path,
exclude: &[String],
) -> Result<BTreeMap<String, BTreeSet<u64>>> {
let json = run_vitest_coverage(root, exclude, "json", "coverage-final.json")?;
uncovered_istanbul_lines(&json)
}
#[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>>,
}
#[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,
}
fn uncovered_istanbul_lines(json: &str) -> Result<BTreeMap<String, BTreeSet<u64>>> {
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 lines = BTreeSet::new();
for (id, span) in &file.statement_map {
if file.s.get(id) == Some(&0) {
lines.extend(span.start.line..=span.end.line);
}
}
for (id, branch) in &file.branch_map {
if file.b.get(id).is_some_and(|counts| counts.contains(&0)) {
lines.insert(branch.loc.start.line);
}
}
out.insert(path, lines);
}
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())
}
pub fn measure_patch_rust(
root: &Path,
ignore: &[String],
) -> Result<BTreeMap<String, BTreeSet<u64>>> {
Ok(uncovered_lcov_lines(&run_cargo_llvm_cov(
root,
ignore,
&["--lcov"],
)?))
}
fn uncovered_lcov_lines(lcov: &str) -> BTreeMap<String, BTreeSet<u64>> {
let mut out: BTreeMap<String, BTreeSet<u64>> = BTreeMap::new();
let mut current: Option<String> = None;
for line in lcov.lines() {
if let Some(path) = line.strip_prefix("SF:") {
let path = path.trim().to_string();
out.entry(path.clone()).or_default();
current = Some(path);
} else if let Some(rest) = line.strip_prefix("DA:") {
if let Some(file) = ¤t {
let mut fields = rest.split(',');
if let (Some(line_no), Some(count)) = (fields.next(), fields.next()) {
if let (Ok(line_no), Ok(0)) =
(line_no.trim().parse::<u64>(), count.trim().parse::<u64>())
{
out.entry(file.clone()).or_default().insert(line_no);
}
}
}
} else if line.trim() == "end_of_record" {
current = None;
}
}
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());
}
#[test]
fn istanbul_flags_an_unexecuted_statement() {
let json = r#"{"/abs/widget.ts":{
"statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":40}},
"1":{"start":{"line":2,"column":2},"end":{"line":2,"column":20}}},
"s":{"0":1,"1":0},
"branchMap":{},"b":{}
}}"#;
let out = uncovered_istanbul_lines(json).unwrap();
assert_eq!(out["/abs/widget.ts"], BTreeSet::from([2]));
}
#[test]
fn istanbul_flags_an_untaken_branch_source() {
let json = r#"{"/abs/widget.ts":{
"statementMap":{"0":{"start":{"line":3,"column":2},"end":{"line":3,"column":20}}},
"s":{"0":5},
"branchMap":{"0":{"loc":{"start":{"line":3,"column":2},"end":{"line":3,"column":40}}}},
"b":{"0":[4,0]}
}}"#;
let out = uncovered_istanbul_lines(json).unwrap();
assert_eq!(out["/abs/widget.ts"], BTreeSet::from([3]));
}
#[test]
fn istanbul_v8_single_arm_branch_counts_as_uncovered() {
let json = r#"{"/abs/widget.ts":{
"statementMap":{},"s":{},
"branchMap":{"0":{"loc":{"start":{"line":7,"column":0},"end":{"line":7,"column":3}}}},
"b":{"0":[0]}
}}"#;
let out = uncovered_istanbul_lines(json).unwrap();
assert_eq!(out["/abs/widget.ts"], BTreeSet::from([7]));
}
#[test]
fn istanbul_spans_every_line_of_an_unexecuted_multiline_statement() {
let json = r#"{"/abs/widget.ts":{
"statementMap":{"0":{"start":{"line":4,"column":2},"end":{"line":6,"column":3}}},
"s":{"0":0},
"branchMap":{},"b":{}
}}"#;
let out = uncovered_istanbul_lines(json).unwrap();
assert_eq!(out["/abs/widget.ts"], BTreeSet::from([4, 5, 6]));
}
#[test]
fn istanbul_fully_covered_file_has_no_uncovered_lines() {
let json = r#"{"/abs/widget.ts":{
"statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":40}}},
"s":{"0":3},
"branchMap":{"0":{"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":40}}}},
"b":{"0":[2,1]}
}}"#;
let out = uncovered_istanbul_lines(json).unwrap();
assert!(out["/abs/widget.ts"].is_empty());
}
#[test]
fn istanbul_widget_report_flags_statement_and_branch_lines() {
let json = r#"{"/abs/widget.ts":{
"statementMap":{
"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":43}},
"1":{"start":{"line":2,"column":2},"end":{"line":2,"column":25}},
"2":{"start":{"line":3,"column":2},"end":{"line":3,"column":16}},
"3":{"start":{"line":4,"column":4},"end":{"line":4,"column":20}},
"4":{"start":{"line":5,"column":2},"end":{"line":5,"column":3}},
"5":{"start":{"line":6,"column":2},"end":{"line":6,"column":15}}
},
"s":{"0":1,"1":2,"2":2,"3":0,"4":0,"5":1},
"branchMap":{
"0":{"loc":{"start":{"line":2,"column":2},"end":{"line":2,"column":25}}},
"1":{"loc":{"start":{"line":3,"column":2},"end":{"line":3,"column":16}}}
},
"b":{"0":[2],"1":[0]}
}}"#;
let out = uncovered_istanbul_lines(json).unwrap();
assert_eq!(out["/abs/widget.ts"], BTreeSet::from([3, 4, 5]));
}
#[test]
fn istanbul_malformed_json_is_an_error() {
assert!(uncovered_istanbul_lines("{ not 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 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")
);
}
#[test]
fn lcov_flags_an_unexecuted_line() {
let lcov = "SF:/abs/grade.rs\nDA:6,1\nDA:7,1\nDA:8,1\nDA:10,0\nDA:12,1\nend_of_record\n";
let out = uncovered_lcov_lines(lcov);
assert_eq!(out["/abs/grade.rs"], BTreeSet::from([10]));
}
#[test]
fn lcov_a_fully_covered_file_maps_to_an_empty_set() {
let lcov = "SF:/abs/widget.rs\nDA:1,2\nDA:2,1\nend_of_record\n";
let out = uncovered_lcov_lines(lcov);
assert!(out["/abs/widget.rs"].is_empty());
}
#[test]
fn lcov_groups_uncovered_lines_by_source_file() {
let lcov =
"SF:/abs/a.rs\nDA:3,0\nend_of_record\nSF:/abs/b.rs\nDA:5,1\nDA:6,0\nend_of_record\n";
let out = uncovered_lcov_lines(lcov);
assert_eq!(out["/abs/a.rs"], BTreeSet::from([3]));
assert_eq!(out["/abs/b.rs"], BTreeSet::from([6]));
}
#[test]
fn lcov_a_da_record_outside_a_file_is_ignored() {
let lcov = "DA:9,0\nSF:/abs/a.rs\nDA:1,1\nend_of_record\nDA:2,0\n";
let out = uncovered_lcov_lines(lcov);
assert_eq!(out.len(), 1);
assert!(out["/abs/a.rs"].is_empty());
}
#[test]
fn lcov_a_checksummed_da_record_parses() {
let lcov = "SF:/abs/a.rs\nDA:4,0,abc123\nend_of_record\n";
let out = uncovered_lcov_lines(lcov);
assert_eq!(out["/abs/a.rs"], BTreeSet::from([4]));
}
}