diff --git a/src/random.rs b/src/random.rs index 98bcc91..41068de 100644 --- a/src/random.rs +++ b/src/random.rs @@ -101,10 +101,6 @@ pub struct Tree { pub nodes: Vec, pub spans: Vec, pub scrolls: 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, } /// Branches on a child's measured length. Comparing boxes catches a widget @@ -138,15 +134,463 @@ impl Widget for Branch { 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. + /// Everything made for this span that it does not hold -- spares never + /// attached and children detached alike. A widget belongs to one parent, + /// and one that belongs to nobody still has to be held here: dropping + /// the last share of it frees its id for the next widget to be given, + /// which puts two trees out of step. pub spares: Vec, /// How many children it was grown with, before any edit. pub grown: usize, } +/// A tree described rather than built: [`plan`] turns a seed into one of +/// these and [`build`] turns it into widgets, where growing did both at once. +/// +/// The split is what makes a counterexample readable. A failing seed used to +/// be the entire record of one, because a grower that makes widgets as it +/// draws leaves nothing to take apart -- a shrinker could only grow its own +/// trees and hope to meet the same shape, which in practice it does not. A +/// plan is reduced by [`Plan::smaller`] and built again, so any seed that +/// fails can be cut down until what is left is small enough to read. +#[derive(Clone, Debug, PartialEq)] +pub struct Plan { + pub kind: Kind, + /// The declared size this widget carries. Whoever grows a widget offers + /// it one and the offer is taken or declined; a second offer to the same + /// widget is dropped, because two rules on one widget would settle in the + /// order they were applied rather than in grow order. + pub size: Option, + /// The alignment it carries, under the same one-offer rule. + pub align: Option, + /// Whether it was offered a movable region of its own and what it + /// answered. `Some(false)` is an offer declined, which still uses up the + /// one offer, where `None` is an offer never made. + pub region_node: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Kind { + /// Wrapped and unwrapped text, because only one of them reads the width + /// it is given and so only one has to be drawn again for a new one. + Wrapped, + OneLine, + Rect { + color: usize, + alpha: u8, + }, + /// Scrolling reads the pixel length of its box, which nothing else here + /// does, and gives its child a box longer than its own. + Scroll { + axis: Axis, + inner: Box, + }, + /// All three sides are grown either way, so a tree that draws one has the + /// same ids as a tree that draws another. + Branch { + probe: Box, + wide: Box, + narrow: Box, + threshold: f32, + }, + /// Each side its own, since a padding that is the same all round hides + /// anything that treats one edge differently from another. + Pad { + padding: [i32; 4], + inner: Box, + }, + Stack { + children: Vec, + }, + Span { + dir: usize, + gap: i32, + /// Grown for this span, in the order they are made. + children: Vec, + /// Grown beside it whether or not they end up in it, so the widget + /// after them has the same id in a tree that leaves them out as in + /// one that puts them in. + spares: Vec, + /// Which of `children` then `spares` are actually in the span, and + /// in what order -- kept apart from the two lists above so that a + /// tree which detaches, attaches or reorders its children still + /// makes the same widgets in the same order, and two builds line up + /// index for index. Anything not named here is built and held + /// rather than dropped, since freeing an id hands it to the next + /// widget and puts two trees out of step. + order: Vec, + }, +} + +impl Plan { + /// A widget carrying nothing anybody has offered it yet. + fn bare(kind: Kind) -> Self { + Self { + kind, + size: None, + align: None, + region_node: None, + } + } + + /// How many widgets building it makes, spares and detached children + /// included, since those are made either way. + pub fn size(&self) -> usize { + 1 + match &self.kind { + Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.size(), + Kind::Branch { + probe, + wide, + narrow, + .. + } => probe.size() + wide.size() + narrow.size(), + Kind::Stack { children } => children.iter().map(Plan::size).sum(), + Kind::Span { + children, spares, .. + } => children.iter().chain(spares).map(Plan::size).sum(), + _ => 0, + } + } + + /// The trees to try instead of this one when reducing a counterexample, + /// biggest cut first: a shrinker takes the first that still fails, so + /// offering "this subtree alone" before "this subtree with one child + /// fewer" is what gets from six hundred widgets to six rather than to + /// five hundred and ninety. + /// + /// Every one of these is a tree the generator could have grown, so a + /// reduced plan is a counterexample in its own right rather than a + /// special case only the shrinker can make. + pub fn smaller(&self) -> Vec { + let mut out = Vec::new(); + // Standing in for the whole of it, which is the largest cut there is. + for kid in self.kids() { + out.push(kid.clone()); + } + // Then what it carries, which costs nothing to put back if it was + // not the thing that mattered. + for dropped in [ + self.region_node.map(|_| Plan { + region_node: None, + ..self.clone() + }), + self.align.map(|_| Plan { + align: None, + ..self.clone() + }), + self.size.map(|_| Plan { + size: None, + ..self.clone() + }), + ] + .into_iter() + .flatten() + { + out.push(dropped); + } + out.extend(self.kind.smaller().into_iter().map(|kind| Plan { + kind, + ..self.clone() + })); + out + } + + /// Visits every widget in the order [`build`] makes them, so a count + /// kept by the visitor indexes the same widget as the matching [`Tree`] + /// vector does. + pub fn walk_mut(&mut self, at: &mut impl FnMut(&mut Plan)) { + match &mut self.kind { + Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.walk_mut(at), + Kind::Branch { + probe, + wide, + narrow, + .. + } => { + probe.walk_mut(at); + wide.walk_mut(at); + narrow.walk_mut(at); + } + Kind::Stack { children } => { + for child in children { + child.walk_mut(at); + } + } + Kind::Span { + children, spares, .. + } => { + for child in children.iter_mut().chain(spares) { + child.walk_mut(at); + } + } + _ => {} + } + at(self); + } + + /// The same tree with `edits` applied, by the indices the generator would + /// have used for them. + /// + /// [`plan`] resolves edits while drawing, which needs a seed. A scenario + /// needs them applied to a tree that already exists -- one it has built, + /// and one a shrinker may already have cut down, where no seed grows it + /// any more. Both routes take the same [`Edits`], so a case written + /// against one reads the same against the other. + pub fn edited(&self, edits: &Edits) -> Plan { + let mut out = self.clone(); + let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0); + out.walk_mut(&mut |plan| { + if let Kind::Span { + children, + spares, + order, + .. + } = &mut plan.kind + { + if let Some(edit) = edits.spans.get(&spans) { + *order = span_edited(order, children.len(), spares.len(), edit); + } + spans += 1; + } + if let Kind::Branch { threshold, .. } = &mut plan.kind + && edits.fixed_branches + { + *threshold = f32::MIN; + } + if plan.size.is_some() { + if let Some(lens) = edits.sizes.get(&sized) { + plan.size = Some(*lens); + } + sized += 1; + } + if plan.align.is_some() { + if let Some(align) = edits.aligns.get(&aligned) { + plan.align = Some(*align); + } + aligned += 1; + } + if plan.region_node.is_some() { + if let Some(take) = edits.nodes.get(&nodes) { + plan.region_node = Some(*take); + } + nodes += 1; + } + }); + out + } + + fn kids(&self) -> Vec<&Plan> { + match &self.kind { + Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => vec![inner], + Kind::Branch { + probe, + wide, + narrow, + .. + } => vec![probe, wide, narrow], + Kind::Stack { children } => children.iter().collect(), + Kind::Span { children, .. } => children.iter().collect(), + _ => Vec::new(), + } + } +} + +impl Kind { + /// Simplifications of the shape alone, leaving what the widget carries to + /// [`Plan::smaller`]. Replacing a node with one of its children is there + /// rather than here, since it answers with a whole `Plan`. + fn smaller(&self) -> Vec { + let mut out = Vec::new(); + /// One child reduced at a time, rebuilt into the same shape. Every + /// answer has the same number of children as it was given, so it is + /// for the shapes whose child count is part of what they are. + fn reduced(kids: &[Plan], rebuild: &dyn Fn(Vec) -> Kind) -> Vec { + let mut out = Vec::new(); + for (i, kid) in kids.iter().enumerate() { + for small in kid.smaller() { + let mut next = kids.to_vec(); + next[i] = small; + out.push(rebuild(next)); + } + } + out + } + + /// One child dropped, then [`reduced`]. For the shapes that hold any + /// number of children, where dropping one is the cut that matters. + fn each(kids: &[Plan], rebuild: &dyn Fn(Vec) -> Kind) -> Vec { + let mut out = Vec::new(); + for i in 0..kids.len() { + if kids.len() > 1 { + let mut less = kids.to_vec(); + less.remove(i); + out.push(rebuild(less)); + } + } + out.extend(reduced(kids, rebuild)); + out + } + match self { + // The one leaf that reads the width it is given, then the one + // that does not, then the one that measures nothing at all. + Kind::Wrapped => out.push(Kind::OneLine), + Kind::OneLine => out.push(Kind::Rect { + color: 0, + alpha: 255, + }), + Kind::Rect { .. } => {} + Kind::Scroll { axis, inner } => { + let axis = *axis; + out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Scroll { + axis, + inner: Box::new(k.remove(0)), + })); + } + Kind::Branch { + probe, + wide, + narrow, + threshold, + } => { + let threshold = *threshold; + // All three sides stay: a branch is the widget that draws + // one of two on a measurement, and one with a side missing + // is a different widget rather than a smaller one. Dropping + // the branch for a side is offered by `Plan::smaller`. + let sides = [(**probe).clone(), (**wide).clone(), (**narrow).clone()]; + out.extend(reduced(&sides, &|k| Kind::Branch { + probe: Box::new(k[0].clone()), + wide: Box::new(k[1].clone()), + narrow: Box::new(k[2].clone()), + threshold, + })); + } + Kind::Pad { padding, inner } => { + let padding = *padding; + if padding != [0; 4] { + out.push(Kind::Pad { + padding: [0; 4], + inner: inner.clone(), + }); + } + out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Pad { + padding, + inner: Box::new(k.remove(0)), + })); + } + Kind::Stack { children } => { + out.extend(each(children, &|children| Kind::Stack { children })) + } + Kind::Span { + dir, + gap, + children, + spares, + order, + } => { + let (dir, gap, n) = (*dir, *gap, children.len()); + let span = |children: Vec, spares: Vec, order: Vec| Kind::Span { + dir, + gap, + children, + spares, + order, + }; + let identity: Vec = (0..n).collect(); + // An order the generator did not choose is part of the tree, + // so take that off before taking the tree apart. + if *order != identity { + out.push(span(children.clone(), spares.clone(), identity)); + } + // Spares exist to be attached; with none attached they are + // widgets the span never holds. + if !spares.is_empty() && order.iter().all(|&i| i < n) { + out.push(span(children.clone(), Vec::new(), order.clone())); + } + if gap != 0 { + out.push(Kind::Span { + dir, + gap: 0, + children: children.clone(), + spares: spares.clone(), + order: order.clone(), + }); + } + for k in 0..n { + if n > 1 { + let mut less = children.clone(); + less.remove(k); + // Everything after it shifts down, spares included, + // since they are indexed past the children. + let order = order + .iter() + .filter(|&&i| i != k) + .map(|&i| if i > k { i - 1 } else { i }) + .collect(); + out.push(span(less, spares.clone(), order)); + } + } + for (i, kid) in children.iter().enumerate() { + for small in kid.smaller() { + let mut next = children.clone(); + next[i] = small; + out.push(span(next, spares.clone(), order.clone())); + } + } + } + } + out + } +} + +/// A [`SpanEdit`] applied to the order a span already holds its children in. +/// +/// `detach` names positions in that order and `attach` takes from the front +/// of what the span is not holding, both of which is what a test changing a +/// live span does -- so an edit means the same thing said to a tree and said +/// to the plan it was built from. On a span nobody has edited the order is +/// the children in the order they were grown, and this is then "leave these +/// out and put that many spares on the end". +fn span_edited(order: &[usize], children: usize, spares: usize, edit: &SpanEdit) -> Vec { + let mut detach = edit.detach.clone(); + detach.sort_unstable(); + detach.dedup(); + let mut next: Vec = order + .iter() + .enumerate() + .filter(|(at, _)| !detach.contains(at)) + .map(|(_, &which)| which) + .collect(); + // What the span is not holding, in the order it hands them back: what it + // was already not holding first, in the order the widgets were made, and + // what this edit takes out after that, highest position first. A child + // just detached goes to the back rather than straight back in, which is + // what makes detaching one and attaching one a trade. + let mut free: Vec = (0..children + spares) + .filter(|i| !order.contains(i)) + .collect(); + free.extend(detach.iter().rev().filter_map(|&at| order.get(at).copied())); + next.extend(free.into_iter().take(edit.attach)); + next +} + +/// Plans the tree `seed` describes, `edits` replacing what it would otherwise +/// have given the widgets that carry them. +/// +/// The edits are resolved here rather than at build time, so that a plan is +/// the whole of what a tree is and building one has nothing left to decide. +pub fn plan(seed: u64, depth: usize, edits: &Edits) -> Plan { + let mut sow = Sow { + rng: Rng::new(seed), + edits, + sized: 0, + aligned: 0, + nodes: 0, + spans: 0, + }; + sow.node(depth) +} + /// Grows the tree `seed` describes, `edits` replacing the declared sizes it /// would otherwise have given those wrappers. pub fn grow( @@ -155,41 +599,32 @@ pub fn grow( depth: usize, edits: &Edits, ) -> (StrongWidget, Tree) { - let mut grow = Grow { - rsc, - rng: Rng::new(seed), - tree: Tree::default(), - edits, - }; - let root = grow.node(depth); - (root, grow.tree) + build(rsc, &plan(seed, depth, edits)) } -struct Grow<'a, Rsc> { - rsc: &'a mut Rsc, +/// Draws a plan out of the random stream. Every draw happens in the order it +/// always has and before the decision it feeds, including the decisions that +/// are then dropped, because a seed has to keep meaning the same tree. +struct Sow<'a> { rng: Rng, - tree: Tree, edits: &'a Edits, + sized: usize, + aligned: usize, + nodes: usize, + spans: usize, } -impl Grow<'_, Rsc> { - fn leaf(&mut self) -> StrongWidget { - let id: StrongWidget = match self.rng.below(4) { - // Wrapped and unwrapped, because only one of them reads the width - // it is given and so only one has to be drawn again for a new one. - 0 => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc), - 1 => wtext("one line, overflowing whatever it is given") - .size(16) - .wrap(false) - .add_strong(self.rsc), +impl Sow<'_> { + fn leaf(&mut self) -> Plan { + Plan::bare(match self.rng.below(4) { + 0 => Kind::Wrapped, + 1 => Kind::OneLine, _ => { - let color = COLORS[self.rng.below(COLORS.len())]; + let color = self.rng.below(COLORS.len()); let alpha = (self.rng.below(5) * 63) as u8; - rect(color.alpha(alpha)).add_strong(self.rsc) + Kind::Rect { color, alpha } } - }; - self.tree.ids.push(id.id()); - id + }) } fn len(&mut self) -> Option { @@ -200,100 +635,77 @@ impl Grow<'_, Rsc> { } } - fn align(&mut self) -> Align { - let mut axis = || match self.rng.below(4) { + fn align(&mut self) -> Aligns { + let axis = |s: &mut Self| match s.rng.below(4) { 0 => None, 1 => Some(AxisAlign::NEG), 2 => Some(AxisAlign::CENTER), _ => Some(AxisAlign::POS), }; - let (mut x, y) = (axis(), axis()); + let (x, y) = (axis(self), axis(self)); // Aligning on neither axis leaves the branch unexercised. - if x.is_none() && y.is_none() { - x = Some(AxisAlign::CENTER); + match x.is_none() && y.is_none() { + true => [Some(AxisAlign::CENTER), y], + false => [x, y], } - Align { x, y } } /// A declared size over half the tree, kept where a test can change it. - fn sized(&mut self, inner: StrongWidget) -> StrongWidget { - // A rule is a property now, so a node already carrying one would take - // a second entry in `sized` -- and two edits naming one widget settle - // in the order they are applied, which is grow order cold and edit - // order warm. One entry per widget instead. Both draws are taken - // whatever is decided, and the decision is grow order alone, so the - // two trees consume the same random stream. + fn sized(&mut self, inner: &mut Plan) { let take = self.rng.chance(); let lens = [self.len(), self.len()]; - if !take || self.tree.sized.contains(&inner.id()) { - return inner; + if !take || inner.size.is_some() { + return; } - let idx = self.tree.sized.len(); - let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens); - let id = inner.id(); - self.rsc - .ui_mut() - .widgets - .set_size_rules(id, lens[0], lens[1]); - self.tree.sized.push(id); - inner + let idx = self.sized; + self.sized += 1; + inner.size = Some(self.edits.sizes.get(&idx).copied().unwrap_or(lens)); } /// An alignment over some of the tree, kept where a test can change it. - /// One entry per widget for the reason `sized` gives. - fn aligned(&mut self, inner: StrongWidget) -> StrongWidget { + fn aligned(&mut self, inner: &mut Plan) { let align = self.align(); - let align = [align.x, align.y]; - if self.tree.aligned.contains(&inner.id()) { - return inner; + if inner.align.is_some() { + return; } - let idx = self.tree.aligned.len(); - let align = self.edits.aligns.get(&idx).copied().unwrap_or(align); - let id = inner.id(); - let widgets = &mut self.rsc.ui_mut().widgets; - for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) { - widgets.set_alignment(id, axis, align.unwrap_or_default()); - } - self.tree.aligned.push(id); - inner + let idx = self.aligned; + self.aligned += 1; + inner.align = Some(self.edits.aligns.get(&idx).copied().unwrap_or(align)); } /// A movable region of its own over some of the tree. What it changes is /// how a move is written and how long a primitive's chain is, neither of /// which any other branch here varies. - fn noded(&mut self, inner: StrongWidget) -> StrongWidget { + fn noded(&mut self, inner: &mut Plan) { let take = self.rng.below(4) == 0; - if self.tree.nodes.contains(&inner.id()) { - return inner; + if inner.region_node.is_some() { + return; } - let idx = self.tree.nodes.len(); - let take = self.edits.nodes.get(&idx).copied().unwrap_or(take); - let id = inner.id(); - self.rsc.ui_mut().widgets.set_region_node(id, take); - self.tree.nodes.push(id); - inner + let idx = self.nodes; + self.nodes += 1; + inner.region_node = Some(self.edits.nodes.get(&idx).copied().unwrap_or(take)); } - fn node(&mut self, depth: usize) -> StrongWidget { + fn offered(&mut self, inner: &mut Plan) { + self.sized(inner); + self.noded(inner); + } + + fn node(&mut self, depth: usize) -> Plan { if depth == 0 { return self.leaf(); } let positioned = self.rng.below(6); if positioned == 0 { - // Scrolling reads the pixel length of its box, which nothing - // else here does, and gives its child a box longer than its own. - let inner = self.node(depth - 1); - let inner = self.sized(inner); - let inner = self.noded(inner); + let mut inner = self.node(depth - 1); + self.offered(&mut inner); let axis = if self.rng.chance() { Axis::X } else { Axis::Y }; - let id = Scroll::new(inner, axis).add(self.rsc); - self.tree.scrolls.push(id); - self.tree.ids.push(id.id()); - return id.add_strong(self.rsc); + return Plan::bare(Kind::Scroll { + axis, + inner: Box::new(inner), + }); } if positioned == 2 { - // Both sides are grown either way, so a tree that draws one has - // the same ids as a tree that draws the other. let probe = self.node(depth - 1); let wide = self.node(depth - 1); let narrow = self.node(depth - 1); @@ -305,90 +717,199 @@ impl Grow<'_, Rsc> { true => f32::MIN, false => measured, }; - let id = Branch { - probe, - wide, - narrow, + return Plan::bare(Kind::Branch { + probe: Box::new(probe), + wide: Box::new(wide), + narrow: Box::new(narrow), threshold, - } - .add(self.rsc); - self.tree.ids.push(id.id()); - return id.add_strong(self.rsc); + }); } if positioned == 1 { - let inner = self.node(depth - 1); - let inner = self.sized(inner); - let inner = self.noded(inner); - return self.aligned(inner); + // Carries an alignment and makes no widget of its own, so the + // plan for it is the child it aligned. + let mut inner = self.node(depth - 1); + self.offered(&mut inner); + self.aligned(&mut inner); + return inner; } if self.rng.below(4) == 0 { - let inner = self.node(depth - 1); - let inner = self.sized(inner); - let inner = self.noded(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 = || Px::from_int(self.rng.below(24) as i32); - 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 mut inner = self.node(depth - 1); + self.offered(&mut inner); + let side = |s: &mut Self| s.rng.below(24) as i32; + let padding = [side(self), side(self), side(self), side(self)]; + return Plan::bare(Kind::Pad { + padding, + inner: Box::new(inner), + }); } let grown = 2 + self.rng.below(3); let mut children = Vec::with_capacity(grown); for _ in 0..grown { - let child = self.node(depth - 1); - let child = self.sized(child); - let child = self.noded(child); + let mut child = self.node(depth - 1); + self.offered(&mut child); children.push(child); } if self.rng.chance() { - let id = Stack { - children, - size: StackSize::Child(0), - } - .add_strong(self.rsc); - self.tree.ids.push(id.id()); - return id; + return Plan::bare(Kind::Stack { children }); } - // 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 spares: Vec = (0..SPARES).map(|_| self.leaf()).collect(); + let idx = self.spans; + self.spans += 1; 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: Px::from_int(self.rng.below(3) as i32 * 4), - } - .add(self.rsc); + let dir = self.rng.below(4); // A row takes the height it is given rather than its tallest child, // which is a rule beside it. Derived from an existing choice and // consuming no randomness: a seed must keep growing the same tree // when the generator gains another configuration. - if dir.axis == Axis::X { + let gap = self.rng.below(3) as i32 * 4; + let grown: Vec = (0..children.len()).collect(); + let order = span_edited(&grown, children.len(), spares.len(), &edit); + Plan::bare(Kind::Span { + dir, + gap, + children, + spares, + order, + }) + } +} + +/// Builds a plan's widgets in the order it describes them, so two builds of +/// one plan line up index for index and their boxes can be compared. +pub fn build(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget, Tree) { + let mut build = Build { + rsc, + tree: Tree::default(), + }; + let root = build.node(plan); + (root, build.tree) +} + +struct Build<'a, Rsc> { + rsc: &'a mut Rsc, + tree: Tree, +} + +impl Build<'_, Rsc> { + fn node(&mut self, plan: &Plan) -> StrongWidget { + let built = self.kind(&plan.kind); + let id = built.id(); + if let Some(lens) = plan.size { self.rsc - .widgets_mut() - .set_size_rules(id, None, Some(LayoutLen::rel(1.0))); + .ui_mut() + .widgets + .set_size_rules(id, lens[0], lens[1]); + self.tree.sized.push(id); } + if let Some(align) = plan.align { + let widgets = &mut self.rsc.ui_mut().widgets; + for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) { + widgets.set_alignment(id, axis, align.unwrap_or_default()); + } + self.tree.aligned.push(id); + } + if let Some(take) = plan.region_node { + self.rsc.ui_mut().widgets.set_region_node(id, take); + self.tree.nodes.push(id); + } + built + } + + fn kind(&mut self, kind: &Kind) -> StrongWidget { + let id: StrongWidget = match kind { + Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc), + Kind::OneLine => wtext("one line, overflowing whatever it is given") + .size(16) + .wrap(false) + .add_strong(self.rsc), + Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc), + Kind::Scroll { axis, inner } => { + let inner = self.node(inner); + let id = Scroll::new(inner, *axis).add(self.rsc); + self.tree.scrolls.push(id); + self.tree.ids.push(id.id()); + return id.add_strong(self.rsc); + } + Kind::Branch { + probe, + wide, + narrow, + threshold, + } => { + let probe = self.node(probe); + let wide = self.node(wide); + let narrow = self.node(narrow); + let id = Branch { + probe, + wide, + narrow, + threshold: *threshold, + } + .add(self.rsc); + self.tree.ids.push(id.id()); + return id.add_strong(self.rsc); + } + Kind::Pad { padding, inner } => { + let inner = self.node(inner); + let [left, right, top, bottom] = padding.map(Px::from_int); + let padding = Padding { + left, + right, + top, + bottom, + }; + Pad { padding, inner }.add_strong(self.rsc) + } + Kind::Stack { children } => { + let children = children.iter().map(|c| self.node(c)).collect(); + Stack { + children, + size: StackSize::Child(0), + } + .add_strong(self.rsc) + } + Kind::Span { + dir, + gap, + children, + spares, + order, + } => { + let grown = children.len(); + // Every one of them is made, in this order, whether or not + // the span ends up holding it. + let made: Vec = children + .iter() + .chain(spares) + .map(|c| self.node(c)) + .collect(); + let mut left: Vec> = made.into_iter().map(Some).collect(); + let children: Vec = order + .iter() + .filter_map(|&i| left.get_mut(i).and_then(Option::take)) + .collect(); + // What the span does not hold is still held here: dropping + // the last share of a widget frees its id for the next one + // to be given, which puts two trees out of step. + let spares: Vec = left.into_iter().flatten().collect(); + let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][*dir % 4]; + let id = Span { + children, + dir, + gap: Px::from_int(*gap), + } + .add(self.rsc); + if dir.axis == Axis::X { + self.rsc + .widgets_mut() + .set_size_rules(id, None, Some(LayoutLen::rel(1.0))); + } + self.tree.ids.push(id.id()); + self.tree.spans.push(Spanned { id, spares, grown }); + return id.add_strong(self.rsc); + } + }; self.tree.ids.push(id.id()); - self.tree.spans.push(Spanned { id, spares, grown }); - id.add_strong(self.rsc) + id } } diff --git a/tests/cases/plan.rs b/tests/cases/plan.rs new file mode 100644 index 0000000..a9baab5 --- /dev/null +++ b/tests/cases/plan.rs @@ -0,0 +1,121 @@ +//! The tree a seed describes, as a value rather than as widgets. +//! +//! Two things have to hold for a plan to be worth having. Editing a plan has +//! to mean what growing with those edits means, or a scenario reads one thing +//! and the oracle another. And reducing a plan has to end, or a shrinker +//! searching for the smallest counterexample never returns. + +use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, plan}; +use std::collections::HashMap; + +fn some_edits(seed: u64, of: &Plan) -> Edits { + let mut rng = Rng::new(seed); + let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0); + let mut of = of.clone(); + of.walk_mut(&mut |p| { + if matches!(p.kind, Kind::Span { .. }) { + spans += 1; + } + sized += p.size.is_some() as usize; + aligned += p.align.is_some() as usize; + nodes += p.region_node.is_some() as usize; + }); + let pick = + |n: usize, rng: &mut Rng| -> Vec { (0..n).filter(|_| rng.chance()).collect() }; + Edits { + sizes: pick(sized, &mut rng) + .into_iter() + .map(|i| (i, [Some(LayoutLen::LEFTOVER), None])) + .collect(), + aligns: pick(aligned, &mut rng) + .into_iter() + .map(|i| (i, [Some(AxisAlign::POS), None])) + .collect(), + nodes: pick(nodes, &mut rng) + .into_iter() + .map(|i| (i, true)) + .collect(), + spans: pick(spans, &mut rng) + .into_iter() + .map(|i| { + ( + i, + SpanEdit { + detach: vec![0], + attach: 2, + }, + ) + }) + .collect::>(), + fixed_branches: false, + } +} + +use iris::prelude::*; + +/// The two routes to an edited tree are one tree. `plan` resolves edits out +/// of the random stream as it draws; `edited` puts them on a tree that +/// already exists, which is the only route a shrunk plan has, since no seed +/// grows one. A scenario written against either has to read the same. +#[test] +fn editing_a_plan_is_growing_one_with_those_edits() { + for seed in 1..=60 { + let bare = plan(seed, 5, &Edits::default()); + let edits = some_edits(seed, &bare); + assert_eq!( + bare.edited(&edits), + plan(seed, 5, &edits), + "seed {seed}: edited and grown-with-edits disagree" + ); + } +} + +/// Every simplification is strictly smaller, so taking them in turn reaches a +/// fixed point instead of circling. A shrinker that can return to a tree it +/// has already tried does not stop. +#[test] +fn every_simplification_of_a_plan_is_smaller_than_it() { + for seed in 1..=60 { + let tree = plan(seed, 4, &Edits::default()); + let mut queue = vec![tree]; + let mut seen = 0; + while let Some(node) = queue.pop() { + seen += 1; + if seen > 400 { + break; + } + for small in node.smaller() { + assert!( + small.size() <= node.size(), + "seed {seed}: a simplification grew from {} to {}", + node.size(), + small.size() + ); + if small.size() < node.size() { + queue.push(small); + } + } + } + } +} + +/// Reducing until nothing reduces ends, and ends at something small enough to +/// read rather than at the tree it started from. +#[test] +fn reducing_a_plan_all_the_way_ends() { + for seed in 1..=30 { + let mut node = plan(seed, 5, &Edits::default()); + let grown = node.size(); + let mut steps = 0; + while let Some(next) = node.smaller().into_iter().next() { + node = next; + steps += 1; + assert!(steps < 10_000, "seed {seed}: reducing did not end"); + } + assert!( + node.size() < grown.max(2), + "seed {seed}: reduced {grown} widgets to {}", + node.size() + ); + } +} diff --git a/tests/cases/unsettled.rs b/tests/cases/unsettled.rs index 926a58a..b972206 100644 --- a/tests/cases/unsettled.rs +++ b/tests/cases/unsettled.rs @@ -398,3 +398,59 @@ fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() { } assert!(wrong.is_empty(), "{}", wrong.join("\n")); } + +/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed +/// 18 grows at depth 6. A scroll inside a scroll, the inner one owning a +/// movable region of its own, and only the text at the bottom marked for +/// redraw. Nothing about the tree changes, so no box may -- and the span +/// lands 76px further down the outer scroll warm than it does cold. +fn plant_nested_scrolls(h: &mut Harness) -> Vec { + let text = wtext("one line, overflowing whatever it is given") + .size(16) + .wrap(false) + .add(&mut h.rsc); + let inner = Scroll::new(text.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc); + h.rsc.widgets_mut().set_region_node(inner.id(), true); + let filler = rect(Color::RED).add(&mut h.rsc); + h.rsc.widgets_mut().set_size_rules( + filler.id(), + Some(LayoutLen::px(87.0)), + Some(LayoutLen::px(24.0)), + ); + let span = Span { + children: vec![inner.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)], + dir: Dir::DOWN, + gap: Px::ZERO, + } + .add(&mut h.rsc); + let root = Scroll::new(span.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc); + h.set_root(root); + vec![text.id(), inner.id(), filler.id(), span.id(), root.id()] +} + +/// **A known defect, not a passing test.** Bisected to `95fb4f9`, which made +/// `Masked` report its box rather than its inner's size: `Scroll` clips +/// through one, so what the outer scroll is told its content measures now +/// depends on whether the inner subtree was redrawn this frame. Warm the +/// span sits at the top of the outer scroll and cold it sits 24px higher, +/// which is exactly the sized child's height. Un-ignore it with the fix. +#[test] +#[ignore = "known defect: a partial repaint moves a scrolled span, from 95fb4f9"] +fn redrawing_one_widget_does_not_move_what_scrolls_around_it() { + let mut warm = Harness::new((900, 1200)); + let ids = plant_nested_scrolls(&mut warm); + warm.rsc.widgets_mut().get_dyn_mut(ids[0]); + warm.frame(); + + let mut cold = Harness::new((900, 1200)); + let cold_ids = plant_nested_scrolls(&mut cold); + + let mut wrong = Vec::new(); + for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() { + let (got, want) = (warm.region(&w), cold.region(&c)); + if got != want { + wrong.push(format!("widget {i}: warm {got:?} cold {want:?}")); + } + } + assert!(wrong.is_empty(), "{}", wrong.join("\n")); +} diff --git a/tests/generated.rs b/tests/generated.rs index 934f133..47159cd 100644 --- a/tests/generated.rs +++ b/tests/generated.rs @@ -1,19 +1,21 @@ -//! Random trees, checked against building the same tree cold. +//! Laying a tree out again has to land where growing it that way would. //! -//! A frame reaches its layout by keeping most of the last one: movable regions -//! or primitive boxes rewritten, some widgets drawn again, the rest untouched. -//! The result must be the tree a cold start would have produced, so anything -//! wrongly retained shows up as a difference in somebody's box. +//! Every case is one of `scenario`'s, over the trees `iris::random` grows +//! from a seed. The fast test takes a handful of seeds and the ignored one +//! takes as many as it is asked for; both run the same cases the shrinker +//! does over the same trees, so a seed that fails here is reduced by //! -//! `iris::random` grows the tree and `examples/random.rs` draws one. A seed is -//! the whole reproduction; `a_long_run_of_seeds_agrees` is the ignored sweep -//! for when it is worth spending the time. +//! SHRINK_SEED= SHRINK_DEPTH= SHRINK_CASE= \ +//! cargo test --release --test shrink -- --ignored --nocapture +//! +//! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH` +//! select what the long run covers. -use std::collections::HashMap; +#[path = "scenario/mod.rs"] +mod scenario; -use iris::harness::Harness; -use iris::prelude::*; -use iris::random::{Aligns, Edits, Lens, Rng, SpanEdit, Tree, grow}; +use iris::random::{Edits, plan}; +use scenario::{ALL, Case, diverges, env, over_seeds}; /// How deep the generator branches. The generator widens two to four ways per /// level, so depth is exponential in width and a deep narrow tree is not @@ -23,562 +25,100 @@ fn depth() -> usize { env("IRIS_GENERATED_DEPTH", 4) } -fn env(name: &str, fallback: T) -> T { - std::env::var(name) - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(fallback) -} +/// The seeds the ordinary tests take. Eight that have never failed and one, +/// 86, that a `Scroll` fixed point once settled differently on. const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98]; -/// The same box, to a step of the grid per level of nesting between the two -/// ways of reaching it. A move, a repaint and a row of shares land on the -/// same number now; what is left is a box centred in a fraction of its parent -/// against the same box centred in its own pixels. A step is a thousandth of -/// a pixel, where this was a twentieth of one before any of it was on a grid. -const AGREE_STEPS: i32 = 2; - -fn same_region(got: Option, want: Option) -> bool { - match (got, want) { - (Some(got), Some(want)) => { - let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS); - same(got.top_left.x, want.top_left.x) - && same(got.top_left.y, want.top_left.y) - && same(got.bot_right.x, want.bot_right.x) - && same(got.bot_right.y, want.bot_right.y) - } - (None, None) => true, - _ => false, - } -} - -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(); - tree -} - -fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens { - let lens = [ - Some(LayoutLen::px(20.0 + rng.below(180) as f32)), - Some(LayoutLen::px(20.0 + rng.below(180) as f32)), - ]; - h.rsc - .widgets_mut() - .set_size_rules(tree.sized[idx], lens[0], lens[1]); - lens -} - -/// Changes a few of the declared sizes, and says which, so the cold tree can -/// be grown with the same ones. -fn edit(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap { - let mut edits = HashMap::new(); - for _ in 0..4 { - let idx = rng.below(tree.sized.len()); - edits.insert(idx, resize_one(h, tree, idx, rng)); - } - edits -} - -/// Every declared size at once, so every reader of a size in the tree has a -/// changed descendant in the same frame and the whole dirty set has to settle -/// together. -fn edit_every(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap { - (0..tree.sized.len()) - .map(|idx| (idx, resize_one(h, tree, idx, rng))) - .collect() -} - -/// 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) -} - -/// What a widget was configured with, so a tree the generator found can be -/// written out by hand. A fuzz failure is a lead; the fast test that replaces -/// it has to be buildable from what the failure printed. -fn describe(id: WidgetId, h: &Harness) -> String { - let rules = h.rsc.widgets().size_rules(id); - let rule = |r: SizeRule| match r.exact() { - Some(len) => format!("{len}"), - None => "-".into(), - }; - let align = h.rsc.widgets().alignment(id); - let side = |a: AxisAlign| { - if a == AxisAlign::NEG { - "neg".into() - } else if a == AxisAlign::CENTER { - "mid".into() - } else if a == AxisAlign::POS { - "pos".into() - } else { - format!("{:.2}", a.rel()) - } - }; - // A rule and an alignment are properties of whatever carries them, so - // they print with that widget rather than as widgets of their own. - let mut out = describe_widget(id, h); - if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) { - out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y)); - } - if align != RegionAlign::default() { - out += &format!("@{},{}", side(align.x), side(align.y)); - } - out -} - -fn describe_widget(id: WidgetId, h: &Harness) -> String { - let label = h.rsc.widgets().label(id).to_string(); - let Some(widget) = h.rsc.widgets().get_dyn(id) else { - return label; - }; - let any: &dyn std::any::Any = widget; - if let Some(w) = any.downcast_ref::() { - let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" }; - return format!( - "Span{{dir:{:?}{sign},gap:{},n:{}}}", - w.dir.axis, - w.gap, - w.children.len() +fn check(seed: u64, depth: usize, case: Case) { + let grown = plan(seed, depth, &Edits::default()); + if let Some(how) = diverges(&grown, case, seed) { + panic!( + "seed {seed} at depth {depth} differs after {}: {how}\n\ + reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \ + SHRINK_CASE={} cargo test --release --test shrink -- --ignored --nocapture", + case.name(), + case.name(), ); } - if let Some(w) = any.downcast_ref::() { - let p = &w.padding; - return format!( - "Pad{{l:{},r:{},t:{},b:{}}}", - p.left, p.right, p.top, p.bottom - ); - } - if let Some(w) = any.downcast_ref::() { - return format!("Stack{{n:{}}}", w.children.len()); - } - label } -/// Every widget in one tree against the matching widget in the other. A -/// mismatch prints the widget's ancestry, marking region nodes, since where -/// two trees disagree is rarely where the cause is. -fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, &Tree)) { - let ((wh, wt), (ch, ct)) = (warm, cold); - assert_eq!(wt.ids.len(), ct.ids.len(), "seed {seed}: different trees"); - let mut drawn = 0; - let mut wrong = 0; - for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() { - let (got, want) = (wh.region(&w), ch.region(&c)); - drawn += usize::from(got.is_some()); - // This oracle cares where rasterization lands, not whether equivalent - // arithmetic produced the same f32. Keep the tolerance to one - // twentieth of a physical pixel, while whether a widget drew remains - // exact. - if same_region(got, want) { - continue; - } - wrong += 1; - if wrong <= 3 { - let mut chain = Vec::new(); - let mut at = Some(w); - while let Some(id) = at { - let active = &wh.render.active[&id]; - let node = match active.move_idx == active.parent_move { - true => "", - false => "*", - }; - chain.push(format!("{}{node}", describe(id, wh))); - at = active.parent; +macro_rules! case { + ($name:ident, $case:expr) => { + #[test] + fn $name() { + for seed in SEEDS { + check(seed, depth(), $case); } - println!( - "seed {seed} after {what}: widget {i}\n warm {got:?}\n cold {want:?}\n {}", - chain.join(" < ") - ); } - } - assert!(drawn > 0, "seed {seed}: nothing was drawn"); - assert_eq!(wrong, 0, "seed {seed}: {wrong} widgets differ after {what}"); -} - -fn changed_size(seed: u64) { - let mut warm = Harness::new((900, 1200)); - let grown = plant(&mut warm, seed, &Edits::default()); - // Not every tree grows a declared size to change. - if grown.sized.is_empty() { - return; - } - - let mut rng = Rng::new(seed ^ 0x5eed); - let sizes = edit(&mut warm, &grown, &mut rng); - warm.frame(); - - let mut cold = Harness::new((900, 1200)); - let same = plant( - &mut cold, - seed, - &Edits { - sizes, - ..Default::default() - }, - ); - assert_same(seed, "a size change", (&warm, &grown), (&cold, &same)); -} - -/// Moves one widget to a different corner of the box it is given. -fn realign_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns { - let mut side = || match rng.below(4) { - 0 => None, - 1 => Some(AxisAlign::NEG), - 2 => Some(AxisAlign::CENTER), - _ => Some(AxisAlign::POS), }; - let aligns = [side(), side()]; - for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(aligns) { - h.rsc - .widgets_mut() - .set_alignment(tree.aligned[idx], axis, align.unwrap_or_default()); - } - aligns } -fn changed_alignment(seed: u64) { - let mut warm = Harness::new((900, 1200)); - let grown = plant(&mut warm, seed, &Edits::default()); - if grown.aligned.is_empty() { - return; - } - - let mut rng = Rng::new(seed ^ 0xa11); - let aligns = (0..grown.aligned.len()) - .step_by(3) - .map(|idx| (idx, realign_one(&mut warm, &grown, idx, &mut rng))) - .collect(); - warm.frame(); - - let mut cold = Harness::new((900, 1200)); - let same = plant( - &mut cold, - seed, - &Edits { - aligns, - ..Default::default() - }, - ); - assert_same(seed, "an alignment change", (&warm, &grown), (&cold, &same)); -} - -/// Giving a widget a movable region of its own, or taking it away, is a -/// structural change: every primitive under it changes which chain resolves -/// it. A cold tree built that way is what says the rebuild was complete. -fn changed_region_node(seed: u64) { - let mut warm = Harness::new((900, 1200)); - let grown = plant(&mut warm, seed, &Edits::default()); - if grown.nodes.is_empty() { - return; - } - - let nodes: HashMap = (0..grown.nodes.len()) - .step_by(2) - .map(|idx| { - let id = grown.nodes[idx]; - let was = warm.rsc.widgets().is_region_node(id); - warm.rsc.widgets_mut().set_region_node(id, !was); - (idx, !was) - }) - .collect(); - warm.frame(); - - let mut cold = Harness::new((900, 1200)); - let same = plant( - &mut cold, - seed, - &Edits { - nodes, - ..Default::default() - }, - ); - assert_same( - seed, - "a region-node 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()); - // Some seeds grow nothing but wrappers, and a shuffle with no span to - // shuffle is not the same thing as one that had no effect. A span behind - // a branch nobody took is the same kind of nothing: it is not drawn, so - // shuffling it cannot move anything. - let shuffles = grown - .spans - .iter() - .step_by(3) - .any(|span| warm.region(&span.id.id()).is_some()); - if !shuffles { - return; - } - let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle); - warm.frame(); - - 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 changed_every_size(seed: u64) { - let mut warm = Harness::new((900, 1200)); - let grown = plant(&mut warm, seed, &Edits::default()); - if grown.sized.is_empty() { - return; - } - - let mut rng = Rng::new(seed ^ 0xa11); - let sizes = edit_every(&mut warm, &grown, &mut rng); - warm.frame(); - - let mut cold = Harness::new((900, 1200)); - let same = plant( - &mut cold, - seed, - &Edits { - sizes, - ..Default::default() - }, - ); - assert_same(seed, "every size at once", (&warm, &grown), (&cold, &same)); -} - -/// Marks a spread of widgets for redraw at once. Nothing changes, so no box -/// may either; what this exercises is the order a frame settles a dirty set -/// in, which the other cases reach one dependency path at a time. -fn repainted_together(seed: u64) { - let mut warm = Harness::new((900, 1200)); - let grown = plant(&mut warm, seed, &Edits::default()); - for &id in grown.ids.iter().step_by(5) { - warm.rsc.widgets_mut().get_dyn_mut(id); - } - assert!( - !warm.rsc.widgets().needs_redraw.is_empty(), - "seed {seed}: nothing was marked" - ); - warm.frame(); - - let mut cold = Harness::new((900, 1200)); - let same = plant(&mut cold, seed, &Edits::default()); - - let what = "many repaints at once"; - 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, &Edits::default()); - warm.resize((640, 900)); - warm.frame(); - - let mut cold = Harness::new((640, 900)); - 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, &Edits::default()); - if grown.sized.is_empty() { - return; - } - warm.resize((640, 900)); - warm.frame(); - - let mut rng = Rng::new(seed ^ 0xb0a7); - let sizes = edit(&mut warm, &grown, &mut rng); - warm.frame(); - - let mut cold = Harness::new((640, 900)); - 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)); -} - -#[test] -fn a_changed_size_lands_where_growing_it_that_way_would() { - SEEDS.into_iter().for_each(changed_size); -} - -#[test] -fn a_changed_alignment_lands_where_growing_it_that_way_would() { - SEEDS.into_iter().for_each(changed_alignment); -} - -#[test] -fn a_toggled_region_node_lands_where_growing_it_that_way_would() { - SEEDS.into_iter().for_each(changed_region_node); -} - -#[test] -fn every_size_changing_at_once_lands_where_growing_it_that_way_would() { - SEEDS.into_iter().for_each(changed_every_size); -} - -#[test] -fn many_widgets_redrawing_at_once_leaves_every_box_where_it_was() { - SEEDS.into_iter().for_each(repainted_together); -} - -#[test] -fn a_resize_lands_where_starting_at_that_size_would() { - SEEDS.into_iter().for_each(resized); -} - -#[test] -fn a_size_change_after_a_resize_lands_the_same_way() { - SEEDS.into_iter().for_each(resized_then_changed); -} +case!( + many_widgets_redrawing_at_once_leaves_every_box_where_it_was, + Case::RepaintSome +); +case!( + everything_redrawing_at_once_leaves_every_box_where_it_was, + Case::Repaint +); +case!( + a_resize_lands_where_starting_at_that_size_would, + Case::Resize +); +case!( + a_resize_and_a_repaint_land_where_starting_that_way_would, + Case::ResizeRepaint +); +case!( + a_size_change_after_a_resize_lands_the_same_way, + Case::ResizeSize +); +case!( + a_size_change_lands_where_growing_it_that_way_would, + Case::Size +); +case!( + every_size_changing_at_once_lands_where_growing_it_that_way_would, + Case::EverySize +); +case!( + an_alignment_change_lands_where_growing_it_that_way_would, + Case::Align +); +case!( + giving_and_taking_a_movable_region_rebuilds_what_resolves_it, + Case::RegionNode +); +case!( + reordering_a_span_lands_where_growing_it_that_way_would, + Case::Reorder +); #[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); + for case in ALL { + if matches!(case, Case::Shuffle(_)) { + for seed in SEEDS { + check(seed, depth(), case); + } } } } -/// The same property over a hundred seeds and every scenario. What it has -/// found so far was never where the trees disagreed: a text measured in a box -/// it was not going to get, and a widget re-measured in a box its own answer -/// had decided. `tests/shrink.rs` is how a seed from here becomes a tree -/// small enough to read. #[test] -#[ignore = "a hundred seeds, rather than the nine the others check"] +#[ignore = "as many seeds as it is asked for, rather than the nine the others check"] fn a_long_run_of_seeds_agrees() { - let seeds = std::env::var("IRIS_GENERATED_SEED") + let depth = depth(); + let seeds: Vec = match std::env::var("IRIS_GENERATED_SEED") .ok() - .and_then(|seed| seed.parse().ok()) - .map(|seed| seed..=seed) - .unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100)); - over_seeds(seeds.collect(), |seed| { - changed_size(seed); - changed_every_size(seed); - repainted_together(seed); - resized(seed); - resized_then_changed(seed); - for shuffle in SHUFFLES { - reshuffled(seed, shuffle); - } - }); -} - -/// Every seed on its own thread's share of them. A tree is grown, laid out -/// and dropped inside one call, so seeds share nothing, and this is most of -/// the time a run takes. A thread that fails takes the scope down with it, -/// which is the same panic libtest would have seen. -/// -/// One core short of all of them, so the machine this runs on stays usable. -pub fn over_seeds(seeds: Vec, run: impl Fn(u64) + Sync) { - let threads = - std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1)); - let chunk = seeds.len().div_ceil(threads).max(1); - std::thread::scope(|scope| { - for part in seeds.chunks(chunk) { - let run = &run; - scope.spawn(move || part.iter().for_each(|&seed| run(seed))); + .and_then(|v| v.parse().ok()) + { + Some(seed) => vec![seed], + None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(), + }; + over_seeds(seeds, |seed| { + for case in ALL { + check(seed, depth, case); } }); } diff --git a/tests/scenario/mod.rs b/tests/scenario/mod.rs new file mode 100644 index 0000000..d144b6f --- /dev/null +++ b/tests/scenario/mod.rs @@ -0,0 +1,459 @@ +//! The scenarios both fuzzers run, over the tree a [`Plan`] describes. +//! +//! One implementation rather than two. The oracle grew its trees from a seed +//! and the shrinker grew its own, with every scenario written out on each +//! side, so a failure the oracle found could not be handed to the shrinker: +//! there was no tree to pass it, only a seed, and a seed cannot be made +//! smaller. Both take a plan now, so whatever finds a counterexample can also +//! reduce it. +//! +//! Each target compiles this for itself, so what only one of them calls is +//! dead code in the other. +#![allow(dead_code)] + +use iris::harness::Harness; +use iris::prelude::*; +use iris::random::{Aligns, Edits, Kind, Lens, Plan, Rng, SpanEdit, Tree, build}; +use std::collections::HashMap; + +/// A seed per thread but one, since a seed grows, lays out and drops its tree +/// alone. A failing seed still shrinks and panics on its own thread. +pub fn over_seeds(seeds: Vec, run: impl Fn(u64) + Sync) { + let threads = + std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1)); + let chunk = seeds.len().div_ceil(threads).max(1); + std::thread::scope(|scope| { + for part in seeds.chunks(chunk) { + let run = &run; + scope.spawn(move || part.iter().for_each(|&seed| run(seed))); + } + }); +} + +pub fn env(name: &str, fallback: T) -> T { + std::env::var(name) + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(fallback) +} + +/// The window a tree is grown in, and the one a resize takes it to. +const OUTER: (f32, f32) = (1920.0, 1200.0); +const INNER: (f32, f32) = (640.0, 900.0); +const STILL: (f32, f32) = (900.0, 1200.0); + +/// The same box, to a step of the grid per level of nesting between the two +/// ways of reaching it. A move, a repaint and a row of shares land on the +/// same number; what is left is a box centred in a fraction of its parent +/// against the same box centred in its own pixels. A step is a thousandth of +/// a pixel, where this was a twentieth of one before any of it was on a grid. +const AGREE_STEPS: i32 = 2; + +/// 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, PartialEq)] +pub 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, +} + +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, + }, + } + } +} + +/// What a warm tree is put through before it is compared with a cold one +/// grown the way it was left. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Case { + /// Nothing changes, so no box may either. What this exercises is the + /// order a frame settles a dirty set in. + Repaint, + /// Every fifth widget rather than all of them: marking all of them + /// redraws the whole tree, which is a cold start reached the long way, + /// where the mixed case leaves a redrawn subtree beside a retained one. + RepaintSome, + Resize, + ResizeRepaint, + /// A resize and then a size change, so a retained answer is asked to + /// survive two different kinds of invalidation in a row. + ResizeSize, + /// A few declared sizes. + Size, + /// Every declared size at once, so every reader of a size has a changed + /// descendant in the same frame and the whole dirty set settles together. + EverySize, + Align, + /// Giving a widget a movable region of its own, or taking it away, is a + /// structural change: every primitive under it changes which chain + /// resolves it. + RegionNode, + /// The same children in a different order, which moves every one of them + /// without changing what any of them is. + Reorder, + Shuffle(Shuffle), +} + +pub const ALL: [Case; 15] = [ + Case::Repaint, + Case::RepaintSome, + Case::Resize, + Case::ResizeRepaint, + Case::ResizeSize, + Case::Size, + Case::EverySize, + Case::Align, + Case::RegionNode, + Case::Reorder, + Case::Shuffle(Shuffle::EveryOther), + Case::Shuffle(Shuffle::AllButFirst), + Case::Shuffle(Shuffle::AddThree), + Case::Shuffle(Shuffle::SwapForThree), + Case::Shuffle(Shuffle::TradeOne), +]; + +impl Case { + /// The name `CASE` selects it by, and the one a failure prints. + pub fn name(self) -> &'static str { + match self { + Self::Repaint => "repaint", + Self::RepaintSome => "repaint-some", + Self::Resize => "resize", + Self::ResizeRepaint => "resize-repaint", + Self::ResizeSize => "resize-size", + Self::Size => "size", + Self::EverySize => "every-size", + Self::Align => "align", + Self::RegionNode => "region-node", + Self::Reorder => "reorder", + Self::Shuffle(Shuffle::EveryOther) => "shuffle-every-other", + Self::Shuffle(Shuffle::AllButFirst) => "shuffle-all-but-first", + Self::Shuffle(Shuffle::AddThree) => "shuffle-add-three", + Self::Shuffle(Shuffle::SwapForThree) => "shuffle-swap-for-three", + Self::Shuffle(Shuffle::TradeOne) => "shuffle-trade-one", + } + } + + pub fn named(name: &str) -> Option { + ALL.into_iter().find(|case| case.name() == name) + } + + /// Grown in the first, compared in the second. + fn window(self) -> ((f32, f32), (f32, f32)) { + match self { + Self::Resize | Self::ResizeRepaint | Self::ResizeSize => (OUTER, INNER), + _ => (STILL, STILL), + } + } +} + +fn mark(warm: &mut Harness, tree: &Tree, step: usize) { + for &id in tree.ids.iter().step_by(step) { + warm.rsc.widgets_mut().get_dyn_mut(id); + } +} + +fn a_len(rng: &mut Rng) -> Option { + Some(LayoutLen::px(20.0 + rng.below(180) as f32)) +} + +fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens { + let lens = [a_len(rng), a_len(rng)]; + warm.rsc + .widgets_mut() + .set_size_rules(tree.sized[idx], lens[0], lens[1]); + lens +} + +fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns { + let side = |rng: &mut Rng| match rng.below(4) { + 0 => None, + 1 => Some(AxisAlign::NEG), + 2 => Some(AxisAlign::CENTER), + _ => Some(AxisAlign::POS), + }; + let align = [side(rng), side(rng)]; + let id = tree.aligned[idx]; + for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) { + warm.rsc + .widgets_mut() + .set_alignment(id, axis, align.unwrap_or_default()); + } + align +} + +/// Every span's children in a different order, said both to the warm tree and +/// to the plan the cold one is grown from. +fn reorder(warm: &mut Harness, tree: &Tree, plan: &Plan) -> Plan { + for span in &tree.spans { + let children = &mut warm.rsc[span.id].children; + if !children.is_empty() { + children.rotate_left(1); + } + } + let mut out = plan.clone(); + out.walk_mut(&mut |node| { + if let Kind::Span { order, .. } = &mut node.kind + && !order.is_empty() + { + order.rotate_left(1); + } + }); + out +} + +/// Applies `shuffle` to every third span. What it takes out is given back to +/// the span's spares: the last share of a widget must outlive the comparison, +/// or its id is handed to something else and the two trees stop lining up. +fn reshuffle(warm: &mut Harness, tree: &mut Tree, shuffle: Shuffle) -> HashMap { + let mut edits = HashMap::new(); + for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) { + let edit = shuffle.of(span.grown); + let mut take = edit.detach.clone(); + take.sort_unstable(); + let children = &mut warm.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() { + span.spares.push(children.remove(j)); + } + } + let attach = edit.attach.min(span.spares.len()); + let moved: Vec<_> = span.spares.drain(..attach).collect(); + warm.rsc[span.id].children.extend(moved); + edits.insert(idx, edit); + } + edits +} + +/// Changes the warm tree and answers with the plan a cold tree grown that way +/// comes from. Each arm settles its own frame, so a case that changes nothing +/// does not get a second one that could settle what the first left. +fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mut Rng) -> Plan { + let some_sizes = |warm: &mut Harness, tree: &Tree, rng: &mut Rng| { + let mut sizes = HashMap::new(); + for _ in 0..4 { + if tree.sized.is_empty() { + break; + } + let idx = rng.below(tree.sized.len()); + sizes.insert(idx, resize_one(warm, tree, idx, rng)); + } + sizes + }; + let edits = match case { + Case::Resize => return plan.clone(), + Case::Repaint | Case::ResizeRepaint => { + mark(warm, tree, 1); + warm.frame(); + return plan.clone(); + } + Case::RepaintSome => { + mark(warm, tree, 5); + warm.frame(); + return plan.clone(); + } + Case::Reorder => { + let out = reorder(warm, tree, plan); + warm.frame(); + return out; + } + Case::Size | Case::ResizeSize => Edits { + sizes: some_sizes(warm, tree, rng), + ..Default::default() + }, + Case::EverySize => Edits { + sizes: (0..tree.sized.len()) + .map(|idx| (idx, resize_one(warm, tree, idx, rng))) + .collect(), + ..Default::default() + }, + Case::Align => Edits { + aligns: (0..tree.aligned.len()) + .step_by(3) + .map(|idx| (idx, realign_one(warm, tree, idx, rng))) + .collect(), + ..Default::default() + }, + Case::RegionNode => { + let mut nodes = HashMap::new(); + for idx in (0..tree.nodes.len()).step_by(2) { + let id = tree.nodes[idx]; + let take = !warm.rsc.widgets().is_region_node(id); + warm.rsc.widgets_mut().set_region_node(id, take); + nodes.insert(idx, take); + } + Edits { + nodes, + ..Default::default() + } + } + Case::Shuffle(shuffle) => Edits { + spans: reshuffle(warm, tree, shuffle), + ..Default::default() + }, + }; + warm.frame(); + plan.edited(&edits) +} + +/// What a widget was configured with, so a tree a fuzzer found can be written +/// out by hand. A failure is a lead; the fast test that replaces it has to be +/// buildable from what the failure printed. +fn describe(id: WidgetId, h: &Harness) -> String { + let rules = h.rsc.widgets().size_rules(id); + let rule = |r: SizeRule| match r.exact() { + Some(len) => format!("{len}"), + None => "-".into(), + }; + let align = h.rsc.widgets().alignment(id); + let side = |a: AxisAlign| { + if a == AxisAlign::NEG { + "neg".into() + } else if a == AxisAlign::CENTER { + "mid".into() + } else if a == AxisAlign::POS { + "pos".into() + } else { + format!("{:.2}", a.rel()) + } + }; + // A rule and an alignment are properties of whatever carries them, so + // they print with that widget rather than as widgets of their own. + let mut out = describe_widget(id, h); + if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) { + out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y)); + } + if align != RegionAlign::default() { + out += &format!("@{},{}", side(align.x), side(align.y)); + } + out +} + +fn describe_widget(id: WidgetId, h: &Harness) -> String { + let label = h.rsc.widgets().label(id).to_string(); + let Some(widget) = h.rsc.widgets().get_dyn(id) else { + return label; + }; + let any: &dyn std::any::Any = widget; + if let Some(w) = any.downcast_ref::() { + let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" }; + return format!( + "Span{{dir:{:?}{sign},gap:{},n:{}}}", + w.dir.axis, + w.gap, + w.children.len() + ); + } + if let Some(w) = any.downcast_ref::() { + let p = &w.padding; + return format!( + "Pad{{l:{},r:{},t:{},b:{}}}", + p.left, p.right, p.top, p.bottom + ); + } + if let Some(w) = any.downcast_ref::() { + return format!("Stack{{n:{}}}", w.children.len()); + } + label +} + +fn same_region(got: Option, want: Option) -> bool { + match (got, want) { + (Some(got), Some(want)) => { + let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS); + same(got.top_left.x, want.top_left.x) + && same(got.top_left.y, want.top_left.y) + && same(got.bot_right.x, want.bot_right.x) + && same(got.bot_right.y, want.bot_right.y) + } + (None, None) => true, + _ => false, + } +} + +/// Runs `case` on the tree `plan` describes, warm and cold, and says where +/// the two disagree. `seed` chooses only the values a case picks at random, +/// so one plan under one case is one comparison however it was reached. +pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option { + let (start, end) = case.window(); + let mut warm = Harness::new(start); + let (root, mut tree) = build(&mut warm.rsc, plan); + warm.state.root = Some(root); + // The frame that makes it warm: without it nothing is retained and the + // comparison is two cold starts agreeing with each other. + warm.frame(); + if start != end { + warm.resize(end); + warm.frame(); + } + let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed)); + + let mut cold = Harness::new(end); + let (root, cold_tree) = build(&mut cold.rsc, &cold_plan); + cold.state.root = Some(root); + cold.frame(); + + let mut drawn = 0; + for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() { + let (got, want) = (warm.region(&w), cold.region(&c)); + drawn += got.is_some() as usize; + if same_region(got, want) { + continue; + } + // Where two trees disagree is rarely where the cause is, so the + // ancestry comes with it, marking the widgets that own a region. + let mut chain = Vec::new(); + let mut at = Some(w); + while let Some(id) = at { + let active = &warm.render.active[&id]; + let node = match active.move_idx == active.parent_move { + true => "", + false => "*", + }; + chain.push(format!("{}{node}", describe(id, &warm))); + at = active.parent; + } + return Some(format!( + "widget {i}\n warm {got:?}\n cold {want:?}\n {}", + chain.join(" < ") + )); + } + match drawn { + 0 => Some("nothing was drawn".into()), + _ => None, + } +} diff --git a/tests/shrink.rs b/tests/shrink.rs index f971874..4c4b52e 100644 --- a/tests/shrink.rs +++ b/tests/shrink.rs @@ -1,556 +1,38 @@ -//! A property test that shrinks its own counterexample. +//! A fuzzer that reduces its own counterexample. //! -//! `generated.rs` reproduces a failure from a seed, but a seed is not a lead -//! anybody can read: the tree is hundreds of widgets, and reconstructing the -//! part that matters by hand has failed every time it has been tried. This -//! grows trees it can take apart, so a failure is reduced to the smallest -//! tree that still shows it and printed as something to write a fast test -//! from. +//! A seed is not a lead anybody can read: the tree is hundreds of widgets, +//! and reconstructing the part that matters by hand has failed every time it +//! has been tried. This grows the trees `iris::random` describes, takes them +//! apart, and prints the smallest one that still fails as something to write +//! a fast test from. //! //! cargo test --release --test shrink -- --ignored --nocapture //! //! `SHRINK_SEEDS` how many trees to try, `SHRINK_DEPTH` how deep to grow -//! them, `SHRINK_CASE` which scenario. It is a fuzzer: run it once the -//! ordinary tests pass, and turn what it finds into a test of its own rather -//! than leaving a seed as the record. +//! them, `SHRINK_CASE` which scenario or `all` for every one. `SHRINK_SEED` +//! takes a single seed, which is how a failure `generated` printed is handed +//! straight here: the two run the same cases over the same trees, so a seed +//! that fails there fails here and is reduced. +//! +//! It is a fuzzer: run it once the ordinary tests pass, and turn what it +//! finds into a test of its own rather than leaving a seed as the record. -use iris::harness::Harness; -use iris::prelude::*; -use iris::random::{Branch, Rng}; +#[path = "scenario/mod.rs"] +mod scenario; -/// The same two leaves `iris::random` grows, since only one of them reads the -/// width it is given and that is the difference that matters. -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.", -]; +use iris::random::{Edits, Plan, plan}; +use scenario::{ALL, Case, diverges, env, over_seeds}; -const ONE_LINE: &str = "one line, overflowing whatever it is given"; - -const OUTER: (f32, f32) = (1920.0, 1200.0); -/// Steps of the grid two ways of reaching a box may differ by: one per level -/// of nesting between them, and these trees are five deep. See -/// `docs/HANDOFF.md`'s "Fixed point" in `ai-app-2` for what is left. -const AGREE_STEPS: i32 = 2; -const INNER: (f32, f32) = (640.0, 900.0); - -#[derive(Clone, Debug, PartialEq)] -enum Node { - /// Words taken from [`WORDS`], and whether it wraps. - Text(usize, bool), - /// The leaf that overflows whatever box it is given rather than wrapping. - OneLine, - Rect, - /// Direction, gap, children in creation order, and the order they are - /// attached in -- separate so a tree that reorders its children - /// still makes the same widgets in the same order, and two - /// builds line up index for index. - Span(bool, f32, Vec, Vec), - Stack(Vec), - Pad(f32, Box), - Aligned(u8, u8, Box), - Sized(Option, Option, Box), - Scroll(bool, Box), - Branch(Box, Box, Box, f32), -} - -fn axis_align(v: u8) -> Option { - match v % 4 { - 0 => None, - 1 => Some(AxisAlign::NEG), - 2 => Some(AxisAlign::CENTER), - _ => Some(AxisAlign::POS), - } -} - -fn dir(down: bool) -> Dir { - if down { Dir::DOWN } else { Dir::RIGHT } -} - -impl Node { - /// Builds into `h`, pushing every id in tree order, so two builds of one - /// node line up index for index and their boxes can be compared. - fn build( - &self, - h: &mut Harness, - out: &mut Vec, - spans: &mut Vec>, - sized: &mut Vec, - ) -> StrongWidget { - let id: StrongWidget = match self { - Node::Text(words, wrap) => { - let n = (*words).clamp(1, WORDS.len()); - wtext(WORDS[..n].join(" ")) - .size(16) - .wrap(*wrap) - .add_strong(&mut h.rsc) - } - Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc), - Node::Rect => rect(Color::RED).add_strong(&mut h.rsc), - Node::Span(down, gap, kids, order) => { - let mut built: Vec<_> = kids - .iter() - .map(|k| Some(k.build(h, out, spans, sized))) - .collect(); - // `order` is a permutation, so each is taken exactly once. - let children = order - .iter() - .map(|&i| built[i].take().expect("order repeats an index")) - .collect(); - let handle = Span { - children, - dir: dir(*down), - gap: Px::from_f32(*gap), - } - .add(&mut h.rsc); - // A row takes the height it is given; a column is as wide - // as its widest child, which needs no rule. - if !*down { - h.rsc - .widgets_mut() - .set_size_rules(handle, None, Some(LayoutLen::rel(1.0))); - } - spans.push(handle); - handle.add_strong(&mut h.rsc) - } - Node::Stack(kids) => { - let children = kids.iter().map(|k| k.build(h, out, spans, sized)).collect(); - Stack { - children, - size: StackSize::Child(0), - } - .add_strong(&mut h.rsc) - } - Node::Pad(p, kid) => { - let inner = kid.build(h, out, spans, sized); - Pad { - padding: Padding::uniform(*p), - inner, - } - .add_strong(&mut h.rsc) - } - Node::Aligned(x, y, kid) => { - let inner = kid.build(h, out, spans, sized); - for (axis, align) in [(Axis::X, axis_align(*x)), (Axis::Y, axis_align(*y))] { - if let Some(align) = align { - h.rsc.widgets_mut().set_alignment(&inner, axis, align); - } - } - inner - } - Node::Sized(x, y, kid) => { - let inner = kid.build(h, out, spans, sized); - h.rsc.widgets_mut().set_size_rules(&inner, *x, *y); - sized.push(inner.id()); - inner - } - Node::Scroll(down, kid) => { - let inner = kid.build(h, out, spans, sized); - let axis = if *down { Axis::Y } else { Axis::X }; - Scroll::new(inner, axis).add_strong(&mut h.rsc) - } - Node::Branch(probe, a, b, at) => { - let probe = probe.build(h, out, spans, sized); - let wide = a.build(h, out, spans, sized); - let narrow = b.build(h, out, spans, sized); - Branch { - probe, - wide, - narrow, - threshold: *at, - } - .add_strong(&mut h.rsc) - } - }; - out.push(id.id()); - id - } - - /// The lengths every `Sized` node would carry after `resized`, in the - /// order `build` pushes them. - fn sized_lens(&self, out: &mut Vec<(Option, Option)>) { - match self { - Node::Text(..) | Node::OneLine | Node::Rect => {} - Node::Span(_, _, kids, _) | Node::Stack(kids) => { - kids.iter().for_each(|k| k.sized_lens(out)); - } - Node::Pad(_, k) | Node::Aligned(_, _, k) | Node::Scroll(_, k) => k.sized_lens(out), - Node::Sized(x, y, k) => { - k.sized_lens(out); - out.push((resized_len(*x), resized_len(*y))); - } - Node::Branch(p, a, b, _) => { - p.sized_lens(out); - a.sized_lens(out); - b.sized_lens(out); - } - } - } - - fn size(&self) -> usize { - 1 + match self { - Node::Text(..) | Node::OneLine | Node::Rect => 0, - Node::Span(_, _, kids, _) | Node::Stack(kids) => kids.iter().map(Node::size).sum(), - Node::Pad(_, k) - | Node::Aligned(_, _, k) - | Node::Sized(_, _, k) - | Node::Scroll(_, k) => k.size(), - Node::Branch(p, a, b, _) => p.size() + a.size() + b.size(), - } - } - - /// Every one-step simplification: a wrapper replaced by what it wrapped, a - /// child dropped, a length or a word count reduced. Ordered cheapest-first - /// so the greedy walk takes the biggest bites early. - fn smaller(&self) -> Vec { - let mut out = Vec::new(); - let leaf = Node::Rect; - match self { - Node::Text(words, wrap) => { - if *words > 1 { - out.push(Node::Text(words / 2, *wrap)); - out.push(Node::Text(words - 1, *wrap)); - } - if *wrap { - out.push(Node::Text(*words, false)); - } - out.push(leaf); - } - Node::OneLine => out.push(Node::Rect), - Node::Rect => {} - Node::Span(down, gap, kids, order) => { - out.extend(order.iter().map(|&i| kids[i].clone())); - for i in 0..kids.len() { - if kids.len() > 1 { - let mut less = kids.clone(); - less.remove(i); - let order = (0..less.len()).collect(); - out.push(Node::Span(*down, *gap, less, order)); - } - } - if *gap != 0.0 { - out.push(Node::Span(*down, 0.0, kids.clone(), order.clone())); - } - for (i, kid) in kids.iter().enumerate() { - for small in kid.smaller() { - let mut next = kids.clone(); - next[i] = small; - out.push(Node::Span(*down, *gap, next, order.clone())); - } - } - } - Node::Stack(kids) => { - out.extend(kids.iter().cloned()); - for i in 0..kids.len() { - if kids.len() > 1 { - let mut less = kids.clone(); - less.remove(i); - out.push(Node::Stack(less)); - } - } - for (i, kid) in kids.iter().enumerate() { - for small in kid.smaller() { - let mut next = kids.clone(); - next[i] = small; - out.push(Node::Stack(next)); - } - } - } - Node::Pad(p, kid) => { - out.push((**kid).clone()); - if *p != 0.0 { - out.push(Node::Pad(0.0, kid.clone())); - } - out.extend( - kid.smaller() - .into_iter() - .map(|k| Node::Pad(*p, Box::new(k))), - ); - } - Node::Aligned(x, y, kid) => { - out.push((**kid).clone()); - for (nx, ny) in [(0, *y), (*x, 0)] { - if (nx, ny) != (*x, *y) { - out.push(Node::Aligned(nx, ny, kid.clone())); - } - } - out.extend( - kid.smaller() - .into_iter() - .map(|k| Node::Aligned(*x, *y, Box::new(k))), - ); - } - Node::Sized(x, y, kid) => { - out.push((**kid).clone()); - if x.is_some() { - out.push(Node::Sized(None, *y, kid.clone())); - } - if y.is_some() { - out.push(Node::Sized(*x, None, kid.clone())); - } - out.extend( - kid.smaller() - .into_iter() - .map(|k| Node::Sized(*x, *y, Box::new(k))), - ); - } - Node::Scroll(down, kid) => { - out.push((**kid).clone()); - out.extend( - kid.smaller() - .into_iter() - .map(|k| Node::Scroll(*down, Box::new(k))), - ); - } - Node::Branch(p, a, b, at) => { - out.push((**p).clone()); - out.push((**a).clone()); - out.push((**b).clone()); - for small in p.smaller() { - out.push(Node::Branch(Box::new(small), a.clone(), b.clone(), *at)); - } - for small in a.smaller() { - out.push(Node::Branch(p.clone(), Box::new(small), b.clone(), *at)); - } - for small in b.smaller() { - out.push(Node::Branch(p.clone(), a.clone(), Box::new(small), *at)); - } - } - } - out - } -} - -/// A declared size over about half the tree, the way `iris::random` puts them -/// in: on the way into every child rather than as a node kind of its own, so -/// readers of a size are dense rather than occasional. -fn sized(rng: &mut Rng, inner: Node) -> Node { - if !rng.chance() { - return inner; - } - let len = |rng: &mut Rng| match rng.below(4) { - 0 => Some(LayoutLen::px(20.0 + rng.below(180) as f32)), - 1 => Some(LayoutLen::LEFTOVER), - _ => None, - }; - Node::Sized(len(rng), len(rng), Box::new(inner)) -} - -fn grow(rng: &mut Rng, depth: usize) -> Node { - if depth == 0 { - return match rng.below(4) { - 0 => Node::Text(1 + rng.below(WORDS.len()), true), - 1 => Node::OneLine, - _ => Node::Rect, - }; - } - let len = |rng: &mut Rng| match rng.below(4) { - 0 => Some(LayoutLen::px(20.0 + rng.below(180) as f32)), - 1 => Some(LayoutLen::LEFTOVER), - 2 => Some(LayoutLen::rel(0.25 + rng.below(3) as f32 * 0.25)), - _ => None, - }; - let kid = |rng: &mut Rng| { - let inner = grow(rng, depth - 1); - sized(rng, inner) - }; - match rng.below(8) { - 0 => Node::Scroll(rng.chance(), Box::new(kid(rng))), - 1 => Node::Aligned(rng.below(4) as u8, rng.below(4) as u8, Box::new(kid(rng))), - 2 => Node::Pad(rng.below(24) as f32, Box::new(kid(rng))), - 3 => Node::Sized(len(rng), len(rng), Box::new(kid(rng))), - 4 => Node::Branch( - Box::new(kid(rng)), - Box::new(kid(rng)), - Box::new(kid(rng)), - rng.below(500) as f32, - ), - 5 => Node::Stack((0..2 + rng.below(2)).map(|_| kid(rng)).collect()), - _ => { - let kids: Vec<_> = (0..2 + rng.below(3)).map(|_| kid(rng)).collect(); - let order = (0..kids.len()).collect(); - Node::Span(rng.chance(), rng.below(3) as f32 * 4.0, kids, order) - } - } -} - -#[derive(Clone, Copy, PartialEq)] -enum Case { - Resize, - Repaint, - ResizeRepaint, - Reorder, - SizeChange, -} - -/// A different declared length, kept the same kind so the change is to the -/// value alone. -fn resized_len(len: Option) -> Option { - let half = Rel::from_f32(0.5); - len.map(|len| LayoutLen { - px: len.px.mul(half) + Px::from_int(13), - rel: len.rel.mul(half), - leftover: len.leftover, - }) -} - -/// Every declared size changed, as a tree rather than as a change. -fn resized(node: &Node) -> Node { - match node { - Node::Span(down, gap, kids, order) => Node::Span( - *down, - *gap, - kids.iter().map(resized).collect(), - order.clone(), - ), - Node::Stack(kids) => Node::Stack(kids.iter().map(resized).collect()), - Node::Pad(p, k) => Node::Pad(*p, Box::new(resized(k))), - Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(resized(k))), - Node::Sized(x, y, k) => Node::Sized(resized_len(*x), resized_len(*y), Box::new(resized(k))), - Node::Scroll(d, k) => Node::Scroll(*d, Box::new(resized(k))), - Node::Branch(p, a, b, at) => Node::Branch( - Box::new(resized(p)), - Box::new(resized(a)), - Box::new(resized(b)), - *at, - ), - leaf => leaf.clone(), - } -} - -/// Every span's children rotated by one, as a tree rather than as a change: -/// what a warm frame reaches by moving them has to be where growing them that -/// way lands. -fn reordered(node: &Node) -> Node { - match node { - Node::Span(down, gap, kids, order) => { - let kids = kids.iter().map(reordered).collect::>(); - let mut order = order.clone(); - order.rotate_left(1); - Node::Span(*down, *gap, kids, order) - } - Node::Stack(kids) => Node::Stack(kids.iter().map(reordered).collect()), - Node::Pad(p, k) => Node::Pad(*p, Box::new(reordered(k))), - Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(reordered(k))), - Node::Sized(x, y, k) => Node::Sized(*x, *y, Box::new(reordered(k))), - Node::Scroll(d, k) => Node::Scroll(*d, Box::new(reordered(k))), - Node::Branch(p, a, b, at) => Node::Branch( - Box::new(reordered(p)), - Box::new(reordered(a)), - Box::new(reordered(b)), - *at, - ), - leaf => leaf.clone(), - } -} - -/// Runs one scenario warm and cold and says where they disagree. -fn diverges(node: &Node, case: Case) -> Option { - let resizes = matches!(case, Case::Resize | Case::ResizeRepaint); - let repaints = matches!(case, Case::Repaint | Case::ResizeRepaint); - let start = if resizes { OUTER } else { INNER }; - let mut warm = Harness::new(start); - let mut warm_ids = Vec::new(); - let mut warm_spans = Vec::new(); - let mut warm_sized = Vec::new(); - let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans, &mut warm_sized); - warm.state.root = Some(root); - // The frame that makes it warm: without it there is nothing retained and - // the comparison is two cold starts agreeing with each other. - warm.frame(); - if resizes { - warm.resize(INNER); - warm.frame(); - } - if repaints { - for &id in &warm_ids { - warm.rsc.widgets_mut().get_dyn_mut(id); - } - warm.frame(); - } - if case == Case::Reorder { - for span in &warm_spans { - warm.rsc[*span].children.rotate_left(1); - } - warm.frame(); - } - if case == Case::SizeChange { - let mut lens = Vec::new(); - node.sized_lens(&mut lens); - for (id, (x, y)) in warm_sized.iter().zip(lens) { - warm.rsc.widgets_mut().set_size_rules(*id, x, y); - } - warm.frame(); - } - - // What the warm tree was moved into, grown that way from the start. - let want = match case { - Case::Reorder => reordered(node), - Case::SizeChange => resized(node), - _ => node.clone(), - }; - let mut cold = Harness::new(INNER); - let mut cold_ids = Vec::new(); - let mut cold_spans = Vec::new(); - let mut cold_sized = Vec::new(); - let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans, &mut cold_sized); - cold.state.root = Some(root); - cold.frame(); - - for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() { - let (got, want) = (warm.region(&w), cold.region(&c)); - // To a couple of steps of the grid, each a thousandth of a pixel: a - // move or a resize lands on the same number now, and a length - // measured one way against the same length composed another can - // still be a step out per composition between them. - let same = match (got, want) { - (Some(g), Some(c)) => { - let d = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS); - d(g.top_left.x, c.top_left.x) - && d(g.top_left.y, c.top_left.y) - && d(g.bot_right.x, c.bot_right.x) - && d(g.bot_right.y, c.bot_right.y) - } - (None, None) => true, - _ => false, - }; - if !same { - return Some(format!("widget {i}: warm {got:?} cold {want:?}")); - } - } - None -} - -/// Takes the first simplification that still fails, until none does. -fn shrink(mut node: Node, case: Case) -> Node { +/// Takes the first simplification that still fails, until none does. The +/// simplifications come biggest first, so this walks down rather than +/// nibbling: a six-hundred-widget tree reaches single figures in a few +/// hundred builds. +fn shrink(mut node: Plan, case: Case, seed: u64) -> Plan { loop { let Some(next) = node .smaller() .into_iter() - .find(|small| diverges(small, case).is_some()) + .find(|small| diverges(small, case, seed).is_some()) else { return node; }; @@ -558,62 +40,60 @@ fn shrink(mut node: Node, case: Case) -> Node { } } -/// One thread per core but one, each taking a share of the seeds: a tree is -/// grown, laid out and dropped within a seed, so nothing is shared. A seed -/// that fails shrinks on its own thread and panics there, which brings the -/// scope down with it. -fn over_seeds(seeds: Vec, run: impl Fn(u64) + Sync) { - let threads = - std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1)); - let chunk = seeds.len().div_ceil(threads).max(1); - std::thread::scope(|scope| { - for part in seeds.chunks(chunk) { - let run = &run; - scope.spawn(move || part.iter().for_each(|&seed| run(seed))); - } - }); -} - -fn env(name: &str, fallback: T) -> T { - std::env::var(name) - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(fallback) +fn cases() -> Vec { + match env("SHRINK_CASE", String::from("all")).as_str() { + "all" => ALL.to_vec(), + name => match Case::named(name) { + Some(case) => vec![case], + None => panic!( + "unknown SHRINK_CASE {name:?}; one of all, {}", + ALL.map(Case::name).join(", ") + ), + }, + } } #[test] #[ignore = "a fuzzer; run it once the ordinary tests pass"] fn no_grown_tree_lays_out_differently_warm_than_cold() { - let seeds: u64 = env("SHRINK_SEEDS", 400); let depth: usize = env("SHRINK_DEPTH", 5); - let case = match env("SHRINK_CASE", String::from("resize")).as_str() { - "repaint" => Case::Repaint, - "resize-repaint" => Case::ResizeRepaint, - "reorder" => Case::Reorder, - "size-change" => Case::SizeChange, - _ => Case::Resize, + let cases = cases(); + let seeds: Vec = match std::env::var("SHRINK_SEED") + .ok() + .and_then(|v| v.parse().ok()) + { + Some(seed) => vec![seed], + None => (1..=env("SHRINK_SEEDS", 400_u64)).collect(), }; + let count = seeds.len(); - over_seeds((1..=seeds).collect(), |seed| { - let node = grow(&mut Rng::new(seed), depth); - let Some(how) = diverges(&node, case) else { - return; - }; - let small = shrink(node.clone(), case); - println!( - "seed {seed}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}", - node.size(), - small.size() - ); - panic!("seed {seed} lays out differently warm than cold"); + over_seeds(seeds, |seed| { + let grown = plan(seed, depth, &Edits::default()); + for &case in &cases { + let Some(how) = diverges(&grown, case, seed) else { + continue; + }; + let small = shrink(grown.clone(), case, seed); + println!( + "seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}", + case.name(), + grown.size(), + small.size() + ); + panic!( + "seed {seed} lays out differently warm than cold after {}", + case.name() + ); + } }); - let sizes: Vec = (1..=seeds) - .map(|seed| grow(&mut Rng::new(seed), depth).size()) + + let sizes: Vec = (1..=count as u64) + .map(|seed| plan(seed, depth, &Edits::default()).size()) .collect(); - let total: usize = sizes.iter().sum(); println!( - "{seeds} trees at depth {depth} agree: {} widgets total, largest {}", - total, + "{count} trees at depth {depth} agree over {} case(s): {} widgets total, largest {}", + cases.len(), + sizes.iter().sum::(), sizes.iter().max().copied().unwrap_or(0) ); } diff --git a/tests/suite.rs b/tests/suite.rs index b11c307..d98c6d4 100644 --- a/tests/suite.rs +++ b/tests/suite.rs @@ -16,6 +16,8 @@ mod drift; mod idempotence; #[path = "cases/layout.rs"] mod layout; +#[path = "cases/plan.rs"] +mod plan; #[path = "cases/pointer.rs"] mod pointer; #[path = "cases/pointer_routing.rs"]