#![allow(missing_docs)]
use crate::policy::CascadePolicy;
pub fn assert_deterministic<F, T>(f: F)
where
F: Fn() -> T,
T: PartialEq + std::fmt::Debug,
{
let a = f();
let b = f();
let c = f();
assert_eq!(a, b, "non-deterministic between invocation 1 and 2");
assert_eq!(b, c, "non-deterministic between invocation 2 and 3");
}
pub fn assert_deterministic_with<I, F, T>(input: I, f: F)
where
I: Clone,
F: Fn(I) -> T,
T: PartialEq + std::fmt::Debug,
{
let a = f(input.clone());
let b = f(input.clone());
let c = f(input);
assert_eq!(a, b, "non-deterministic between invocation 1 and 2");
assert_eq!(b, c, "non-deterministic between invocation 2 and 3");
}
pub fn assert_deterministic_over<I, F, T>(inputs: &[I], f: F)
where
I: Clone,
F: Fn(&I) -> T,
T: PartialEq + std::fmt::Debug,
{
for input in inputs {
let a = f(input);
let b = f(input);
let c = f(input);
assert_eq!(a, b, "non-deterministic for one input, 1 vs 2");
assert_eq!(b, c, "non-deterministic for one input, 2 vs 3");
}
}
pub fn assert_cascade_laws<P>(default: P, layers: &[P])
where
P: CascadePolicy + PartialEq + std::fmt::Debug,
{
assert_eq!(
P::resolve(&[], default.clone()),
default,
"CascadePolicy law (resolve-identity): resolve(&[], default) must equal default"
);
for (i, l) in layers.iter().enumerate() {
let mut once = default.clone();
once.merge(l);
let mut twice = default.clone();
twice.merge(l);
twice.merge(l);
assert_eq!(
once, twice,
"CascadePolicy law (idempotence): merge applied twice must equal once (layer {i})"
);
let mut self_merged = l.clone();
self_merged.merge(l);
assert_eq!(
&self_merged, l,
"CascadePolicy law (merge-self-identity): L.merge(&L) must equal L (layer {i})"
);
}
let refs: Vec<Option<&P>> = layers.iter().map(Some).collect();
assert_eq!(
P::resolve(&refs, default.clone()),
P::resolve(&refs, default.clone()),
"CascadePolicy law (determinism): resolve must be deterministic for the same layers"
);
let mut folded = default.clone();
for l in layers {
folded.merge(l);
}
assert_eq!(
P::resolve(&refs, default.clone()),
folded,
"CascadePolicy law (fold-order): resolve must fold layers in declared order (innermost/rightmost wins)"
);
}
pub fn assert_cascade_laws_with_default<P>(default: P, layers: &[P])
where
P: CascadePolicy + PartialEq + std::fmt::Debug + Default,
{
assert_cascade_laws(default.clone(), layers);
let empty = P::default();
assert_eq!(
P::resolve(&[Some(&empty)], default.clone()),
default,
"CascadePolicy law (empty-layer): an all-None (Default) layer must preserve the accumulator"
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pure_closure_passes() {
assert_deterministic(|| 1 + 1);
}
#[test]
fn string_returning_closure_passes() {
assert_deterministic(|| String::from("hello"));
}
#[test]
fn with_input_passes() {
assert_deterministic_with(42_u32, |x| x.wrapping_mul(7));
}
#[test]
fn over_inputs_passes() {
assert_deterministic_over(&[0_u32, 1, 2, 100, u32::MAX], |&x| x.wrapping_mul(7));
}
#[test]
fn over_empty_slice_is_vacuous_truth() {
let empty: &[u32] = &[];
assert_deterministic_over(empty, |&x| x);
}
#[derive(Clone, Default, PartialEq, Debug)]
struct GoodPolicy {
a: Option<u32>,
b: Option<String>,
}
impl CascadePolicy for GoodPolicy {
fn merge(&mut self, layer: &Self) {
if let Some(v) = &layer.a {
self.a = Some(*v);
}
if let Some(v) = &layer.b {
self.b = Some(v.clone());
}
}
}
#[test]
fn cascade_harness_passes_a_correct_impl() {
assert_cascade_laws_with_default(
GoodPolicy {
a: Some(0),
b: Some("base".into()),
},
&[
GoodPolicy {
a: Some(1),
..Default::default()
},
GoodPolicy {
b: Some("x".into()),
..Default::default()
},
GoodPolicy {
a: Some(9),
b: Some("y".into()),
},
],
);
}
#[derive(Clone, Default, PartialEq, Debug)]
struct NonIdempotentPolicy {
acc: Vec<u32>,
a: Option<u32>,
}
impl CascadePolicy for NonIdempotentPolicy {
fn merge(&mut self, layer: &Self) {
if let Some(v) = layer.a {
self.acc.push(v); self.a = Some(v);
}
}
}
#[test]
#[should_panic(expected = "idempotence")]
fn cascade_harness_catches_a_non_idempotent_impl() {
assert_cascade_laws(
NonIdempotentPolicy::default(),
&[NonIdempotentPolicy {
a: Some(5),
..Default::default()
}],
);
}
#[test]
#[should_panic(expected = "non-deterministic")]
fn non_deterministic_panics() {
use std::sync::atomic::{AtomicU32, Ordering};
static COUNT: AtomicU32 = AtomicU32::new(0);
COUNT.store(0, Ordering::SeqCst);
assert_deterministic(|| COUNT.fetch_add(1, Ordering::SeqCst));
}
}