use std::collections::BTreeSet;
use proptest::prelude::*;
use super::harness::{
Program, Transaction, ZSet, any_config, apply_proposals, check, configs, fixpoint, map_steps,
proposals, read_zset, set_after, set_zset, workloads,
};
use crate::{
OutputHandle, RootCircuit, Stream, ZSetHandle, ZWeight,
typed_batch::{OrdZSet, SpineSnapshot},
utils::{Tup2, test::CIRCUIT_CASES},
};
type Version = Tup2<u64, u64>;
type ActionChanges = Vec<(Version, ZWeight)>;
#[derive(Clone)]
struct StateMachine;
impl Program for StateMachine {
type Input = ActionChanges;
type Handles = (
ZSetHandle<Version>,
OutputHandle<SpineSnapshot<OrdZSet<Version>>>,
);
type Output = ZSet<Version>;
fn build(&self, circuit: &mut RootCircuit) -> Self::Handles {
let (actions, actions_handle) = circuit.add_input_zset::<Version>();
let state = circuit
.recursive(|child, state: Stream<_, OrdZSet<Version>>| {
let actions = actions.delta0(child);
let state_by_id = state.map_index(|Tup2(id, version)| (*id, *version));
let next_state = state
.map_index(|Tup2(id, version)| (Tup2(*id, version + 1), ()))
.join(
&actions.map_index(|action| (*action, ())),
|action, _, _| *action,
);
let started = actions
.filter(|Tup2(_, version)| *version == 0)
.map_index(|Tup2(id, version)| (*id, *version))
.antijoin(&state_by_id)
.map(|(id, version)| Tup2(*id, *version));
let unchanged = state_by_id
.antijoin(&next_state.map_index(|Tup2(id, version)| (*id, *version)))
.map(|(id, version)| Tup2(*id, *version));
Ok(started.plus(&next_state).plus(&unchanged))
})
.unwrap();
(
actions_handle,
state.accumulate_integrate().accumulate_output(),
)
}
fn push(&self, (actions, _): &Self::Handles, input: &ActionChanges) {
for (row, weight) in input {
actions.push(*row, *weight);
}
}
fn read(&self, (_, state): &Self::Handles) -> ZSet<Version> {
read_zset(state)
}
fn model(&self, inputs: &[ActionChanges]) -> ZSet<Version> {
let actions = set_after(inputs.iter().map(Vec::as_slice));
let has_state = |state: &BTreeSet<Version>, id: u64| {
state
.range(Tup2(id, 0)..=Tup2(id, u64::MAX))
.next()
.is_some()
};
let state = fixpoint(BTreeSet::new(), |state: &BTreeSet<Version>| {
let next_state: BTreeSet<Version> = state
.iter()
.map(|Tup2(id, version)| Tup2(*id, version + 1))
.filter(|action| actions.contains(action))
.collect();
let started = actions
.iter()
.filter(|Tup2(id, version)| *version == 0 && !has_state(state, *id));
let unchanged = state
.iter()
.filter(|Tup2(id, _)| !has_state(&next_state, *id));
started
.chain(&next_state)
.chain(unchanged)
.cloned()
.collect()
});
set_zset(&state)
}
}
fn actions(id: u64, versions: impl IntoIterator<Item = u64>, weight: ZWeight) -> ActionChanges {
versions
.into_iter()
.map(|version| (Tup2(id, version), weight))
.collect()
}
fn state_machine_triggers() -> Vec<Vec<Transaction<ActionChanges>>> {
let setup = [actions(0, 0..=8, 1), actions(1, 0..=8, 1)].concat();
vec![
vec![vec![setup.clone()], vec![actions(0, [1], -1)]],
vec![vec![setup.clone()], vec![actions(0, [0], -1)]],
vec![
vec![setup.clone()],
vec![actions(0, [1], -1), actions(1, [2], -1)],
],
vec![
vec![setup],
vec![actions(0, [1], -1)],
vec![actions(0, [1], 1)],
],
]
}
fn state_machine_workloads() -> impl Strategy<Value = Vec<Transaction<ActionChanges>>> {
let action = (0..3u64, 0..6u64).prop_map(|(id, version)| Tup2(id, version));
workloads(proposals(action, 4), 5, 3).prop_map(|raw| {
let mut actions = BTreeSet::new();
map_steps(raw, |proposals| apply_proposals(&mut actions, &proposals))
})
}
#[test]
fn state_machine_triggers_hold() {
for workload in state_machine_triggers() {
for config in configs() {
check(&StateMachine, &workload, config);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(CIRCUIT_CASES))]
#[test]
fn state_machine_random(workload in state_machine_workloads(), config in any_config()) {
check(&StateMachine, &workload, config);
}
}