use crate::aggregate::AggregateState;
pub struct AggregateTestHarness<S: AggregateState> {
state: S,
}
impl<S: AggregateState> AggregateTestHarness<S> {
#[must_use]
pub fn new() -> Self {
Self {
state: S::default(),
}
}
#[must_use]
pub const fn with_state(state: S) -> Self {
Self { state }
}
pub fn update<F>(&mut self, f: F)
where
F: FnOnce(&mut S),
{
f(&mut self.state);
}
pub fn combine<F>(&mut self, source: &Self, combine_fn: F)
where
F: FnOnce(&S, &mut S),
{
combine_fn(&source.state, &mut self.state);
}
#[must_use]
pub fn finalize(self) -> S {
self.state
}
#[must_use]
pub const fn state(&self) -> &S {
&self.state
}
pub fn reset(&mut self) {
self.state = S::default();
}
pub fn aggregate<I, T, F>(inputs: I, update_fn: F) -> S
where
I: IntoIterator<Item = T>,
F: Fn(&mut S, T),
{
let mut harness = Self::new();
for input in inputs {
harness.update(|s| update_fn(s, input));
}
harness.finalize()
}
}
impl<S: AggregateState> Default for AggregateTestHarness<S> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aggregate::AggregateState;
#[derive(Default, Debug, PartialEq, Clone)]
struct SumState {
total: i64,
}
impl AggregateState for SumState {}
#[derive(Default, Debug, PartialEq, Clone)]
struct CountConfig {
window_size: i64,
count: u64,
}
impl AggregateState for CountConfig {}
#[test]
fn new_creates_default_state() {
let h = AggregateTestHarness::<SumState>::new();
assert_eq!(h.state().total, 0);
}
#[test]
fn with_state() {
let initial = SumState { total: 100 };
let h = AggregateTestHarness::with_state(initial);
assert_eq!(h.state().total, 100);
}
#[test]
fn update_accumulates() {
let mut h = AggregateTestHarness::<SumState>::new();
h.update(|s| s.total += 10);
h.update(|s| s.total += 20);
h.update(|s| s.total += 5);
assert_eq!(h.finalize().total, 35);
}
#[test]
fn finalize_returns_state() {
let mut h = AggregateTestHarness::<SumState>::new();
h.update(|s| s.total = 42);
assert_eq!(h.finalize(), SumState { total: 42 });
}
#[test]
fn state_borrow() {
let mut h = AggregateTestHarness::<SumState>::new();
h.update(|s| s.total = 7);
assert_eq!(h.state().total, 7);
h.update(|s| s.total += 1);
assert_eq!(h.state().total, 8);
}
#[test]
fn reset_clears_state() {
let mut h = AggregateTestHarness::<SumState>::new();
h.update(|s| s.total = 999);
h.reset();
assert_eq!(h.state().total, 0);
}
#[test]
fn combine_merges_states() {
let mut h1 = AggregateTestHarness::<SumState>::new();
h1.update(|s| s.total += 10);
let mut h2 = AggregateTestHarness::<SumState>::new();
h2.update(|s| s.total += 20);
h2.combine(&h1, |src, tgt| tgt.total += src.total);
assert_eq!(h2.finalize().total, 30);
}
#[test]
fn combine_propagates_config_fields() {
let mut h1 = AggregateTestHarness::<CountConfig>::new();
h1.update(|s| {
s.window_size = 3600; s.count += 5;
});
let h2 = AggregateTestHarness::<CountConfig>::new();
let mut target = h2;
target.combine(&h1, |src, tgt| {
tgt.window_size = src.window_size;
tgt.count += src.count;
});
let result = target.finalize();
assert_eq!(
result.window_size, 3600,
"config field must be propagated in combine"
);
assert_eq!(result.count, 5);
}
#[test]
fn combine_bug_missing_config_propagation() {
let mut h1 = AggregateTestHarness::<CountConfig>::new();
h1.update(|s| {
s.window_size = 3600;
s.count += 5;
});
let h2 = AggregateTestHarness::<CountConfig>::new();
let mut target = h2;
target.combine(&h1, |src, tgt| {
tgt.count += src.count; });
let result = target.finalize();
assert_eq!(
result.window_size, 0,
"demonstrates the bug: config is lost"
);
assert_eq!(result.count, 5);
}
#[test]
fn aggregate_convenience_method() {
let result =
AggregateTestHarness::<SumState>::aggregate([1_i64, 2, 3, 4, 5], |s, v| s.total += v);
assert_eq!(result.total, 15);
}
#[test]
fn aggregate_empty_input() {
let result =
AggregateTestHarness::<SumState>::aggregate(std::iter::empty::<i64>(), |s, v| {
s.total += v;
});
assert_eq!(result.total, 0);
}
#[test]
fn default_impl() {
let h = AggregateTestHarness::<SumState>::default();
assert_eq!(h.state().total, 0);
}
mod proptest_harness {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn aggregate_sum_matches_iter_sum(values: Vec<i64>) {
let safe_values: Vec<i64> = values.iter().copied()
.filter(|&v| v.abs() < 1_000_000)
.collect();
let harness_sum = AggregateTestHarness::<SumState>::aggregate(
safe_values.iter().copied(),
|s, v| s.total += v,
);
let iter_sum: i64 = safe_values.iter().sum();
prop_assert_eq!(harness_sum.total, iter_sum);
}
#[test]
fn combine_is_associative(a: i64, b: i64, c: i64) {
let limit = 1_000_000_i64;
let (a, b, c) = (a % limit, b % limit, c % limit);
let mut h1 = AggregateTestHarness::<SumState>::new();
h1.update(|s| s.total = a);
let mut h2 = AggregateTestHarness::<SumState>::new();
h2.update(|s| s.total = b);
h2.combine(&h1, |src, tgt| tgt.total += src.total);
let mut h3 = AggregateTestHarness::<SumState>::new();
h3.update(|s| s.total = c);
h3.combine(&h2, |src, tgt| tgt.total += src.total);
let result1 = h3.finalize().total;
let mut h_b = AggregateTestHarness::<SumState>::new();
h_b.update(|s| s.total = b);
let mut h_c = AggregateTestHarness::<SumState>::new();
h_c.update(|s| s.total = c);
h_c.combine(&h_b, |src, tgt| tgt.total += src.total);
let mut h_a = AggregateTestHarness::<SumState>::new();
h_a.update(|s| s.total = a);
h_a.combine(&h_c, |src, tgt| tgt.total += src.total);
let result2 = h_a.finalize().total;
prop_assert_eq!(result1, result2);
}
#[test]
fn combine_identity_element(value: i64) {
let v = value % 1_000_000;
let mut h = AggregateTestHarness::<SumState>::new();
h.update(|s| s.total = v);
let empty = AggregateTestHarness::<SumState>::new();
h.combine(&empty, |src, tgt| tgt.total += src.total);
prop_assert_eq!(h.finalize().total, v);
}
}
}
}