use proptest::prelude::*;
use renew_ecs::{Entities, Entity, Store};
use renew_fixed::{Angle, Fixed, Vec2};
use renew_scene::{Global, Local, Parent, Propagated, Scratch, propagate};
#[derive(Clone, Debug)]
struct Shape {
parents: Vec<Option<usize>>,
locals: Vec<Local>,
}
fn shapes(max_nodes: usize) -> impl Strategy<Value = Shape> {
prop::collection::vec(
(
0usize..8,
-1000i32..1000,
-1000i32..1000,
any::<u32>(),
),
1..max_nodes,
)
.prop_map(|rows| {
let mut parents = Vec::with_capacity(rows.len());
let mut locals = Vec::with_capacity(rows.len());
for (index, (back, x, y, turn)) in rows.into_iter().enumerate() {
parents.push(if back == 0 || back > index {
None
} else {
Some(index - back)
});
locals.push(Local::new(
Vec2::new(Fixed::from_int(x), Fixed::from_int(y)),
Angle::from_bits(turn),
));
}
Shape { parents, locals }
})
}
fn reference(shape: &Shape) -> Vec<(Vec2, Angle)> {
let mut placed: Vec<(Vec2, Angle)> = Vec::with_capacity(shape.parents.len());
for (index, parent) in shape.parents.iter().enumerate() {
let (base_translation, base_rotation) = parent
.and_then(|parent| placed.get(parent).copied())
.unwrap_or((Vec2::ZERO, Angle::ZERO));
let local = shape.locals[index];
placed.push((
base_translation + local.translation.rotate(base_rotation),
base_rotation + local.rotation,
));
}
placed
}
struct Laid {
entities: Vec<Entity>,
globals: Store<Global>,
counts: Propagated,
}
fn lay_out(shape: &Shape, order: &[usize]) -> Laid {
let mut entities = Entities::new();
let created: Vec<Entity> = order.iter().map(|_| entities.spawn()).collect();
let mut handles = created.clone();
for (position, &index) in order.iter().enumerate() {
handles[index] = created[position];
}
let mut parents: Store<Parent> = Store::default();
let mut locals: Store<Local> = Store::default();
let mut globals: Store<Global> = Store::default();
for (index, handle) in handles.iter().enumerate() {
locals.insert(handle.index(), shape.locals[index]);
if let Some(parent) = shape.parents[index] {
parents.insert(handle.index(), Parent(handles[parent]));
}
}
let mut scratch = Scratch::new();
let counts = propagate(&mut scratch, &entities, &parents, &locals, &mut globals);
assert_eq!(
counts.nodes as usize,
shape.parents.len(),
"totality: every node in the table must be placed"
);
assert_eq!(counts.cyclic, 0, "the strategy cannot emit a loop");
assert_eq!(counts.orphaned, 0, "nothing was despawned");
Laid {
entities: handles,
globals,
counts,
}
}
#[allow(clippy::expect_used, reason = "a missing placement is the defect")]
fn placement(laid: &Laid, index: usize) -> Global {
*laid
.globals
.get(laid.entities[index].index())
.expect("placed")
}
proptest! {
#[test]
fn the_pass_agrees_with_an_independent_composition(shape in shapes(40)) {
let expected = reference(&shape);
let order: Vec<usize> = (0..shape.parents.len()).collect();
let laid = lay_out(&shape, &order);
for (index, &(translation, rotation)) in expected.iter().enumerate() {
let placed = placement(&laid, index);
prop_assert_eq!(placed.translation(), translation, "node {}", index);
prop_assert_eq!(placed.rotation(), rotation, "node {}", index);
}
}
#[test]
fn placements_survive_relabelling(shape in shapes(40), seed in any::<u64>()) {
let count = shape.parents.len();
let straight: Vec<usize> = (0..count).collect();
let mut shuffled = straight.clone();
let mut state = seed | 1;
for index in (1..count).rev() {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let span = u64::try_from(index).unwrap_or(u64::MAX).saturating_add(1);
let pick = usize::try_from(state % span).unwrap_or(0);
shuffled.swap(index, pick);
}
if shuffled == straight && count > 1 {
shuffled.swap(0, 1);
}
let edges = shape.parents.iter().filter(|p| p.is_some()).count();
let inverts = |order: &[usize]| {
let mut slots = vec![0usize; count];
for (position, &index) in order.iter().enumerate() {
slots[index] = position;
}
(0..count).any(|n| shape.parents[n].is_some_and(|p| slots[n] < slots[p]))
};
if edges > 0 && !inverts(&shuffled) {
shuffled = straight.iter().rev().copied().collect();
}
let first = lay_out(&shape, &straight);
let second = lay_out(&shape, &shuffled);
let differs = (0..count).any(|n| first.entities[n].index() != second.entities[n].index());
prop_assert!(
differs || count == 1,
"premise: the permutation must actually move something"
);
if edges > 0 {
prop_assert!(
(0..count).any(|n| {
shape.parents[n]
.is_some_and(|p| second.entities[n].index() < second.entities[p].index())
}),
"premise: no child ended up below its parent, so resolution order went untested"
);
}
let expected = reference(&shape);
for (index, &(translation, rotation)) in expected.iter().enumerate() {
let a = placement(&first, index);
let b = placement(&second, index);
prop_assert_eq!(a.translation(), b.translation(), "node {}", index);
prop_assert_eq!(a.rotation(), b.rotation(), "node {}", index);
prop_assert_eq!(b.translation(), translation, "node {} vs reference", index);
prop_assert_eq!(b.rotation(), rotation, "node {} vs reference", index);
}
}
#[test]
fn the_counts_report_how_many_independent_trees_there_were(shape in shapes(40)) {
let order: Vec<usize> = (0..shape.parents.len()).collect();
let laid = lay_out(&shape, &order);
let rootless = shape.parents.iter().filter(|p| p.is_none()).count();
prop_assert_eq!(laid.counts.roots as usize, rootless);
prop_assert_eq!(laid.counts.orphaned, 0);
prop_assert_eq!(laid.counts.cyclic, 0);
prop_assert_eq!(laid.counts.nodes as usize, shape.parents.len());
}
#[test]
fn a_second_pass_over_an_unchanged_world_changes_nothing(shape in shapes(24)) {
let mut entities = Entities::new();
let handles: Vec<Entity> = (0..shape.parents.len()).map(|_| entities.spawn()).collect();
let mut parents: Store<Parent> = Store::default();
let mut locals: Store<Local> = Store::default();
let mut globals: Store<Global> = Store::default();
for (index, handle) in handles.iter().enumerate() {
locals.insert(handle.index(), shape.locals[index]);
if let Some(parent) = shape.parents[index] {
parents.insert(handle.index(), Parent(handles[parent]));
}
}
let mut scratch = Scratch::new();
let first = propagate(&mut scratch, &entities, &parents, &locals, &mut globals);
let after_first: Vec<Global> = handles
.iter()
.map(|h| *globals.get(h.index()).expect("placed"))
.collect();
let second = propagate(&mut scratch, &entities, &parents, &locals, &mut globals);
let after_second: Vec<Global> = handles
.iter()
.map(|h| *globals.get(h.index()).expect("placed"))
.collect();
prop_assert_eq!(first, second);
prop_assert_eq!(after_first, after_second);
}
}