use proptest::prelude::*;
use proptest::test_runner::RngSeed;
use renew_ecs::{Entities, Store, join};
#[derive(Clone, Copy, Debug)]
enum Op {
Insert(u32, u32),
Remove(u32),
Replace(u32, u32),
}
fn op() -> impl Strategy<Value = Op> {
prop_oneof![
(0u32..12, any::<u32>()).prop_map(|(slot, value)| Op::Insert(slot, value)),
(0u32..12).prop_map(Op::Remove),
(0u32..12, any::<u32>()).prop_map(|(slot, value)| Op::Replace(slot, value)),
]
}
proptest! {
#![proptest_config(ProptestConfig {
rng_seed: RngSeed::Fixed(0x51a7_c0de_0e15_9a44),
cases: 256,
..ProptestConfig::default()
})]
#[test]
fn the_store_matches_a_naive_model(ops in proptest::collection::vec(op(), 0..80)) {
let mut store: Store<u32> = Store::new();
let mut model: Vec<Option<u32>> = vec![None; 12];
for (step, operation) in ops.iter().enumerate() {
match *operation {
Op::Insert(slot, value) | Op::Replace(slot, value) => {
let returned = store.insert(slot, value);
let expected = model[slot as usize].replace(value);
prop_assert_eq!(returned, expected, "step {} insert {}", step, slot);
}
Op::Remove(slot) => {
let returned = store.remove(slot);
let expected = model[slot as usize].take();
prop_assert_eq!(returned, expected, "step {} remove {}", step, slot);
}
}
let live = model.iter().filter(|slot| slot.is_some()).count();
prop_assert_eq!(store.len(), live, "step {} length", step);
prop_assert_eq!(store.is_empty(), live == 0, "step {} emptiness", step);
for (slot, expected) in model.iter().enumerate() {
let slot = u32::try_from(slot).unwrap_or(u32::MAX);
prop_assert_eq!(store.get(slot), expected.as_ref(), "step {} slot {}", step, slot);
prop_assert_eq!(store.contains(slot), expected.is_some(), "step {} contains {}", step, slot);
}
}
}
#[test]
fn iteration_is_always_sorted(ops in proptest::collection::vec(op(), 0..80)) {
let mut store: Store<u32> = Store::new();
for operation in &ops {
match *operation {
Op::Insert(slot, value) | Op::Replace(slot, value) => {
store.insert(slot, value);
}
Op::Remove(slot) => {
store.remove(slot);
}
}
}
let slots: Vec<u32> = store.iter().map(|(slot, _)| slot).collect();
let mut sorted = slots.clone();
sorted.sort_unstable();
prop_assert_eq!(&slots, &sorted);
prop_assert_eq!(slots.len(), store.len());
}
#[test]
fn the_same_final_contents_iterate_identically(
ops in proptest::collection::vec(op(), 0..60),
churn in proptest::collection::vec(op(), 0..40),
) {
let apply = |sequence: &[Op]| {
let mut store: Store<u32> = Store::new();
for operation in sequence {
match *operation {
Op::Insert(slot, value) | Op::Replace(slot, value) => {
store.insert(slot, value);
}
Op::Remove(slot) => {
store.remove(slot);
}
}
}
store
};
let direct = apply(&ops);
let mut prefixed: Vec<Op> = churn.clone();
prefixed.extend_from_slice(&ops);
let churned = apply(&prefixed);
for (slot, value) in direct.iter() {
if let Some(other) = churned.get(slot) {
prop_assert_eq!(value, other, "slot {} disagrees", slot);
}
}
let one: Vec<u32> = direct.iter().map(|(slot, _)| slot).collect();
let two: Vec<u32> = churned.iter().map(|(slot, _)| slot).collect();
prop_assert!(one.iter().is_sorted());
prop_assert!(two.iter().is_sorted());
}
#[test]
fn a_join_is_the_ordered_intersection(
left_slots in proptest::collection::vec(0u32..16, 0..16),
right_slots in proptest::collection::vec(0u32..16, 0..16),
) {
let mut left: Store<u32> = Store::new();
let mut right: Store<u32> = Store::new();
for slot in &left_slots {
left.insert(*slot, *slot);
}
for slot in &right_slots {
right.insert(*slot, *slot);
}
let mut expected: Vec<u32> = left_slots
.iter()
.copied()
.filter(|slot| right_slots.contains(slot))
.collect();
expected.sort_unstable();
expected.dedup();
let found: Vec<u32> = join(&left, &right).map(|(slot, _, _)| slot).collect();
prop_assert_eq!(found, expected);
}
#[test]
fn handles_are_never_confused(rounds in proptest::collection::vec(any::<bool>(), 0..120)) {
let mut entities = Entities::new();
let mut live: Vec<renew_ecs::Entity> = Vec::new();
let mut retired: Vec<renew_ecs::Entity> = Vec::new();
for spawn in rounds {
if spawn || live.is_empty() {
live.push(entities.spawn());
} else if let Some(victim) = live.pop() {
prop_assert!(entities.despawn(victim));
retired.push(victim);
}
prop_assert_eq!(entities.len(), live.len());
for handle in &live {
prop_assert!(entities.is_alive(*handle), "{} should be alive", handle);
}
for handle in &retired {
prop_assert!(!entities.is_alive(*handle), "{} should be dead", handle);
}
}
}
}