cstat/tests/coverage_cli.rs

190 lines
6.4 KiB
Rust
Raw Permalink Normal View History

2026-07-19 07:56:08 +00:00
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_project(name: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = std::env::temp_dir().join(format!("cstat-coverage-cli-{name}-{unique}"));
fs::create_dir_all(root.join("src")).unwrap();
fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
fs::write(
root.join("src/lib.rs"),
r#"pub fn target(flag: bool) -> u8 {
if flag {
1
} else {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn covers_true_arm() {
assert_eq!(target(true), 1);
}
}
"#,
)
.unwrap();
root
}
fn export_json(root: &Path) -> PathBuf {
let source = root.join("src/lib.rs");
let escaped_source = source.to_string_lossy().replace('\\', "\\\\");
let content = format!(
r#"{{"type":"llvm.coverage.json.export","data":[{{"files":[{{"filename":"{}","segments":[[1,1,1,true,true,false],[2,5,1,true,true,false],[3,9,1,true,true,false],[5,9,0,true,true,false]],"branches":[[2,8,2,12,1,0,0,0,0]],"summary":{{}}}}]}}]}}"#,
escaped_source
);
let export = root.join("coverage.json");
fs::write(&export, content).unwrap();
export
}
fn run_cstat(path: &Path, export: &Path, args: &[&str]) -> Output {
let bin = env!("CARGO_BIN_EXE_cstat");
let mut command = Command::new(bin);
command.args(["--no-color", "--path"]);
command.arg(path);
command.args(args);
command.env("CSTAT_LLVM_COV_EXPORT_JSON", export);
command.output().expect("invoke cstat binary")
}
fn run_cstat_without_export(path: &Path, args: &[&str]) -> Output {
let bin = env!("CARGO_BIN_EXE_cstat");
let mut command = Command::new(bin);
command.args(["--no-color", "--path"]);
command.arg(path);
command.args(args);
command.env_remove("CSTAT_LLVM_COV_EXPORT_JSON");
command.output().expect("invoke cstat binary")
}
2026-07-19 07:56:08 +00:00
fn assert_success(output: &Output) {
assert!(
output.status.success(),
"cstat failed: status={:?}\nstderr={}\nstdout={}",
output.status,
String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout),
);
}
#[test]
fn coverage_verbose_prints_guidance_before_no_run_env_error() {
let root = temp_project("missing-env");
let output = run_cstat_without_export(&root, &["coverage", "--no-run", "-v"]);
assert!(
!output.status.success(),
"coverage --no-run without export env should fail"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Normal mode runs cargo test with Rust source-based coverage flags"),
"stdout={stdout}"
);
assert!(
stdout.contains("Use coverage --no-run with CSTAT_LLVM_COV_EXPORT_JSON"),
"stdout={stdout}"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("CSTAT_LLVM_COV_EXPORT_JSON is required"),
"stderr={stderr}"
);
fs::remove_dir_all(root).unwrap();
}
2026-07-19 07:56:08 +00:00
#[test]
fn coverage_no_run_reports_dynamic_lines_and_branches_from_export_json() {
let root = temp_project("json");
let file = root.join("src/lib.rs");
let export = export_json(&root);
let output = run_cstat(&file, &export, &["--json", "coverage", "--no-run"]);
assert_success(&output);
let stdout = String::from_utf8(output.stdout).unwrap();
let value: Value = serde_json::from_str(&stdout).expect("parse coverage JSON");
assert_eq!(value["target"], "src/lib.rs");
assert!(value.get("static_reachability").is_some(), "json={stdout}");
assert!(
value.get("projected_line_reachability").is_some(),
"json={stdout}"
);
assert_eq!(value["dynamic_line_coverage"]["coverable_lines"], 4);
assert_eq!(value["dynamic_line_coverage"]["executed_lines"], 3);
assert_eq!(
value["dynamic_line_coverage"]["missing_line_numbers"],
serde_json::json!([5])
);
assert_eq!(value["branch_coverage"]["total_branch_outcomes"], 2);
assert_eq!(value["branch_coverage"]["covered_branch_outcomes"], 1);
assert_eq!(value["branch_coverage"]["missing_branch_outcomes"], 1);
assert_eq!(
value["branch_coverage"]["sites"][0]["taken"],
serde_json::json!(["true"])
);
assert_eq!(
value["branch_coverage"]["sites"][0]["missing"],
serde_json::json!(["false"])
);
assert_eq!(value["tool_status"]["ran_tests"], false);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn coverage_human_separates_static_projected_dynamic_and_branch_sections() {
let root = temp_project("human");
let file = root.join("src/lib.rs");
let export = export_json(&root);
let output = run_cstat(&file, &export, &["coverage", "--no-run", "-v"]);
2026-07-19 07:56:08 +00:00
assert_success(&output);
let stdout = String::from_utf8_lossy(&output.stdout);
for section in [
"Static test/benchmark reachability",
"Projected line reachability",
"Dynamic line coverage",
"Branch coverage",
] {
assert_eq!(
stdout
.lines()
.filter(|line| line.trim_start().starts_with(&format!("┌─ {section}")))
.count(),
1,
"section header should appear once: {section}: {stdout}"
);
}
for expected in [
"Normal mode runs cargo test with Rust source-based coverage flags",
"Line coverage is dynamic llvm-cov segment data; branch coverage uses branch records when the Rust/LLVM toolchain emits them.",
"Reachability is computed from the static call graph; it is not runtime coverage.",
"Projected reachable lines are production code lines inside functions statically reached from project tests/benches.",
"Coverable/executed lines come from llvm-cov segment counts after cstat's production-span filter.",
"Branch coverage counts true/false outcomes from llvm-cov branch records.",
"missing lines: 5",
"line 2: flag missing false",
] {
assert!(stdout.contains(expected), "missing {expected}: {stdout}");
2026-07-19 07:56:08 +00:00
}
assert!(!stdout.contains("Path coverage"), "stdout={stdout}");
fs::remove_dir_all(root).unwrap();
}