use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use crate::spec::types::MutationClass;
#[path = "mutation_cargo.rs"]
mod mutation_cargo;
use mutation_cargo::{assert_source_matches_original, run_cargo_test};
pub trait AppliedMutation: Send + Sync {
fn id(&self) -> &str;
fn description(&self) -> &str;
fn class(&self) -> MutationClass;
fn apply(&self, source: &str) -> Result<String, MutationApplyError>;
fn hint(&self) -> String {
format!(
"test passed when I applied `{}`. Add an assertion that distinguishes the \
original from the mutated behaviour.",
self.description()
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MutationApplyError {
NotApplicable {
reason: String,
},
WouldProduceInvalidSyntax {
reason: String,
},
}
impl core::fmt::Display for MutationApplyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotApplicable { reason } => write!(f, "mutation not applicable: {reason}"),
Self::WouldProduceInvalidSyntax { reason } => {
write!(f, "mutation would produce invalid syntax: {reason}")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MutationOutcome {
Killed,
Survived,
Skipped {
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MutationResult {
pub mutation_id: String,
pub description: String,
pub outcome: MutationOutcome,
pub duration: Duration,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructuredFeedback {
pub mutation_id: String,
pub hint: String,
}
#[derive(Debug, Clone)]
pub struct GateReport {
pub test_name: String,
pub source_file: PathBuf,
pub results: Vec<MutationResult>,
pub total_duration: Duration,
pub feedback: Vec<StructuredFeedback>,
}
impl GateReport {
#[inline]
pub fn killed(&self) -> Vec<&MutationResult> {
self.results
.iter()
.filter(|r| matches!(r.outcome, MutationOutcome::Killed))
.collect()
}
#[inline]
pub fn survived(&self) -> Vec<&MutationResult> {
self.results
.iter()
.filter(|r| matches!(r.outcome, MutationOutcome::Survived))
.collect()
}
#[inline]
pub fn skipped(&self) -> Vec<&MutationResult> {
self.results
.iter()
.filter(|r| matches!(r.outcome, MutationOutcome::Skipped { .. }))
.collect()
}
#[inline]
pub fn is_pass(&self) -> bool {
self.survived().is_empty() && !self.killed().is_empty()
}
}
struct SourceSnapshot {
path: PathBuf,
original: Vec<u8>,
restored: bool,
}
impl SourceSnapshot {
fn new(path: &Path) -> io::Result<Self> {
let original = fs::read(path)?;
Ok(Self {
path: path.to_path_buf(),
original,
restored: false,
})
}
fn restore_explicit(&mut self) -> io::Result<()> {
if self.restored {
return Ok(());
}
fs::write(&self.path, &self.original)?;
self.restored = true;
Ok(())
}
}
impl Drop for SourceSnapshot {
fn drop(&mut self) {
if self.restored {
return;
}
if let Err(err) = fs::write(&self.path, &self.original) {
eprintln!(
"vyre-conform H6 mutation gate: FAILED TO RESTORE {} on drop: {}. \
The working tree may be in a mutated state. \
Original contents are {} bytes; write the snapshot back manually.",
self.path.display(),
err,
self.original.len()
);
}
}
}
mod probe;
pub use probe::{canary_plus_to_minus, mutation_probe};