use crate::adversarial::mutations::catalog::{
apply, class_of, Mutation, MutationClass, MUTATION_CATALOG,
};
use crate::spec::types::MutationClass as SpecMutationClass;
use crate::verify::harnesses::mutation::{AppliedMutation, MutationApplyError};
pub struct CatalogMutation {
mutation: Mutation,
id: String,
description: String,
}
impl CatalogMutation {
#[must_use]
#[inline]
pub fn new(mutation: Mutation) -> Self {
let description = describe(&mutation);
let id = identify(&mutation);
Self {
mutation,
id,
description,
}
}
}
impl AppliedMutation for CatalogMutation {
fn id(&self) -> &str {
&self.id
}
fn description(&self) -> &str {
&self.description
}
fn class(&self) -> SpecMutationClass {
let catalog_class: MutationClass = class_of(&self.mutation);
catalog_class
}
fn apply(&self, source: &str) -> Result<String, MutationApplyError> {
match apply(source, &self.mutation) {
Ok(mutated) => Ok(mutated),
Err(err) => Err(MutationApplyError::NotApplicable {
reason: err.to_string(),
}),
}
}
fn hint(&self) -> String {
format!(
"test passed when I applied `{}`. Add an assertion that distinguishes \
the original from the mutated behaviour. The catalog mutation id is `{}` \
so you can reproduce locally with: \
cargo run --bin mutation_probe -- --mutation {} --test <name>.",
self.description, self.id, self.id,
)
}
}
#[must_use]
#[inline]
pub fn applied_for_classes(classes: &[SpecMutationClass]) -> Vec<Box<dyn AppliedMutation>> {
let mut out: Vec<Box<dyn AppliedMutation>> = Vec::new();
for mutation in MUTATION_CATALOG {
if !classes.is_empty() && !classes.contains(&class_of(mutation)) {
continue;
}
out.push(Box::new(CatalogMutation::new(mutation.clone())));
}
out
}
#[must_use]
#[inline]
pub fn applied_for(mutation: Mutation) -> Box<dyn AppliedMutation> {
Box::new(CatalogMutation::new(mutation))
}
fn identify(mutation: &Mutation) -> String {
let raw = format!("{mutation:?}");
raw.chars()
.map(|c| match c {
'A'..='Z' | 'a'..='z' | '0'..='9' => c.to_ascii_lowercase(),
_ => '_',
})
.collect::<String>()
.trim_matches('_')
.to_string()
}
fn describe(mutation: &Mutation) -> String {
format!("{mutation:?}")
}
#[cfg(test)]
mod tests {
use super::*;
use super::{
applied_for, applied_for_classes, CatalogMutation, Mutation, SpecMutationClass,
MUTATION_CATALOG,
};
#[test]
fn applied_for_classes_returns_full_catalog_for_empty_filter() {
let applied = applied_for_classes(&[]);
assert_eq!(applied.len(), MUTATION_CATALOG.len());
}
#[test]
fn applied_for_classes_filters_to_one_class() {
let applied = applied_for_classes(&[SpecMutationClass::ArithmeticMutations]);
assert!(!applied.is_empty(), "arithmetic class should have entries");
for adapter in &applied {
assert_eq!(adapter.class(), SpecMutationClass::ArithmeticMutations);
}
}
#[test]
fn arith_op_swap_adapter_applies_to_a_known_source() {
let applied = applied_for(Mutation::ArithOpSwap {
from: super::super::BinOpKind::Add,
to: super::super::BinOpKind::Sub,
});
let source = "fn add(a: u32, b: u32) -> u32 { a + b }";
let mutated = applied.apply(source).expect("apply should succeed");
assert!(
mutated.contains("a - b"),
"expected subtraction in mutated source"
);
}
#[test]
fn id_is_stable_across_calls() {
let m = Mutation::ConstantZeroToOne;
let a = CatalogMutation::new(m.clone());
let b = CatalogMutation::new(m);
assert_eq!(
a.id(),
b.id(),
"id() must be deterministic for the same mutation"
);
}
}