diff --git a/examples/random.rs b/examples/random.rs index 0fa4ab4..44cf947 100644 --- a/examples/random.rs +++ b/examples/random.rs @@ -2,7 +2,7 @@ //! looked at. `IRIS_SEED` and `IRIS_DEPTH` choose which one. use iris::prelude::*; -use std::collections::HashMap; +use iris::random::Edits; fn env(name: &str, fallback: u64) -> u64 { std::env::var(name) @@ -24,7 +24,7 @@ impl DefaultAppState for State { fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc, _: Proxy) -> Self { let seed = env("IRIS_SEED", 1); let depth = env("IRIS_DEPTH", 4) as usize; - let (root, _) = iris::random::grow(rsc, seed, depth, &HashMap::new()); + let (root, _) = iris::random::grow(rsc, seed, depth, &Edits::default()); ui_state.set_root(root); Self { ui_state } } diff --git a/src/random.rs b/src/random.rs index 56f6b29..385b154 100644 --- a/src/random.rs +++ b/src/random.rs @@ -11,6 +11,24 @@ use std::collections::HashMap; /// The declared lengths of one `SetSize`, by axis. pub type Lens = [Option; 2]; +/// What a test changes between two trees grown from the same seed, so the +/// warm one can be mutated and the cold one grown that way to begin with. +#[derive(Default)] +pub struct Edits { + /// Declared sizes, by the order the `SetSize` wrappers were made. + pub sizes: HashMap, + /// Which children a span has, by the order the spans were made. + pub spans: HashMap, +} + +#[derive(Default, Clone)] +pub struct SpanEdit { + /// Children to leave out, by index among the ones grown. + pub detach: Vec, + /// How many of the span's spares are in it, appended in order. + pub attach: usize, +} + /// xorshift64, written out rather than taken from a crate so that a seed /// keeps meaning the same tree. pub struct Rng(u64); @@ -45,6 +63,9 @@ const COLORS: [UiColor; 6] = [ UiColor::MAGENTA, ]; +/// Leaves grown beside every span, for a test to put into it. +const SPARES: usize = 3; + const WORDS: &str = "Wrapping shapes one source into as many lines as the box \ leaves room for, so a paragraph's height is an answer and not a setting."; @@ -55,6 +76,22 @@ const WORDS: &str = "Wrapping shapes one source into as many lines as the box \ pub struct Tree { pub ids: Vec, pub sized: Vec>, + pub spans: Vec, + /// Children a `SpanEdit` took out, held so that dropping the last share + /// of one does not free its id for the next widget to be given -- which + /// would put the two trees' `ids` out of step. + pub detached: Vec, +} + +pub struct Spanned { + pub id: WeakWidget, + /// Leaves grown with the span whether or not they end up in it, so both + /// trees make the same widgets in the same order either way. Attaching + /// one moves it out of here: a widget belongs to one parent, and one that + /// belongs to nobody still has to be held or it reads as a leak. + pub spares: Vec, + /// How many children it was grown with, before any edit. + pub grown: usize, } /// Grows the tree `seed` describes, `edits` replacing the declared sizes it @@ -63,7 +100,7 @@ pub fn grow( rsc: &mut Rsc, seed: u64, depth: usize, - edits: &HashMap, + edits: &Edits, ) -> (StrongWidget, Tree) { let mut grow = Grow { rsc, @@ -79,7 +116,7 @@ struct Grow<'a, Rsc> { rsc: &'a mut Rsc, rng: Rng, tree: Tree, - edits: &'a HashMap, + edits: &'a Edits, } impl Grow<'_, Rsc> { @@ -117,7 +154,7 @@ impl Grow<'_, Rsc> { } let idx = self.tree.sized.len(); let lens = [self.len(), self.len()]; - let lens = self.edits.get(&idx).copied().unwrap_or(lens); + let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens); let id = SetSize { inner, x: lens[0], @@ -133,29 +170,62 @@ impl Grow<'_, Rsc> { if depth == 0 { return self.leaf(); } - let count = 2 + self.rng.below(2); - let mut children = Vec::with_capacity(count); - for _ in 0..count { + if self.rng.below(4) == 0 { + let inner = self.node(depth - 1); + let inner = self.sized(inner); + // Each side its own, since a padding that is the same all round + // hides anything that treats one edge differently from another. + let mut side = || self.rng.below(24) as f32; + let padding = Padding { + left: side(), + right: side(), + top: side(), + bottom: side(), + }; + let id = Pad { padding, inner }.add_strong(self.rsc); + self.tree.ids.push(id.id()); + return id; + } + let grown = 2 + self.rng.below(3); + let mut children = Vec::with_capacity(grown); + for _ in 0..grown { let child = self.node(depth - 1); children.push(self.sized(child)); } - let id: StrongWidget = match self.rng.below(3) { - 0 => Stack { + if self.rng.chance() { + let id = Stack { children, size: StackSize::Child(0), } - .add_strong(self.rsc), - _ => { - let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)]; - Span { - children, - dir, - gap: self.rng.below(3) as f32 * 4.0, - } - .add_strong(self.rsc) + .add_strong(self.rsc); + self.tree.ids.push(id.id()); + return id; + } + // Grown either way, so the widget after them has the same id in a + // tree that leaves them out as in one that puts them in. + let mut spares: Vec = (0..SPARES).map(|_| self.leaf()).collect(); + let idx = self.tree.spans.len(); + let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default(); + // Highest first, so an index means the same child however many of its + // neighbours are going too. + let mut detach = edit.detach.clone(); + detach.sort_unstable(); + for j in detach.into_iter().rev() { + if j < children.len() { + self.tree.detached.push(children.remove(j)); } - }; + } + let attach = edit.attach.min(spares.len()); + children.extend(spares.drain(..attach)); + let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)]; + let id = Span { + children, + dir, + gap: self.rng.below(3) as f32 * 4.0, + } + .add(self.rsc); self.tree.ids.push(id.id()); - id + self.tree.spans.push(Spanned { id, spares, grown }); + id.add_strong(self.rsc) } } diff --git a/tests/generated.rs b/tests/generated.rs index 2dc3f90..6b665b2 100644 --- a/tests/generated.rs +++ b/tests/generated.rs @@ -14,12 +14,12 @@ use std::collections::HashMap; use iris::harness::Harness; use iris::prelude::*; -use iris::random::{Lens, Rng, Tree, grow}; +use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow}; const DEPTH: usize = 4; const SEEDS: [u64; 6] = [1, 2, 3, 5, 8, 13]; -fn plant(h: &mut Harness, seed: u64, edits: &HashMap) -> Tree { +fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree { let (root, tree) = grow(&mut h.rsc, seed, DEPTH, edits); h.state.root = Some(root); h.frame(); @@ -44,6 +44,89 @@ fn edit(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap { edits } +/// A way of changing what a span holds. Each is a shape worth its own case: +/// taking a child out of the middle is not the same as emptying a span, and +/// adding one is not the same as adding three. +#[derive(Clone, Copy, Debug)] +enum Shuffle { + /// Every other child, so what is left is interleaved with what went. + EveryOther, + /// Everything but the first, which is the last step before empty. + AllButFirst, + /// Three more on the end at once. + AddThree, + /// The first out and three more on, so the count moves both ways. + SwapForThree, + /// One out of the middle and one on the end. + TradeOne, +} + +const SHUFFLES: [Shuffle; 5] = [ + Shuffle::EveryOther, + Shuffle::AllButFirst, + Shuffle::AddThree, + Shuffle::SwapForThree, + Shuffle::TradeOne, +]; + +impl Shuffle { + fn of(self, grown: usize) -> SpanEdit { + let all = |step: usize, from: usize| (from..grown).step_by(step).collect(); + match self { + Self::EveryOther => SpanEdit { + detach: all(2, 0), + attach: 0, + }, + Self::AllButFirst => SpanEdit { + detach: all(1, 1), + attach: 0, + }, + Self::AddThree => SpanEdit { + detach: Vec::new(), + attach: 3, + }, + Self::SwapForThree => SpanEdit { + detach: vec![0], + attach: 3, + }, + Self::TradeOne => SpanEdit { + detach: vec![grown / 2], + attach: 1, + }, + } + } +} + +/// Applies `shuffle` to every third span, and says what it did so the cold +/// tree can be grown that way. The widgets it takes out are given back: the +/// last share of one must outlive the comparison, or its id is handed to +/// something else and the two trees stop lining up. +fn reshuffle( + h: &mut Harness, + tree: &mut Tree, + shuffle: Shuffle, +) -> (HashMap, Vec) { + let mut edits = HashMap::new(); + let mut detached = Vec::new(); + for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) { + let span_edit = shuffle.of(span.grown); + let mut take = span_edit.detach.clone(); + take.sort_unstable(); + let children = &mut h.rsc[span.id].children; + // Highest first, so an index means the same child however many of + // its neighbours are going too. + for j in take.into_iter().rev() { + if j < children.len() { + detached.push(children.remove(j)); + } + } + let attach = span_edit.attach.min(span.spares.len()); + children.extend(span.spares.drain(..attach)); + edits.insert(idx, span_edit); + } + (edits, detached) +} + /// Every widget in one tree against the matching widget in the other. A /// mismatch prints the widget's ancestry, marking the ones that own a slot, /// since where two trees disagree is rarely where the cause is. @@ -83,42 +166,83 @@ fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, fn changed_size(seed: u64) { let mut warm = Harness::new((900, 1200)); - let grown = plant(&mut warm, seed, &HashMap::new()); + let grown = plant(&mut warm, seed, &Edits::default()); let mut rng = Rng::new(seed ^ 0x5eed); - let edits = edit(&mut warm, &grown, &mut rng); + let sizes = edit(&mut warm, &grown, &mut rng); warm.frame(); let mut cold = Harness::new((900, 1200)); - let same = plant(&mut cold, seed, &edits); + let same = plant( + &mut cold, + seed, + &Edits { + sizes, + ..Default::default() + }, + ); assert_same(seed, "a size change", (&warm, &grown), (&cold, &same)); } +fn reshuffled(seed: u64, shuffle: Shuffle) { + let mut warm = Harness::new((900, 1200)); + let mut grown = plant(&mut warm, seed, &Edits::default()); + let before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect(); + + let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle); + warm.frame(); + + // Or the two trees would agree for want of anything having happened. + let after = grown.ids.iter().map(|id| warm.region(id)); + let moved = before.iter().zip(after).filter(|(a, b)| *a != b).count(); + assert!(moved > 0, "seed {seed}: {shuffle:?} changed nothing"); + + let mut cold = Harness::new((900, 1200)); + let same = plant( + &mut cold, + seed, + &Edits { + spans, + ..Default::default() + }, + ); + + let what = format!("{shuffle:?}"); + assert_same(seed, &what, (&warm, &grown), (&cold, &same)); +} + fn resized(seed: u64) { let mut warm = Harness::new((1920, 1200)); - let grown = plant(&mut warm, seed, &HashMap::new()); + let grown = plant(&mut warm, seed, &Edits::default()); warm.resize((640, 900)); warm.frame(); let mut cold = Harness::new((640, 900)); - let same = plant(&mut cold, seed, &HashMap::new()); + let same = plant(&mut cold, seed, &Edits::default()); assert_same(seed, "a resize", (&warm, &grown), (&cold, &same)); } fn resized_then_changed(seed: u64) { let mut warm = Harness::new((1920, 1200)); - let grown = plant(&mut warm, seed, &HashMap::new()); + let grown = plant(&mut warm, seed, &Edits::default()); warm.resize((640, 900)); warm.frame(); let mut rng = Rng::new(seed ^ 0xb0a7); - let edits = edit(&mut warm, &grown, &mut rng); + let sizes = edit(&mut warm, &grown, &mut rng); warm.frame(); let mut cold = Harness::new((640, 900)); - let same = plant(&mut cold, seed, &edits); + let same = plant( + &mut cold, + seed, + &Edits { + sizes, + ..Default::default() + }, + ); let what = "a resize then a size change"; assert_same(seed, what, (&warm, &grown), (&cold, &same)); @@ -139,6 +263,15 @@ fn a_size_change_after_a_resize_lands_the_same_way() { SEEDS.into_iter().for_each(resized_then_changed); } +#[test] +fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() { + for shuffle in SHUFFLES { + for seed in SEEDS { + reshuffled(seed, shuffle); + } + } +} + /// Reproduces a divergence that predates the position chain: laying a tree out /// again does not always land where growing it cold does. /// @@ -165,5 +298,8 @@ fn a_long_run_of_seeds_agrees() { changed_size(seed); resized(seed); resized_then_changed(seed); + for shuffle in SHUFFLES { + reshuffled(seed, shuffle); + } } }