ci/verbs/tests/coverage_verb.rs
//! The nightly coverage step (OSSF-NEXT, silver prep): `ci-verbs coverage` runs
//! `cargo llvm-cov` over a checkout's chosen packages and writes the numbers the CI
//! status page shows: the totals (lines, functions, regions) and one row per crate.
use std::path::Path;
use ci_verbs::coverage;
use serde_json::json;
fn export() -> serde_json::Value {
// the shape `cargo llvm-cov --json --summary-only` writes (llvm-cov export 2.0.1)
let f = |name: &str, lc: u64, ln: u64, fc: u64, fnn: u64| {
json!({ "filename": name, "summary": {
"lines": { "count": ln, "covered": lc, "percent": 100.0 * lc as f64 / ln as f64 },
"functions": { "count": fnn, "covered": fc, "percent": 100.0 * fc as f64 / fnn as f64 },
"regions": { "count": ln * 2, "covered": lc * 2, "percent": 100.0 * lc as f64 / ln as f64 }
}})
};
json!({
"type": "llvm.coverage.json.export",
"version": "2.0.1",
"data": [{
"files": [
f("/src/gunnar/crates/gunnar-wire/src/pkt.rs", 90, 100, 9, 10),
f("/src/gunnar/crates/gunnar-wire/src/pack.rs", 30, 100, 3, 10),
f("/src/gunnar/crates/gunnar-crypt/src/lib.rs", 80, 100, 8, 10),
f("/src/gunnar/xtask/src/main.rs", 0, 50, 0, 5),
f("/home/u/.cargo/registry/src/x/lib.rs", 1, 1, 1, 1)
],
"totals": {
"lines": { "count": 350, "covered": 200, "percent": 57.142857 },
"functions": { "count": 35, "covered": 20, "percent": 57.142857 },
"regions": { "count": 700, "covered": 400, "percent": 57.142857 }
}
}]
})
}
#[test]
fn the_totals_and_one_row_per_crate_come_from_the_export() {
let e = export();
let t = coverage::totals(&e).unwrap();
assert_eq!((t.lines.covered, t.lines.count), (200, 350));
assert_eq!((t.functions.covered, t.functions.count), (20, 35));
assert_eq!((t.regions.covered, t.regions.count), (400, 700));
assert!((t.lines.percent() - 57.14).abs() < 0.01);
let rows = coverage::per_crate(&e, Path::new("/src/gunnar"));
let names: Vec<&str> = rows.iter().map(|r| r.name.as_str()).collect();
assert_eq!(names, ["crates/gunnar-crypt", "crates/gunnar-wire", "xtask"], "files outside the checkout are not a crate of it");
let wire = &rows[1];
assert_eq!((wire.lines.covered, wire.lines.count), (120, 200));
assert!(coverage::totals(&json!({ "data": [] })).is_err(), "an empty export is refused");
}
#[test]
fn the_command_is_cargo_llvm_cov_over_the_named_packages() {
let a = coverage::args(&["gunnar-wire".into(), "gunnar-crypt".into()], &[], Path::new("/o/llvm-cov-export.data"));
let line = a.join(" ");
assert!(line.starts_with("llvm-cov --json --summary-only --output-path /o/llvm-cov-export.data"), "{line}");
assert!(line.contains("-p gunnar-wire -p gunnar-crypt"), "{line}");
assert!(!line.contains("--workspace"), "{line}");
let all = coverage::args(&[], &["metered".into()], Path::new("/o/x")).join(" ");
assert!(all.contains("--workspace") && all.contains("--features metered"), "{all}");
}
#[test]
fn a_skipped_test_is_handed_to_every_test_binary() {
let a = coverage::args_skipping(&[], &[], &["needs_podman".into(), "needs_a_tag".into()], Path::new("/o/x"));
let line = a.join(" ");
assert!(line.ends_with("--workspace -- --skip needs_podman --skip needs_a_tag"), "{line}");
let none = coverage::args_skipping(&[], &[], &[], Path::new("/o/x"));
assert_eq!(none, coverage::args(&[], &[], Path::new("/o/x")), "no skip, no `--`");
}
#[test]
fn the_page_shows_the_numbers_and_a_floor_turns_a_drop_red() {
let e = export();
let t = coverage::totals(&e).unwrap();
let rows = coverage::per_crate(&e, Path::new("/src/gunnar"));
let md = coverage::summary("gunnar", "abcdef0123456789", &t, &rows, Some(50.0));
assert!(md.contains("**57.1 %** of lines"), "{md}");
assert!(md.contains("| crates/gunnar-wire | 60.0 % (120 / 200) |"), "{md}");
assert!(md.contains("`abcdef01`"), "{md}");
assert!(coverage::below_floor(&t, Some(50.0)).is_none());
let why = coverage::below_floor(&t, Some(80.0)).expect("57 % is under an 80 % floor");
assert!(why.contains("80"), "{why}");
assert!(coverage::below_floor(&t, None).is_none(), "no floor, never red for the number");
}
#[test]
fn the_verb_says_usage_and_names_a_missing_cargo_llvm_cov() {
assert_eq!(coverage::verb(&[]), 2);
let d = std::env::temp_dir().join(format!("ci-verbs-cov-{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
let args: Vec<String> = ["--checkout", d.to_str().unwrap(), "--out", d.join("out").to_str().unwrap(), "--cargo", "/nonexistent/cargo"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(coverage::verb(&args), 1);
let md = std::fs::read_to_string(d.join("out/summary.md")).unwrap();
assert!(md.contains("cargo llvm-cov"), "the page says what could not run: {md}");
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn a_crate_is_the_nearest_directory_with_a_manifest() {
// Holger keeps crates at server/lib and server/inspect: one row each, not one "server"
let d = std::env::temp_dir().join(format!("ci-verbs-cov-crates-{}", std::process::id()));
for c in ["server/lib", "server/inspect", "traits"] {
std::fs::create_dir_all(d.join(c).join("src")).unwrap();
std::fs::write(d.join(c).join("Cargo.toml"), "[package]\n").unwrap();
}
let f = |p: &str| json!({ "filename": d.join(p).display().to_string(), "summary": { "lines": { "count": 10, "covered": 5 } } });
let e = json!({ "data": [{ "files": [f("server/lib/src/a.rs"), f("server/inspect/src/b.rs"), f("traits/src/c.rs")], "totals": {} }] });
let names: Vec<String> = coverage::per_crate(&e, &d).into_iter().map(|r| r.name).collect();
assert_eq!(names, ["server/inspect", "server/lib", "traits"]);
let _ = std::fs::remove_dir_all(d);
}