Comparing boxes catches a widget that moved. It does not catch one that measured a child, was handed an answer a cold start would not have given, and took the other branch -- the same defect, arriving where a pixel comparison cannot see it. Branching on what the painter tells you is something a widget is allowed to do, so the library owes the same answer warm and cold; only a widget changing its own configuration is exempt. `random::Branch` measures a child and draws one of two others on the result, with both grown either way so the ids match whichever is drawn. It joins the generator, which makes every existing scenario a control-flow oracle as well as a geometric one. `tests/determinism.rs` is the same widget by hand across eight thresholds, including either side of the answer, and is the fast check -- the sweep is a fuzzer and confirms at the end rather than being iterated against. A span behind a branch nobody took is not drawn, so shuffling it cannot move anything; `reshuffled` now treats that as vacuous, the way it already treats a tree with no spans, rather than as a shuffle that had no effect. Both new tests pass, and the sweep passes at depth 4 and 5 over 200 seeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
318 lines
11 KiB
Rust
318 lines
11 KiB
Rust
//! A seeded random widget tree, for tests and for looking at.
|
|
//!
|
|
//! One seed is one tree, on any machine and after any upgrade, so a test can
|
|
//! grow the same tree twice and a failing seed is reproduced by its number.
|
|
//! `examples/random.rs` draws one; `tests/generated.rs` checks that laying one
|
|
//! out again lands where growing it from scratch would.
|
|
|
|
use crate::prelude::*;
|
|
use std::collections::HashMap;
|
|
|
|
/// The declared lengths of one `SetSize`, by axis.
|
|
pub type Lens = [Option<Len>; 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<usize, Lens>,
|
|
/// Which children a span has, by the order the spans were made.
|
|
pub spans: HashMap<usize, SpanEdit>,
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct SpanEdit {
|
|
/// Children to leave out, by index among the ones grown.
|
|
pub detach: Vec<usize>,
|
|
/// 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);
|
|
|
|
impl Rng {
|
|
pub fn new(seed: u64) -> Self {
|
|
Self(seed | 1)
|
|
}
|
|
|
|
pub fn bits(&mut self) -> u64 {
|
|
self.0 ^= self.0 << 13;
|
|
self.0 ^= self.0 >> 7;
|
|
self.0 ^= self.0 << 17;
|
|
self.0
|
|
}
|
|
|
|
pub fn below(&mut self, n: usize) -> usize {
|
|
(self.bits() % n as u64) as usize
|
|
}
|
|
|
|
pub fn chance(&mut self) -> bool {
|
|
self.bits() & 1 == 0
|
|
}
|
|
}
|
|
|
|
const COLORS: [UiColor; 6] = [
|
|
UiColor::RED,
|
|
UiColor::GREEN,
|
|
UiColor::BLUE,
|
|
UiColor::YELLOW,
|
|
UiColor::CYAN,
|
|
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.";
|
|
|
|
/// What growing a tree gives back: every widget in creation order, so two
|
|
/// trees from one seed line up index for index, and the declared sizes, which
|
|
/// are what a test changes to watch the change propagate.
|
|
#[derive(Default)]
|
|
pub struct Tree {
|
|
pub ids: Vec<WidgetId>,
|
|
pub sized: Vec<WeakWidget<SetSize>>,
|
|
pub spans: Vec<Spanned>,
|
|
pub scrolls: Vec<WeakWidget<Scroll>>,
|
|
/// 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<StrongWidget>,
|
|
}
|
|
|
|
/// Branches on a child's measured length. Comparing boxes catches a widget
|
|
/// that moved; this catches one that believed a measurement a cold start
|
|
/// would not have given it, by turning that into a different tree. Its own
|
|
/// configuration never changes, so which side draws is a property of the
|
|
/// layout alone.
|
|
pub struct Branch {
|
|
pub probe: StrongWidget,
|
|
pub wide: StrongWidget,
|
|
pub narrow: StrongWidget,
|
|
pub threshold: f32,
|
|
}
|
|
|
|
impl Widget for Branch {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
let mut top = UiRegion::FULL;
|
|
top.y.end = top.y.start.offset(40.0);
|
|
let measured = painter.place(&self.probe, top).len(Axis::X);
|
|
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
|
|
|
|
let mut rest = UiRegion::FULL;
|
|
rest.y.start = rest.y.start.offset(40.0);
|
|
match px > self.threshold {
|
|
true => painter.place(&self.wide, rest),
|
|
false => painter.place(&self.narrow, rest),
|
|
};
|
|
Size::REST
|
|
}
|
|
}
|
|
|
|
pub struct Spanned {
|
|
pub id: WeakWidget<Span>,
|
|
/// 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<StrongWidget>,
|
|
/// How many children it was grown with, before any edit.
|
|
pub grown: usize,
|
|
}
|
|
|
|
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
|
|
/// would otherwise have given those wrappers.
|
|
pub fn grow<Rsc: UiRsc + 'static>(
|
|
rsc: &mut Rsc,
|
|
seed: u64,
|
|
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)
|
|
}
|
|
|
|
struct Grow<'a, Rsc> {
|
|
rsc: &'a mut Rsc,
|
|
rng: Rng,
|
|
tree: Tree,
|
|
edits: &'a Edits,
|
|
}
|
|
|
|
impl<Rsc: UiRsc + 'static> 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),
|
|
_ => {
|
|
let color = COLORS[self.rng.below(COLORS.len())];
|
|
let alpha = (self.rng.below(5) * 63) as u8;
|
|
rect(color.alpha(alpha)).add_strong(self.rsc)
|
|
}
|
|
};
|
|
self.tree.ids.push(id.id());
|
|
id
|
|
}
|
|
|
|
fn len(&mut self) -> Option<Len> {
|
|
match self.rng.below(4) {
|
|
0 => Some(Len::px(20.0 + self.rng.below(180) as f32)),
|
|
1 => Some(Len::REST),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn align(&mut self) -> Align {
|
|
let mut axis = || match self.rng.below(4) {
|
|
0 => None,
|
|
1 => Some(AxisAlign::Neg),
|
|
2 => Some(AxisAlign::Center),
|
|
_ => Some(AxisAlign::Pos),
|
|
};
|
|
let (mut x, y) = (axis(), axis());
|
|
// Aligning on neither axis is just another transparent wrapper and
|
|
// would leave this branch unexercised.
|
|
if x.is_none() && y.is_none() {
|
|
x = Some(AxisAlign::Center);
|
|
}
|
|
Align { x, y }
|
|
}
|
|
|
|
/// A declared size over half the tree, kept where a test can change it.
|
|
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
|
|
if !self.rng.chance() {
|
|
return inner;
|
|
}
|
|
let idx = self.tree.sized.len();
|
|
let lens = [self.len(), self.len()];
|
|
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
|
|
let id = SetSize {
|
|
inner,
|
|
x: lens[0],
|
|
y: lens[1],
|
|
}
|
|
.add(self.rsc);
|
|
self.tree.sized.push(id);
|
|
self.tree.ids.push(id.id());
|
|
id.add_strong(self.rsc)
|
|
}
|
|
|
|
fn node(&mut self, depth: usize) -> StrongWidget {
|
|
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 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);
|
|
}
|
|
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);
|
|
let threshold = self.rng.below(500) as f32;
|
|
let id = Branch {
|
|
probe,
|
|
wide,
|
|
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 id = Aligned {
|
|
inner,
|
|
align: self.align(),
|
|
}
|
|
.add_strong(self.rsc);
|
|
self.tree.ids.push(id.id());
|
|
return id;
|
|
}
|
|
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));
|
|
}
|
|
if self.rng.chance() {
|
|
let id = Stack {
|
|
children,
|
|
size: StackSize::Child(0),
|
|
}
|
|
.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<StrongWidget> = (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());
|
|
self.tree.spans.push(Spanned { id, spares, grown });
|
|
id.add_strong(self.rsc)
|
|
}
|
|
}
|