use std::collections::{HashMap, HashSet};
use proptest::prelude::*;
use teksilo_core::Signal;
use teksilo_data::{
CheckState, CheckedModel, KeyedTreeCheckedModel, NodeId, TreeCheckedModel, TreeDataSlice,
TreeModel, TreeRow,
};
fn arb_parent_sel(max_nodes: usize) -> impl Strategy<Value = Option<u16>> {
prop_oneof![
1 => Just(None),
4 => (0u16..max_nodes as u16).prop_map(Some),
]
}
fn arb_insert_ops(max_nodes: usize) -> impl Strategy<Value = Vec<Option<u16>>> {
prop::collection::vec(arb_parent_sel(max_nodes), 1..=max_nodes)
}
fn build_tree(ops: &[Option<u16>]) -> (TreeModel<()>, Vec<NodeId>) {
let tree = TreeModel::new();
let ids = append_tree(&tree, ops);
(tree, ids)
}
fn append_tree(tree: &TreeModel<()>, ops: &[Option<u16>]) -> Vec<NodeId> {
let mut ids: Vec<NodeId> = Vec::with_capacity(ops.len());
for (i, sel) in ops.iter().enumerate() {
let node = match sel {
Some(s) if i > 0 => {
let parent = ids[(*s as usize) % i];
let idx = tree.child_count(parent);
tree.insert_child(parent, idx, ())
}
_ => {
let idx = tree.root_count();
tree.insert_root(idx, ())
}
};
ids.push(node);
}
ids
}
fn preorder_rows(tree: &TreeModel<()>, ids: &[NodeId]) -> Vec<TreeRow<u64, ()>> {
let key_of: HashMap<NodeId, u64> = ids
.iter()
.enumerate()
.map(|(i, &id)| (id, i as u64))
.collect();
let mut rows = Vec::with_capacity(ids.len());
fn walk(
tree: &TreeModel<()>,
node: NodeId,
depth: usize,
key_of: &HashMap<NodeId, u64>,
rows: &mut Vec<TreeRow<u64, ()>>,
) {
rows.push(TreeRow::new(key_of[&node], (), depth));
for child in tree.children(node) {
walk(tree, child, depth + 1, key_of, rows);
}
}
for i in 0..tree.root_count() {
walk(tree, tree.root(i), 0, &key_of, &mut rows);
}
rows
}
fn combine_tristate(states: impl IntoIterator<Item = CheckState>) -> CheckState {
let mut any_checked = false;
let mut any_unchecked = false;
for s in states {
match s {
CheckState::Checked => any_checked = true,
CheckState::Unchecked => any_unchecked = true,
CheckState::Indeterminate => {
any_checked = true;
any_unchecked = true;
}
}
}
match (any_checked, any_unchecked) {
(true, false) => CheckState::Checked,
(false, true) => CheckState::Unchecked,
_ => CheckState::Indeterminate,
}
}
fn brute_force_tree_state(
tree: &TreeModel<()>,
model: &TreeCheckedModel<()>,
node: NodeId,
) -> CheckState {
let children = tree.children(node);
if children.is_empty() {
return model.check_state(node);
}
combine_tristate(
children
.into_iter()
.map(|c| brute_force_tree_state(tree, model, c)),
)
}
fn brute_force_keyed_state(
slice: &TreeDataSlice<u64, ()>,
model: &KeyedTreeCheckedModel<u64>,
key: u64,
) -> CheckState {
let children = slice.child_keys_of(&key);
if children.is_empty() {
return model.check_state(&key);
}
combine_tristate(
children
.into_iter()
.map(|c| brute_force_keyed_state(slice, model, c)),
)
}
#[derive(Debug, Clone, Copy)]
enum Op {
Check(usize),
Uncheck(usize),
ToggleLeaf(usize),
}
fn arb_op(n: usize) -> impl Strategy<Value = Op> {
prop_oneof![
(0..n).prop_map(Op::Check),
(0..n).prop_map(Op::Uncheck),
(0..n).prop_map(Op::ToggleLeaf),
]
}
fn arb_case(
max_nodes: usize,
max_ops: usize,
) -> impl Strategy<Value = (Vec<Option<u16>>, Vec<Op>)> {
arb_insert_ops(max_nodes).prop_flat_map(move |tree_ops| {
let n = tree_ops.len();
prop::collection::vec(arb_op(n), 0..=max_ops).prop_map(move |ops| (tree_ops.clone(), ops))
})
}
fn apply_tree_op(tree: &TreeModel<()>, model: &TreeCheckedModel<()>, ids: &[NodeId], op: Op) {
match op {
Op::Check(i) => model.check(ids[i]),
Op::Uncheck(i) => model.uncheck(ids[i]),
Op::ToggleLeaf(i) => {
if tree.children(ids[i]).is_empty() {
model.toggle(ids[i]);
}
}
}
}
fn apply_keyed_op(slice: &TreeDataSlice<u64, ()>, model: &KeyedTreeCheckedModel<u64>, op: Op) {
match op {
Op::Check(i) => model.check(i as u64),
Op::Uncheck(i) => model.uncheck(i as u64),
Op::ToggleLeaf(i) => {
let key = i as u64;
if slice.child_keys_of(&key).is_empty() {
model.toggle(key);
}
}
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 512, ..ProptestConfig::default() })]
#[test]
fn every_node_state_matches_the_leaf_aggregate_after_any_check_uncheck_sequence(
(tree_ops, ops) in arb_case(20, 30)
) {
let (tree, ids) = build_tree(&tree_ops);
let model = TreeCheckedModel::new(tree.clone());
for op in &ops {
apply_tree_op(&tree, &model, &ids, *op);
for &id in &ids {
let actual = model.check_state(id);
let expected = brute_force_tree_state(&tree, &model, id);
prop_assert_eq!(
actual, expected,
"node {:?} has state {:?} but the brute-force leaf aggregate says {:?} \
after op {:?} (tree_ops={:?}, full ops={:?})",
id, actual, expected, op, tree_ops, ops
);
}
}
}
}
fn append_star(tree: &TreeModel<()>, k: usize) -> Vec<NodeId> {
let root = tree.insert_root(tree.root_count(), ());
let mut ids = vec![root];
for i in 0..k {
ids.push(tree.insert_child(root, i, ()));
}
ids
}
fn arb_reentrancy_case(
max_children: usize,
max_ops: usize,
) -> impl Strategy<Value = (usize, usize, Vec<Op>)> {
(1..=max_children, 1..=max_children).prop_flat_map(move |(ka, kb)| {
let n = (1 + ka) + (1 + kb);
prop::collection::vec(arb_op(n), 0..=max_ops).prop_map(move |ops| (ka, kb, ops))
})
}
proptest! {
#![proptest_config(ProptestConfig { cases: 512, ..ProptestConfig::default() })]
#[test]
fn reentrant_check_from_an_observer_still_recomputes_its_own_ancestors(
(ka, kb, ops) in arb_reentrancy_case(8, 30)
) {
let tree = TreeModel::<()>::new();
let ids_a = append_star(&tree, ka); let ids_b = append_star(&tree, kb); let model = TreeCheckedModel::new(tree.clone());
let trigger = ids_a[1];
let sentinel = ids_b[1];
let model_for_observer = model.clone();
let _obs = model.signal_for(trigger).observe(move |state| {
if *state == CheckState::Checked {
model_for_observer.check(sentinel);
}
});
let mut ids = ids_a.clone();
ids.extend(ids_b.clone());
for op in &ops {
apply_tree_op(&tree, &model, &ids, *op);
for &id in &ids {
let actual = model.check_state(id);
let expected = brute_force_tree_state(&tree, &model, id);
prop_assert_eq!(
actual, expected,
"node {:?} has state {:?} but the brute-force leaf aggregate says {:?} \
after op {:?}, with a reentrant observer checking an unrelated node \
from inside trigger's own cascade (ka={}, kb={})",
id, actual, expected, op, ka, kb
);
}
}
}
}
proptest! {
#[test]
fn toggling_a_leaf_twice_restores_every_nodes_state(
(tree_ops, ops) in arb_case(20, 30)
) {
let (tree, ids) = build_tree(&tree_ops);
let model = TreeCheckedModel::new(tree.clone());
for op in &ops {
apply_tree_op(&tree, &model, &ids, *op);
}
let Some(&leaf) = ids.iter().find(|&&id| tree.children(id).is_empty()) else {
return Ok(());
};
let before: Vec<CheckState> = ids.iter().map(|&id| model.check_state(id)).collect();
model.toggle(leaf);
model.toggle(leaf);
let after: Vec<CheckState> = ids.iter().map(|&id| model.check_state(id)).collect();
prop_assert_eq!(
after, before,
"toggling leaf {:?} twice must restore every node's state exactly \
(tree_ops={:?}, ops={:?})",
leaf, tree_ops, ops
);
}
}
#[derive(Debug, Clone, Copy)]
enum BridgeOp {
Check(usize),
Uncheck(usize),
ToggleLeaf(usize),
SetBool(usize, bool),
}
fn arb_bridge_op(n: usize) -> impl Strategy<Value = BridgeOp> {
prop_oneof![
(0..n).prop_map(BridgeOp::Check),
(0..n).prop_map(BridgeOp::Uncheck),
(0..n).prop_map(BridgeOp::ToggleLeaf),
(0..n, any::<bool>()).prop_map(|(i, b)| BridgeOp::SetBool(i, b)),
]
}
fn arb_bridge_case(
max_nodes: usize,
max_ops: usize,
) -> impl Strategy<Value = (Vec<Option<u16>>, Vec<BridgeOp>)> {
arb_insert_ops(max_nodes).prop_flat_map(move |tree_ops| {
let n = tree_ops.len();
prop::collection::vec(arb_bridge_op(n), 0..=max_ops)
.prop_map(move |ops| (tree_ops.clone(), ops))
})
}
proptest! {
#[test]
fn bool_signal_bridge_matches_tristate_checked_for_every_leaf(
(tree_ops, ops) in arb_bridge_case(20, 30)
) {
let (tree, ids) = build_tree(&tree_ops);
let model = TreeCheckedModel::new(tree.clone());
let leaf_bools: Vec<(NodeId, Signal<bool>)> = ids
.iter()
.copied()
.filter(|&id| tree.children(id).is_empty())
.map(|id| (id, model.bool_signal_for(id)))
.collect();
for op in &ops {
match *op {
BridgeOp::Check(i) => model.check(ids[i]),
BridgeOp::Uncheck(i) => model.uncheck(ids[i]),
BridgeOp::ToggleLeaf(i) => {
if tree.children(ids[i]).is_empty() {
model.toggle(ids[i]);
}
}
BridgeOp::SetBool(i, value) => {
if tree.children(ids[i]).is_empty() {
model.bool_signal_for(ids[i]).set(value);
}
}
}
for (id, bool_sig) in &leaf_bools {
let tri = model.check_state(*id);
prop_assert_eq!(
bool_sig.get(), tri == CheckState::Checked,
"leaf {:?}: bool signal is {} but tristate is {:?} (expected bool == \
(tristate == Checked)) after op {:?} (tree_ops={:?})",
id, bool_sig.get(), tri, op, tree_ops
);
}
}
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 512, ..ProptestConfig::default() })]
#[test]
fn every_key_state_matches_the_leaf_aggregate_after_any_check_uncheck_sequence_keyed(
(tree_ops, ops) in arb_case(20, 30)
) {
let (tree, ids) = build_tree(&tree_ops);
let rows = preorder_rows(&tree, &ids);
let slice = TreeDataSlice::from_rows(rows);
let model = KeyedTreeCheckedModel::from_source(slice.clone());
let n = ids.len();
for op in &ops {
apply_keyed_op(&slice, &model, *op);
for i in 0..n {
let key = i as u64;
let actual = model.check_state(&key);
let expected = brute_force_keyed_state(&slice, &model, key);
prop_assert_eq!(
actual, expected,
"key {} has state {:?} but the brute-force leaf aggregate says {:?} \
after op {:?} (tree_ops={:?}, full ops={:?})",
key, actual, expected, op, tree_ops, ops
);
}
}
}
}
proptest! {
#[test]
fn reaggregate_is_a_noop_when_the_model_is_already_consistent(
(tree_ops, ops) in arb_case(20, 30)
) {
let (tree, ids) = build_tree(&tree_ops);
let rows = preorder_rows(&tree, &ids);
let slice = TreeDataSlice::from_rows(rows);
let model = KeyedTreeCheckedModel::from_source(slice.clone());
let n = ids.len();
for op in &ops {
apply_keyed_op(&slice, &model, *op);
}
let before: Vec<CheckState> = (0..n).map(|i| model.check_state(&(i as u64))).collect();
model.reaggregate();
let after: Vec<CheckState> = (0..n).map(|i| model.check_state(&(i as u64))).collect();
prop_assert_eq!(
after, before,
"reaggregate() must not change any key's state when the model is already \
consistent (tree_ops={:?}, ops={:?})",
tree_ops, ops
);
}
}
proptest! {
#[test]
fn reaggregate_reaches_a_fixed_point_even_from_a_forced_inconsistent_state(
(tree_ops, ops, force_indices) in arb_case(20, 30).prop_flat_map(|(tree_ops, ops)| {
let n = tree_ops.len();
prop::collection::vec(0..n, 0..=5).prop_map(move |idxs| (tree_ops.clone(), ops.clone(), idxs))
})
) {
let (tree, ids) = build_tree(&tree_ops);
let rows = preorder_rows(&tree, &ids);
let slice = TreeDataSlice::from_rows(rows);
let model = KeyedTreeCheckedModel::from_source(slice.clone());
let n = ids.len();
for op in &ops {
apply_keyed_op(&slice, &model, *op);
}
for i in force_indices {
let key = i as u64;
if !slice.child_keys_of(&key).is_empty() {
model.signal_for(key).set(CheckState::Indeterminate);
}
}
model.reaggregate();
let after_one: Vec<CheckState> = (0..n).map(|i| model.check_state(&(i as u64))).collect();
model.reaggregate();
let after_two: Vec<CheckState> = (0..n).map(|i| model.check_state(&(i as u64))).collect();
prop_assert_eq!(
after_two, after_one,
"a second reaggregate() must be a no-op once the first has converged \
(tree_ops={:?}, ops={:?})",
tree_ops, ops
);
}
}
proptest! {
#[test]
fn prune_missing_drops_removed_keys_and_reaggregates_surviving_ancestors(
(tree_ops, ops, victim_idx) in arb_case(20, 30).prop_flat_map(|(tree_ops, ops)| {
let n = tree_ops.len();
(0..n).prop_map(move |v| (tree_ops.clone(), ops.clone(), v))
})
) {
let (tree, ids) = build_tree(&tree_ops);
let rows = preorder_rows(&tree, &ids);
let slice = TreeDataSlice::from_rows(rows.clone());
let model = KeyedTreeCheckedModel::from_source(slice.clone());
let n = ids.len();
for op in &ops {
apply_keyed_op(&slice, &model, *op);
}
let victim = victim_idx as u64;
let mut removed: HashSet<u64> = HashSet::new();
let mut stack = vec![victim];
while let Some(k) = stack.pop() {
if removed.insert(k) {
stack.extend(slice.child_keys_of(&k));
}
}
let new_rows: Vec<TreeRow<u64, ()>> = rows.into_iter().filter(|r| !removed.contains(&r.key)).collect();
slice.set_rows(new_rows);
model.prune_missing(|k| slice.contains_key(k));
for i in 0..n {
let key = i as u64;
if removed.contains(&key) {
prop_assert_eq!(
model.check_state(&key), CheckState::Unchecked,
"removed key {} must read back as Unchecked (forgotten) after prune_missing \
(victim={}, tree_ops={:?})",
key, victim, tree_ops
);
prop_assert!(
!model.checked_keys().contains(&key),
"removed key {} must not appear in checked_keys() after prune_missing",
key
);
} else {
let actual = model.check_state(&key);
let expected = brute_force_keyed_state(&slice, &model, key);
prop_assert_eq!(
actual, expected,
"surviving key {} has state {:?} but the brute-force leaf aggregate over \
the POST-prune tree shape says {:?} (victim={}, tree_ops={:?})",
key, actual, expected, victim, tree_ops
);
}
}
}
}
fn arb_insert_ops_exact(n: usize) -> impl Strategy<Value = Vec<Option<u16>>> {
prop::collection::vec(arb_parent_sel(n), n..=n)
}
proptest! {
#[test]
fn checked_state_survives_a_reload_with_a_different_shape_then_resyncs_on_reaggregate(
(tree_ops, ops, tree_ops2) in arb_case(20, 30).prop_flat_map(|(tree_ops, ops)| {
let n = tree_ops.len();
arb_insert_ops_exact(n).prop_map(move |ops2| (tree_ops.clone(), ops.clone(), ops2))
})
) {
let (tree, ids) = build_tree(&tree_ops);
let rows = preorder_rows(&tree, &ids);
let slice = TreeDataSlice::from_rows(rows);
let model = KeyedTreeCheckedModel::from_source(slice.clone());
let n = ids.len();
for op in &ops {
apply_keyed_op(&slice, &model, *op);
}
let before: Vec<CheckState> = (0..n).map(|i| model.check_state(&(i as u64))).collect();
let (tree2, ids2) = build_tree(&tree_ops2);
let rows2 = preorder_rows(&tree2, &ids2);
slice.set_rows(rows2);
let after_reload: Vec<CheckState> = (0..n).map(|i| model.check_state(&(i as u64))).collect();
prop_assert_eq!(
after_reload, before.clone(),
"raw check state for every key must be byte-for-byte unchanged immediately \
after set_rows reshapes the source, before any reaggregate \
(tree_ops={:?}, tree_ops2={:?}, ops={:?})",
tree_ops, tree_ops2, ops
);
model.reaggregate();
for i in 0..n {
let key = i as u64;
let actual = model.check_state(&key);
let expected = brute_force_keyed_state(&slice, &model, key);
prop_assert_eq!(
actual, expected,
"key {} must match the brute-force leaf aggregate over the NEW shape once \
reaggregate() is called after a reload (tree_ops2={:?})",
key, tree_ops2
);
}
}
}
#[derive(Debug, Clone, Copy)]
enum FlatOp {
Check(usize),
Uncheck(usize),
Insert(usize, usize),
Remove(usize, usize),
Move(usize, usize, usize),
}
fn arb_flat_op() -> impl Strategy<Value = FlatOp> {
prop_oneof![
(0usize..1000).prop_map(FlatOp::Check),
(0usize..1000).prop_map(FlatOp::Uncheck),
(0usize..1000, 1usize..=4).prop_map(|(p, c)| FlatOp::Insert(p, c)),
(0usize..1000, 1usize..=4).prop_map(|(p, c)| FlatOp::Remove(p, c)),
(0usize..1000, 0usize..1000, 1usize..=4).prop_map(|(f, t, c)| FlatOp::Move(f, t, c)),
]
}
fn arb_flat_case() -> impl Strategy<Value = (usize, Vec<FlatOp>)> {
(0usize..=15, prop::collection::vec(arb_flat_op(), 0..=30))
}
proptest! {
#[test]
fn checked_indices_matches_a_logical_identity_model_after_arbitrary_shifts(
(initial_len, ops) in arb_flat_case()
) {
let m = CheckedModel::new();
let mut logical: Vec<u64> = (0..initial_len as u64).collect();
let mut next_id: u64 = initial_len as u64;
let mut checked_ids: HashSet<u64> = HashSet::new();
for op in &ops {
match *op {
FlatOp::Check(raw) => {
if !logical.is_empty() {
let i = raw % logical.len();
m.check(i);
checked_ids.insert(logical[i]);
}
}
FlatOp::Uncheck(raw) => {
if !logical.is_empty() {
let i = raw % logical.len();
m.uncheck(i);
checked_ids.remove(&logical[i]);
}
}
FlatOp::Insert(raw_at, count) => {
let at = raw_at % (logical.len() + 1);
m.adjust_for_insert(at, count);
let new_ids: Vec<u64> = (0..count as u64).map(|k| next_id + k).collect();
next_id += count as u64;
logical.splice(at..at, new_ids);
}
FlatOp::Remove(raw_at, raw_count) => {
if !logical.is_empty() {
let at = raw_at % logical.len();
let count = raw_count.min(logical.len() - at);
m.adjust_for_remove(at, count);
for id in logical.drain(at..at + count) {
checked_ids.remove(&id);
}
}
}
FlatOp::Move(raw_from, raw_to, raw_count) => {
if !logical.is_empty() {
let from = raw_from % logical.len();
let count = raw_count.min(logical.len() - from);
if count > 0 {
let remaining = logical.len() - count;
let to = raw_to % (remaining + 1);
m.adjust_for_move(from, to, count);
let block: Vec<u64> = logical.drain(from..from + count).collect();
for (k, id) in block.into_iter().enumerate() {
logical.insert(to + k, id);
}
}
}
}
}
let expected: Vec<usize> = (0..logical.len())
.filter(|&i| checked_ids.contains(&logical[i]))
.collect();
prop_assert_eq!(
m.checked_indices(), expected,
"checked_indices() diverged from the logical-identity model after op {:?} \
(initial_len={}, ops={:?})",
op, initial_len, ops
);
}
}
}