use std::fs;
use std::path::Path;
use std::process::Command;
use super::MutationOutcome;
pub(super) fn run_cargo_test(test_fn_name: &str, extra_args: &[&str]) -> MutationOutcome {
let mut cmd = Command::new("cargo");
cmd.arg("test").arg("--").arg("--exact").arg(test_fn_name);
for arg in extra_args {
cmd.arg(arg);
}
let output = match cmd.output() {
Ok(o) => o,
Err(err) => {
return MutationOutcome::Skipped {
reason: format!("cargo spawn failed: {err}"),
};
}
};
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
classify_cargo_test_output(
output.status.success(),
&stdout,
&stderr,
&output.status.to_string(),
)
}
pub(super) fn assert_source_matches_original(
source_file: &Path,
original: &str,
) -> Result<(), String> {
let current = fs::read_to_string(source_file)
.map_err(|err| format!("source corruption check failed to read file: {err}"))?;
if current == original {
Ok(())
} else {
Err(format!(
"source corruption detected before mutation. Fix: restore {} to the probe's original bytes before running more mutations.",
source_file.display()
))
}
}
fn classify_cargo_test_output(
status_success: bool,
stdout: &str,
stderr: &str,
status_label: &str,
) -> MutationOutcome {
if stderr.contains("error[E") || stderr.contains("could not compile") {
return MutationOutcome::Skipped {
reason: "mutation produced code that did not compile".to_string(),
};
}
let Some((passed, failed, ignored)) = parse_test_result_counts(stdout) else {
return MutationOutcome::Skipped {
reason: format!("cargo exited {status_label} without a test-result line"),
};
};
let ran = passed.saturating_add(failed).saturating_add(ignored);
if ran == 0 {
return MutationOutcome::Skipped {
reason: "test function not found".to_string(),
};
}
if status_success {
MutationOutcome::Survived
} else if stdout.contains("FAILED") || stdout.contains("test result: FAILED") {
MutationOutcome::Killed
} else {
MutationOutcome::Skipped {
reason: format!("cargo exited {status_label} without a recognized failure"),
}
}
}
fn parse_test_result_counts(stdout: &str) -> Option<(usize, usize, usize)> {
let line = stdout
.lines()
.rev()
.find(|line| line.trim_start().starts_with("test result:"))?;
Some((
count_before_token(line, "passed")?,
count_before_token(line, "failed")?,
count_before_token(line, "ignored")?,
))
}
fn count_before_token(line: &str, token: &str) -> Option<usize> {
let mut previous = None;
for word in line.split(|ch: char| !ch.is_ascii_alphanumeric()) {
if word == token {
return previous.and_then(|value: &str| value.parse::<usize>().ok());
}
if !word.is_empty() {
previous = Some(word);
}
}
None
}
#[cfg(test)]
mod tests {
use super::{assert_source_matches_original, classify_cargo_test_output};
use crate::verify::harnesses::mutation::MutationOutcome;
use std::fs;
#[test]
fn cargo_output_with_zero_tests_is_skipped_not_survived() {
let stdout = "running 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s\n";
let outcome = classify_cargo_test_output(true, stdout, "", "exit status: 0");
assert_eq!(
outcome,
MutationOutcome::Skipped {
reason: "test function not found".to_string()
},
"H6 regression: nonexistent exact test was classified as survived"
);
}
#[test]
fn source_corruption_check_rejects_mutated_disk_state() {
let tmp =
std::env::temp_dir().join(format!("vyre-conform-h6-corruption-{}", std::process::id()));
fs::create_dir_all(&tmp).expect("create temp dir");
let source_path = tmp.join("fixture.rs");
fs::write(&source_path, "pub fn f() -> u32 { 2 }\n").expect("write fixture");
let err = assert_source_matches_original(&source_path, "pub fn f() -> u32 { 1 }\n")
.expect_err("H6 regression: corrupted source was accepted as a new snapshot");
assert!(
err.contains("source corruption detected") && err.contains("Fix:"),
"source corruption error must be actionable, got {err}"
);
let _ = fs::remove_dir_all(&tmp);
}
}