#![cfg_attr(coverage_nightly, coverage(off))]
use std::path::PathBuf;
pub(crate) struct CargoTestResult {
pub passed: u64,
pub failed: u64,
pub ignored: u64,
pub total: u64,
}
pub(crate) async fn execute_test_record(from_stdin: bool, dry_run: bool) -> anyhow::Result<()> {
let output = if from_stdin {
read_stdin_cargo_output()?
} else {
run_cargo_test()?
};
let result = parse_cargo_test_output(&output)?;
let short_sha = get_current_commit_sha()?;
let timestamp = chrono::Utc::now().to_rfc3339();
if dry_run {
print_dry_run(&result, &short_sha, ×tamp);
return Ok(());
}
write_test_record(&result, &short_sha, ×tamp)?;
println!(
"Recorded: {}/{} pass (commit {})",
result.passed, result.total, short_sha
);
Ok(())
}
fn run_cargo_test() -> anyhow::Result<String> {
use std::process::Command;
println!("Running cargo test --no-fail-fast ...");
let output = Command::new("cargo")
.args(["test", "--no-fail-fast"])
.env("RUST_MIN_STACK", "8388608")
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
Ok(format!("{stdout}\n{stderr}"))
}
fn read_stdin_cargo_output() -> anyhow::Result<String> {
use std::io::Read;
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
Ok(buf)
}
pub(crate) fn parse_cargo_test_output(output: &str) -> anyhow::Result<CargoTestResult> {
let mut total_passed: u64 = 0;
let mut total_failed: u64 = 0;
let mut total_ignored: u64 = 0;
let mut found = false;
for line in output.lines() {
if line.contains("test result:") {
found = true;
total_passed += extract_count(line, "passed");
total_failed += extract_count(line, "failed");
total_ignored += extract_count(line, "ignored");
}
}
if !found {
anyhow::bail!(
"No 'test result:' lines found in cargo test output. \
Ensure cargo test ran successfully."
);
}
Ok(CargoTestResult {
passed: total_passed,
failed: total_failed,
ignored: total_ignored,
total: total_passed + total_failed,
})
}
fn extract_count(line: &str, keyword: &str) -> u64 {
for part in line.split(';') {
let trimmed = part.trim().trim_start_matches("test result:");
let trimmed = trimmed
.trim()
.trim_start_matches("ok.")
.trim_start_matches("FAILED.");
let trimmed = trimmed.trim();
if trimmed.ends_with(keyword) {
if let Some(num_str) = trimmed.strip_suffix(keyword) {
if let Ok(n) = num_str.trim().parse::<u64>() {
return n;
}
}
}
}
0
}
fn get_current_commit_sha() -> anyhow::Result<String> {
use std::process::Command;
let output = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()?;
if !output.status.success() {
anyhow::bail!("Failed to get git commit SHA");
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn write_test_record(
result: &CargoTestResult,
short_sha: &str,
timestamp: &str,
) -> anyhow::Result<()> {
let metrics_dir = PathBuf::from(".pmat-metrics");
std::fs::create_dir_all(&metrics_dir)?;
let filename = format!("commit-{short_sha}-tests.json");
let filepath = metrics_dir.join(&filename);
let json = serde_json::json!({
"commit": short_sha,
"pass": result.passed,
"total": result.total,
"failed": result.failed,
"ignored": result.ignored,
"timestamp": timestamp,
});
std::fs::write(&filepath, serde_json::to_string_pretty(&json)?)?;
Ok(())
}
fn print_dry_run(result: &CargoTestResult, short_sha: &str, timestamp: &str) {
println!("Dry run — would record:");
println!(" File: .pmat-metrics/commit-{short_sha}-tests.json");
println!(" Commit: {short_sha}");
println!(" Passed: {}", result.passed);
println!(" Failed: {}", result.failed);
println!(" Ignored: {}", result.ignored);
println!(" Total: {}", result.total);
println!(" Timestamp: {timestamp}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_single_result_line() {
let output = "test result: ok. 19795 passed; 0 failed; 167 ignored; 0 measured; 0 filtered out; finished in 45.32s";
let result = parse_cargo_test_output(output).unwrap();
assert_eq!(result.passed, 19795);
assert_eq!(result.failed, 0);
assert_eq!(result.ignored, 167);
assert_eq!(result.total, 19795);
}
#[test]
fn test_parse_multiple_result_lines() {
let output = "\
test result: ok. 100 passed; 2 failed; 10 ignored; 0 measured; 0 filtered out
test result: ok. 50 passed; 1 failed; 5 ignored; 0 measured; 0 filtered out";
let result = parse_cargo_test_output(output).unwrap();
assert_eq!(result.passed, 150);
assert_eq!(result.failed, 3);
assert_eq!(result.ignored, 15);
assert_eq!(result.total, 153);
}
#[test]
fn test_parse_failed_test_result() {
let output = "test result: FAILED. 18000 passed; 500 failed; 167 ignored";
let result = parse_cargo_test_output(output).unwrap();
assert_eq!(result.passed, 18000);
assert_eq!(result.failed, 500);
assert_eq!(result.total, 18500);
}
#[test]
fn test_parse_no_result_line() {
let output = "Running tests...\nAll done.";
let result = parse_cargo_test_output(output);
assert!(result.is_err());
}
#[test]
fn test_extract_count_passed() {
let line = "test result: ok. 19795 passed; 0 failed; 167 ignored";
assert_eq!(extract_count(line, "passed"), 19795);
assert_eq!(extract_count(line, "failed"), 0);
assert_eq!(extract_count(line, "ignored"), 167);
}
}