vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
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);
    }
}