use serde_json::Value as J;
use std::path::Path;
fn same(got: f64, want: f64) -> bool {
got.to_bits() == want.to_bits()
}
fn load() -> Vec<(String, J)> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/sequences");
let mut paths: Vec<_> = match std::fs::read_dir(&dir) {
Ok(e) => e
.filter_map(|x| x.ok())
.map(|x| x.path())
.filter(|p| p.extension().is_some_and(|x| x == "json"))
.collect(),
Err(_) => return Vec::new(),
};
paths.sort();
paths
.iter()
.map(|p| {
let j: J = serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap();
(p.file_stem().unwrap().to_string_lossy().into_owned(), j)
})
.collect()
}
fn bits(s: &str) -> f64 {
f64::from_bits(u64::from_str_radix(s, 16).unwrap())
}
#[derive(Clone)]
struct State {
t0: u32,
n: u32,
w0: f64,
w1: f64,
p00: f64,
p11: f64,
p01: f64,
lam: f64,
}
impl State {
fn new(t0: f64, srate: f64, halftime: f64) -> Self {
let mut s = State {
t0: t0 as u32,
n: 0,
w0: 0.0,
w1: 0.0,
p00: 1e10,
p11: 1e10,
p01: 0.0,
lam: 0.0,
};
if srate > 0.0 {
s.w1 = 1. / srate;
s.lam = 2f64.powf(-1. / (srate * halftime));
}
s
}
}
struct Mutant {
name: &'static str,
why: &'static str,
init: fn(f64, f64, f64) -> State,
step: fn(&mut State, f64) -> f64,
}
fn real_step(s: &mut State, t: f64) -> f64 {
let t = t - s.t0 as f64;
let u1 = s.n as f64;
s.n += 1;
let pi0 = s.p00 + u1 * s.p01;
let pi1 = s.p01 + u1 * s.p11;
let al = t - (s.w0 + u1 * s.w1);
let g_inv = 1. / (s.lam + pi0 + pi1 * u1);
let il_ = 1. / s.lam;
s.p00 = il_ * (s.p00 - pi0 * pi0 * g_inv);
s.p01 = il_ * (s.p01 - pi0 * pi1 * g_inv);
s.p11 = il_ * (s.p11 - pi1 * pi1 * g_inv);
s.w0 += al * (s.p00 + s.p01 * u1);
s.w1 += al * (s.p01 + s.p11 * u1);
s.w0 + u1 * s.w1 + s.t0 as f64
}
const MUTANTS: &[Mutant] = &[
Mutant {
name: "no_baseline_subtraction",
why: "drops the integer baseline, which the comment calls a numerical aid",
init: State::new,
step: |s, t| {
let u1 = s.n as f64;
s.n += 1;
let pi0 = s.p00 + u1 * s.p01;
let pi1 = s.p01 + u1 * s.p11;
let al = t - (s.w0 + u1 * s.w1);
let g_inv = 1. / (s.lam + pi0 + pi1 * u1);
let il_ = 1. / s.lam;
s.p00 = il_ * (s.p00 - pi0 * pi0 * g_inv);
s.p01 = il_ * (s.p01 - pi0 * pi1 * g_inv);
s.p11 = il_ * (s.p11 - pi1 * pi1 * g_inv);
s.w0 += al * (s.p00 + s.p01 * u1);
s.w1 += al * (s.p01 + s.p11 * u1);
s.w0 + u1 * s.w1
},
},
Mutant {
name: "fused_multiply_add",
why: "folds a multiply and an add together, which changes the rounding",
init: State::new,
step: |s, t| {
let t = t - s.t0 as f64;
let u1 = s.n as f64;
s.n += 1;
let pi0 = u1.mul_add(s.p01, s.p00);
let pi1 = u1.mul_add(s.p11, s.p01);
let al = t - u1.mul_add(s.w1, s.w0);
let g_inv = 1. / (s.lam + pi0 + pi1 * u1);
let il_ = 1. / s.lam;
s.p00 = il_ * (-(pi0 * pi0)).mul_add(g_inv, s.p00);
s.p01 = il_ * (-(pi0 * pi1)).mul_add(g_inv, s.p01);
s.p11 = il_ * (-(pi1 * pi1)).mul_add(g_inv, s.p11);
s.w0 = al.mul_add(s.p01.mul_add(u1, s.p00), s.w0);
s.w1 = al.mul_add(s.p11.mul_add(u1, s.p01), s.w1);
u1.mul_add(s.w1, s.w0) + s.t0 as f64
},
},
Mutant {
name: "reassociated_gain",
why: "rewrites the gain denominator in a different order",
init: State::new,
step: |s, t| {
let t = t - s.t0 as f64;
let u1 = s.n as f64;
s.n += 1;
let pi0 = s.p00 + u1 * s.p01;
let pi1 = s.p01 + u1 * s.p11;
let al = t - (s.w0 + u1 * s.w1);
let g_inv = 1. / (pi1 * u1 + pi0 + s.lam);
let il_ = 1. / s.lam;
s.p00 = il_ * (s.p00 - pi0 * pi0 * g_inv);
s.p01 = il_ * (s.p01 - pi0 * pi1 * g_inv);
s.p11 = il_ * (s.p11 - pi1 * pi1 * g_inv);
s.w0 += al * (s.p00 + s.p01 * u1);
s.w1 += al * (s.p01 + s.p11 * u1);
s.w0 + u1 * s.w1 + s.t0 as f64
},
},
Mutant {
name: "weights_before_covariance",
why: "updates the weights before the covariance, so it uses the old values",
init: State::new,
step: |s, t| {
let t = t - s.t0 as f64;
let u1 = s.n as f64;
s.n += 1;
let pi0 = s.p00 + u1 * s.p01;
let pi1 = s.p01 + u1 * s.p11;
let al = t - (s.w0 + u1 * s.w1);
let g_inv = 1. / (s.lam + pi0 + pi1 * u1);
let il_ = 1. / s.lam;
s.w0 += al * (s.p00 + s.p01 * u1);
s.w1 += al * (s.p01 + s.p11 * u1);
s.p00 = il_ * (s.p00 - pi0 * pi0 * g_inv);
s.p01 = il_ * (s.p01 - pi0 * pi1 * g_inv);
s.p11 = il_ * (s.p11 - pi1 * pi1 * g_inv);
s.w0 + u1 * s.w1 + s.t0 as f64
},
},
Mutant {
name: "counter_increments_after_use",
why: "reads the counter after the increment instead of before",
init: State::new,
step: |s, t| {
let t = t - s.t0 as f64;
s.n += 1;
let u1 = s.n as f64;
let pi0 = s.p00 + u1 * s.p01;
let pi1 = s.p01 + u1 * s.p11;
let al = t - (s.w0 + u1 * s.w1);
let g_inv = 1. / (s.lam + pi0 + pi1 * u1);
let il_ = 1. / s.lam;
s.p00 = il_ * (s.p00 - pi0 * pi0 * g_inv);
s.p01 = il_ * (s.p01 - pi0 * pi1 * g_inv);
s.p11 = il_ * (s.p11 - pi1 * pi1 * g_inv);
s.w0 += al * (s.p00 + s.p01 * u1);
s.w1 += al * (s.p01 + s.p11 * u1);
s.w0 + u1 * s.w1 + s.t0 as f64
},
},
Mutant {
name: "baseline_not_truncated",
why: "keeps the fraction of the first timestamp instead of truncating it",
init: |t0, srate, halftime| {
let mut s = State::new(t0, srate, halftime);
s.t0 = t0.round() as u32;
s
},
step: real_step,
},
Mutant {
name: "wrong_initial_covariance",
why: "starts the covariance at 1e9 instead of 1e10",
init: |t0, srate, halftime| {
let mut s = State::new(t0, srate, halftime);
s.p00 = 1e9;
s.p11 = 1e9;
s
},
step: real_step,
},
Mutant {
name: "slope_starts_at_zero",
why: "leaves the slope at zero instead of one over the rate",
init: |t0, srate, halftime| {
let mut s = State::new(t0, srate, halftime);
s.w1 = 0.0;
s
},
step: real_step,
},
Mutant {
name: "forgetting_factor_uses_e",
why: "uses a natural exponent instead of a power of two",
init: |t0, srate, halftime| {
let mut s = State::new(t0, srate, halftime);
if srate > 0.0 {
s.lam = (-1. / (srate * halftime)).exp();
}
s
},
step: real_step,
},
Mutant {
name: "baseline_not_added_back",
why: "leaves the baseline out of the returned value",
init: State::new,
step: |s, t| {
let r = real_step(s, t);
r - s.t0 as f64
},
},
];
fn run(name: &str, m: &Mutant) -> usize {
let all = load();
let mut caught = 0;
for (_, j) in &all {
let arr = j["samples"].as_array().unwrap();
let srate = bits(j["srate_bits"].as_str().unwrap());
let halftime = bits(j["halftime_bits"].as_str().unwrap());
let first = bits(arr[0].as_array().unwrap()[0].as_str().unwrap());
let mut st = (m.init)(first, srate, halftime);
let mut differs = false;
for pair in arr {
let p = pair.as_array().unwrap();
let t = bits(p[0].as_str().unwrap());
let want = bits(p[1].as_str().unwrap());
let got = (m.step)(&mut st, t);
if !same(got, want) {
differs = true;
break;
}
}
if differs {
caught += 1;
}
}
let _ = name;
caught
}
#[test]
fn every_mutant_is_caught_by_at_least_one_sequence() {
let all = load();
if all.is_empty() {
eprintln!("no sequences; skipping");
return;
}
let mut uncaught = Vec::new();
println!("mutation report over {} sequences:", all.len());
for m in MUTANTS {
let caught = run(m.name, m);
println!(
" {:32} caught by {}/{} sequences",
m.name,
caught,
all.len()
);
if caught == 0 {
uncaught.push(format!("{}: {}", m.name, m.why));
}
}
assert!(
uncaught.is_empty(),
"these changes slipped past every sequence. The set has a hole, and the \
fix is a new sequence.\n {}",
uncaught.join("\n ")
);
}
#[test]
fn the_real_filter_is_not_caught() {
let all = load();
if all.is_empty() {
return;
}
let real = Mutant {
name: "real",
why: "",
init: State::new,
step: real_step,
};
assert_eq!(
run("real", &real),
0,
"the reference copy of the filter differs from the oracle, so the \
mutation report means nothing"
);
println!("the reference copy matches all {} sequences", all.len());
}