#![cfg(not(miri))] use std::fs;
use std::path::Path;
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
fn make_workspace(dir: &Path, members: &[(&str, Option<&str>)], workspace_min_lines_percent: Option<&str>) {
let names: Vec<&&str> = members.iter().map(|(n, _)| n).collect();
let members_list = names.iter().map(|n| format!("\"{n}\"")).collect::<Vec<_>>().join(", ");
let workspace_meta = workspace_min_lines_percent
.map(|m| format!("\n[workspace.metadata.coverage-gate]\nmin-lines-percent = {m}\n"))
.unwrap_or_default();
fs::write(
dir.join("Cargo.toml"),
format!("[workspace]\nresolver = \"2\"\nmembers = [{members_list}]\n{workspace_meta}"),
)
.expect("write workspace root Cargo.toml");
for (name, min_lines_percent) in members {
let member_dir = dir.join(name);
fs::create_dir_all(member_dir.join("src")).expect("mkdir member src");
let metadata = min_lines_percent
.map(|m| format!("\n[package.metadata.coverage-gate]\nmin-lines-percent = {m}\n"))
.unwrap_or_default();
fs::write(
member_dir.join("Cargo.toml"),
format!(
r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"
{metadata}"#
),
)
.expect("write member Cargo.toml");
fs::write(member_dir.join("src/lib.rs"), "// empty\n").expect("write lib.rs");
}
}
fn make_coverage_lcov(dir: &Path, files: &[(&str, u32, u32)]) -> String {
use std::fmt::Write as _;
let mut out = String::new();
for (rel, count, covered) in files {
let full = dir.join(rel);
let path = full.to_string_lossy().replace('\\', "/");
out.push_str("TN:\n");
writeln!(out, "SF:{path}").expect("write to String never fails");
for i in 1..=*count {
let hits = if i <= *covered { 5 } else { 0 };
writeln!(out, "DA:{i},{hits}").expect("write to String never fails");
}
writeln!(out, "LF:{count}").expect("write to String never fails");
writeln!(out, "LH:{covered}").expect("write to String never fails");
out.push_str("end_of_record\n");
}
out
}
fn write_lcov(dir: &Path, files: &[(&str, u32, u32)]) -> String {
let path = dir.join("lcov.info");
fs::write(&path, make_coverage_lcov(dir, files)).expect("write lcov.info");
path.to_string_lossy().into_owned()
}
fn coverage_gate(dir: &Path) -> Command {
let mut cmd = Command::cargo_bin("cargo-coverage-gate").expect("binary present");
cmd.current_dir(dir)
.arg("coverage-gate")
.env_remove("GITHUB_STEP_SUMMARY")
.env_remove("COVERAGE_GATE_SUMMARY");
cmd
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn all_pass_mixed_sources() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(
tmp.path(),
&[
("alpha", Some("80")), ("beta", None), ("gamma", None), ],
Some("75"),
);
let lcov_path = write_lcov(
tmp.path(),
&[
("alpha/src/lib.rs", 100, 95),
("beta/src/lib.rs", 100, 90),
("gamma/src/lib.rs", 100, 100),
],
);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path])
.assert()
.success()
.stdout(predicate::str::contains("alpha"))
.stdout(predicate::str::contains("beta"))
.stdout(predicate::str::contains("gamma"))
.stdout(predicate::str::contains("all packages meet their threshold"))
.stdout(predicate::str::contains("package"))
.stdout(predicate::str::contains("workspace"));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn one_crate_below_threshold_exits_1() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", Some("80")), ("beta", Some("80"))], None);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 100, 95), ("beta/src/lib.rs", 100, 60)]);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path])
.assert()
.code(1)
.stdout(predicate::str::contains("FAIL"))
.stdout(predicate::str::contains("1 package below threshold"))
.stdout(predicate::str::contains("beta: 60/100 lines covered; 40 uncovered."))
.stdout(predicate::str::contains("src/lib.rs: 61-100"));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn displayed_rounding_does_not_satisfy_full_coverage_threshold() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", Some("100"))], None);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 2_000, 1_999)]);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path])
.assert()
.code(1)
.stdout(predicate::str::contains("99.9%"))
.stdout(predicate::str::contains("100.0%"))
.stdout(predicate::str::contains("-<0.1pp"))
.stdout(predicate::str::contains("FAIL"))
.stdout(predicate::str::contains("1999/2000 lines covered; 1 uncovered."));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn multiple_lcov_files_merge_at_line_level() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", Some("80"))], None);
let src = tmp.path().join("alpha/src/lib.rs").to_string_lossy().replace('\\', "/");
let config_a = format!("TN:\nSF:{src}\nDA:1,5\nDA:2,5\nDA:3,0\nDA:4,0\nend_of_record\n");
let config_b = format!("TN:\nSF:{src}\nDA:1,0\nDA:2,0\nDA:3,5\nDA:4,5\nend_of_record\n");
let path_a = tmp.path().join("a.info");
let path_b = tmp.path().join("b.info");
fs::write(&path_a, &config_a).expect("write a.info");
fs::write(&path_b, &config_b).expect("write b.info");
let a = path_a.to_string_lossy().into_owned();
let b = path_b.to_string_lossy().into_owned();
coverage_gate(tmp.path())
.args(["--lcov", &a])
.assert()
.code(1)
.stdout(predicate::str::contains("FAIL"));
coverage_gate(tmp.path())
.args(["--lcov", &a, "--lcov", &b])
.assert()
.success()
.stdout(predicate::str::contains("all packages meet their threshold"));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn gated_crate_with_no_data_exits_2() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", Some("80")), ("beta", Some("80"))], None);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 100, 95)]);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path])
.assert()
.code(2)
.stdout(predicate::str::contains("NO DATA"))
.stdout(predicate::str::contains("no attributed coverage data"));
}
fn make_workspace_with_gate(dir: &Path, members: &[(&str, &str)]) {
let members_list = members.iter().map(|(n, _)| format!("\"{n}\"")).collect::<Vec<_>>().join(", ");
fs::write(
dir.join("Cargo.toml"),
format!("[workspace]\nresolver = \"2\"\nmembers = [{members_list}]\n"),
)
.expect("write workspace root Cargo.toml");
for (name, gate_body) in members {
let member_dir = dir.join(name);
fs::create_dir_all(member_dir.join("src")).expect("mkdir member src");
let metadata = if gate_body.is_empty() {
String::new()
} else {
format!("\n[package.metadata.coverage-gate]\n{gate_body}\n")
};
fs::write(
member_dir.join("Cargo.toml"),
format!(
r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"
{metadata}"#
),
)
.expect("write member Cargo.toml");
fs::write(member_dir.join("src/lib.rs"), "// empty\n").expect("write lib.rs");
}
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn expect_no_coverable_lines_passes_with_no_data() {
let tmp = TempDir::new().expect("tempdir");
make_workspace_with_gate(
tmp.path(),
&[("alpha", "min-lines-percent = 80"), ("beta", "expect-no-coverable-lines = true")],
);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 100, 95)]);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path])
.assert()
.success()
.stdout(predicate::str::contains("EMPTY"))
.stdout(predicate::str::contains("all packages meet their threshold"));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn expect_no_coverable_lines_fails_when_lines_present_exits_1() {
let tmp = TempDir::new().expect("tempdir");
make_workspace_with_gate(
tmp.path(),
&[("alpha", "min-lines-percent = 80"), ("beta", "expect-no-coverable-lines = true")],
);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 100, 95), ("beta/src/lib.rs", 5, 0)]);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path])
.assert()
.code(1)
.stdout(predicate::str::contains("NOT EMPTY"))
.stdout(predicate::str::contains("unexpected coverable lines"));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn conflicting_coverage_metadata_exits_2() {
let tmp = TempDir::new().expect("tempdir");
make_workspace_with_gate(tmp.path(), &[("alpha", "min-lines-percent = 50\nexpect-no-coverable-lines = true")]);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 10, 10)]);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path])
.assert()
.code(2)
.stderr(predicate::str::contains("cannot set both"));
}
#[test]
fn target_zero_threshold_opts_package_out_of_gate() {
let tmp = TempDir::new().expect("tempdir");
make_workspace_with_gate(
tmp.path(),
&[
(
"alpha",
"min-lines-percent = 100\n\n\
[package.metadata.coverage-gate.target.'cfg(not(windows))']\n\
min-lines-percent = 0",
),
("beta", "min-lines-percent = 80"),
],
);
let lcov_path = write_lcov(tmp.path(), &[("beta/src/lib.rs", 10, 9)]);
coverage_gate(tmp.path())
.args(["--target", "x86_64-unknown-linux-gnu", "--lcov", &lcov_path])
.assert()
.success()
.stdout(predicate::str::contains("beta"))
.stdout(predicate::str::contains("alpha"))
.stdout(predicate::str::contains("(no data)"))
.stdout(predicate::str::contains("0.0%"));
}
#[test]
fn empty_lcov_passes_when_all_effective_policies_allow_no_data() {
let tmp = TempDir::new().expect("tempdir");
make_workspace_with_gate(
tmp.path(),
&[
(
"alpha",
"min-lines-percent = 100\n\n\
[package.metadata.coverage-gate.target.'cfg(not(windows))']\n\
min-lines-percent = 0",
),
("beta", "expect-no-coverable-lines = true"),
],
);
let empty_lcov = write_lcov(tmp.path(), &[]);
coverage_gate(tmp.path())
.args(["--target", "x86_64-unknown-linux-gnu", "--lcov", &empty_lcov])
.assert()
.success()
.stdout(predicate::str::contains("alpha"))
.stdout(predicate::str::contains("beta"))
.stdout(predicate::str::contains("all packages meet their threshold"));
}
#[test]
fn target_zero_threshold_package_remains_gated_when_override_does_not_match() {
let tmp = TempDir::new().expect("tempdir");
make_workspace_with_gate(
tmp.path(),
&[(
"alpha",
"min-lines-percent = 100\n\n\
[package.metadata.coverage-gate.target.'cfg(not(windows))']\n\
min-lines-percent = 0",
)],
);
let empty_lcov = write_lcov(tmp.path(), &[]);
coverage_gate(tmp.path())
.args(["--target", "x86_64-pc-windows-msvc", "--lcov", &empty_lcov])
.assert()
.code(2)
.stdout(predicate::str::contains("NO DATA"));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn package_flag_restricts_scope() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", Some("80")), ("beta", Some("80"))], None);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 100, 95), ("beta/src/lib.rs", 100, 50)]);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path, "-p", "alpha"])
.assert()
.success()
.stdout(predicate::str::contains("alpha"))
.stdout(predicate::str::contains("beta").not());
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn package_flag_accepts_repeated_short_form() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(
tmp.path(),
&[("alpha", Some("80")), ("beta", Some("80")), ("gamma", Some("80"))],
None,
);
let lcov_path = write_lcov(
tmp.path(),
&[
("alpha/src/lib.rs", 100, 95),
("beta/src/lib.rs", 100, 95),
("gamma/src/lib.rs", 100, 50),
],
);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path, "-p", "alpha", "-p", "beta"])
.assert()
.success()
.stdout(predicate::str::contains("alpha"))
.stdout(predicate::str::contains("beta"))
.stdout(predicate::str::contains("gamma").not());
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn package_flag_accepts_glob_pattern() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(
tmp.path(),
&[("alpha", Some("80")), ("alpha_macros", Some("80")), ("beta", Some("80"))],
None,
);
let lcov_path = write_lcov(
tmp.path(),
&[
("alpha/src/lib.rs", 100, 95),
("alpha_macros/src/lib.rs", 100, 95),
("beta/src/lib.rs", 100, 50),
],
);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path, "-p", "alpha*"])
.assert()
.success()
.stdout(predicate::str::contains("alpha "))
.stdout(predicate::str::contains("alpha_macros"))
.stdout(predicate::str::contains("beta").not());
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn package_flag_with_unknown_name_exits_2() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", None)], None);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 100, 100)]);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path, "-p", "typo"])
.assert()
.code(2)
.stderr(predicate::str::contains("typo"));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn summary_file_flag_writes_markdown() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", Some("80"))], None);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 100, 95)]);
let summary = tmp.path().join("summary.md");
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path, "--summary-file", summary.to_str().expect("utf-8")])
.assert()
.success();
let body = fs::read_to_string(&summary).expect("summary file written");
assert!(body.contains("### coverage-gate"), "got:\n{body}");
assert!(body.contains("| alpha |"), "got:\n{body}");
assert!(body.contains("✅"), "got:\n{body}");
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn github_step_summary_env_is_auto_detected() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", Some("80"))], None);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 100, 95)]);
let summary = tmp.path().join("step-summary.md");
Command::cargo_bin("cargo-coverage-gate")
.expect("binary present")
.current_dir(tmp.path())
.arg("coverage-gate")
.env("GITHUB_STEP_SUMMARY", &summary)
.env_remove("COVERAGE_GATE_SUMMARY")
.args(["--lcov", &lcov_path])
.assert()
.success();
assert!(summary.exists(), "GITHUB_STEP_SUMMARY file must be written");
let body = fs::read_to_string(&summary).expect("read summary");
assert!(body.contains("### coverage-gate"));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn quiet_suppresses_stdout_but_still_writes_summary() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", Some("80"))], None);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 100, 95)]);
let summary = tmp.path().join("summary.md");
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path, "--summary-file", summary.to_str().expect("utf-8"), "--quiet"])
.assert()
.success()
.stdout(predicate::str::is_empty());
assert!(summary.exists());
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn lcov_with_test_name_records_is_parsed() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", Some("80"))], None);
let full = tmp.path().join("alpha/src/lib.rs");
let path = full.to_string_lossy().replace('\\', "/");
let body = format!("TN:my_test\nSF:{path}\nDA:1,5\nDA:2,5\nDA:3,5\nDA:4,5\nDA:5,0\nLF:5\nLH:4\nend_of_record\n");
let lcov_path = tmp.path().join("cov.info");
fs::write(&lcov_path, body).expect("write lcov");
coverage_gate(tmp.path())
.args(["--lcov", lcov_path.to_str().expect("utf-8")])
.assert()
.success();
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn malformed_lcov_exits_2() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", None)], None);
let lcov_path = tmp.path().join("bad.info");
fs::write(&lcov_path, "this is not lcov\n").expect("write lcov");
coverage_gate(tmp.path())
.args(["--lcov", lcov_path.to_str().expect("utf-8")])
.assert()
.code(2)
.stderr(predicate::str::contains("lcov tracefile"));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn missing_lcov_file_exits_2() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", None)], None);
coverage_gate(tmp.path())
.args(["--lcov", "does-not-exist.info"])
.assert()
.code(2)
.stderr(predicate::str::contains("does-not-exist.info"));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn default_threshold_is_100_when_nothing_configured() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", None)], None);
let lcov_path = write_lcov(tmp.path(), &[("alpha/src/lib.rs", 100, 95)]);
coverage_gate(tmp.path())
.args(["--lcov", &lcov_path])
.assert()
.code(1)
.stdout(predicate::str::contains("100.0%"))
.stdout(predicate::str::contains("default"));
}
#[test]
#[cfg_attr(miri, ignore = "spawns the binary as a subprocess")]
fn defaults_to_target_coverage_lcov_when_flag_omitted() {
let tmp = TempDir::new().expect("tempdir");
make_workspace(tmp.path(), &[("alpha", Some("80"))], None);
let default_dir = tmp.path().join("target/coverage");
fs::create_dir_all(&default_dir).expect("create target/coverage");
fs::write(
default_dir.join("lcov.info"),
make_coverage_lcov(tmp.path(), &[("alpha/src/lib.rs", 100, 100)]),
)
.expect("write default lcov");
coverage_gate(tmp.path())
.assert()
.success()
.stdout(predicate::str::contains("all packages meet their threshold"));
}