`Px` and `PxVec2` reach the last places a pixel was a float: the window, the box a widget reads, the box it is compared against, and `PixelRegion`. A pointer, a wheel notch and a shaped glyph advance still arrive as floats, and each is put on the grid where it arrives. `Holds` is an interval of `Px`. `HOLDS_EPSILON_PX` is gone with the `exact`/tolerant split it existed for: `at` is the length a widget read, an open end is the next step along, and `same_px` is equality. `Span`'s margin from `5ed9e87` goes too -- the box a parent hands back and the sum of what its children asked for are counts of the same step, so the boundary decides the same way from either side. Three things had to be true for that, and were not: `Holds::through` inverts `px + rel * box`, which rounds -- so a part of a given length came from a range of boxes, and inverting the length alone gave a point that need not contain the box the part was drawn in. It now maps the half step either side, and one more for a length composed down the chain against the same length measured against the window. `RegionRemap` translates when a box only moved, rather than dividing to find each part's fraction and multiplying to place it again. Two roundings landed a step from where growing the tree that way does; a move is exact on a grid, which is the whole reason `tests/drift.rs` was written. A pixel is `1/1024` rather than `1/64`. At `1/64` the residue of a length reached two ways was one step, and one step was 0.016 px -- enough to move a box. `PX_SHIFT` and `REL_SHIFT` are the only statement of the grid now, and the shader's copy is prepended from them rather than written twice. Checked: fmt, clippy, 102 tests, 100 generated seeds in 75 s, all five shrinker cases at 300 seeds, and `tabs`, `view`, `minimal`, `text` and `random` byte-identical at 1920x1200. What the fuzzers ask for is now a step, not a twentieth of a pixel: the shrinker's five cases agree within one (`resize` exactly), and the oracle's two-operation cases within two. The residue is a single rounding either way -- it scales with the grid rather than accumulating, which is why it is a thousandth of a pixel now. Closing it means one way of asking how long a box is, rather than a chain composed down and a length measured against the window; that is a bigger change than this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
596 lines
21 KiB
Rust
596 lines
21 KiB
Rust
//! A property test that shrinks 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.
|
|
//!
|
|
//! 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.
|
|
|
|
use iris::harness::Harness;
|
|
use iris::prelude::*;
|
|
use iris::random::{Branch, Rng};
|
|
|
|
/// 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.",
|
|
];
|
|
|
|
const ONE_LINE: &str = "one line, overflowing whatever it is given";
|
|
|
|
const OUTER: (f32, f32) = (1920.0, 1200.0);
|
|
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<Node>, Vec<usize>),
|
|
Stack(Vec<Node>),
|
|
Pad(f32, Box<Node>),
|
|
Aligned(u8, u8, Box<Node>),
|
|
Sized(Option<Len>, Option<Len>, Box<Node>),
|
|
Scroll(bool, Box<Node>),
|
|
Branch(Box<Node>, Box<Node>, Box<Node>, f32),
|
|
}
|
|
|
|
fn axis_align(v: u8) -> Option<AxisAlign> {
|
|
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<WidgetId>,
|
|
spans: &mut Vec<WeakWidget<Span>>,
|
|
sized: &mut Vec<WidgetId>,
|
|
) -> 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),
|
|
ortho: match down {
|
|
true => OrthoSize::Children,
|
|
false => OrthoSize::Full,
|
|
},
|
|
}
|
|
.add(&mut h.rsc);
|
|
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<Len>, Option<Len>)>) {
|
|
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<Node> {
|
|
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(Len::px(20.0 + rng.below(180) as f32)),
|
|
1 => Some(Len::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(Len::px(20.0 + rng.below(180) as f32)),
|
|
1 => Some(Len::LEFTOVER),
|
|
2 => Some(Len::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<Len>) -> Option<Len> {
|
|
let half = Rel::from_f32(0.5);
|
|
len.map(|len| Len {
|
|
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::<Vec<_>>();
|
|
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<String> {
|
|
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 one step of the grid. A move or a resize lands on the same
|
|
// number now; a length measured one way and composed another can
|
|
// still be a step apart.
|
|
let same = match (got, want) {
|
|
(Some(g), Some(c)) => {
|
|
let d = |a: Px, b: Px| (a - b).abs() <= Px::STEP;
|
|
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 {
|
|
loop {
|
|
let Some(next) = node
|
|
.smaller()
|
|
.into_iter()
|
|
.find(|small| diverges(small, case).is_some())
|
|
else {
|
|
return node;
|
|
};
|
|
node = next;
|
|
}
|
|
}
|
|
|
|
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
std::env::var(name)
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(fallback)
|
|
}
|
|
|
|
#[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,
|
|
};
|
|
|
|
for seed in 1..=seeds {
|
|
let node = grow(&mut Rng::new(seed), depth);
|
|
let Some(how) = diverges(&node, case) else {
|
|
continue;
|
|
};
|
|
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");
|
|
}
|
|
let sizes: Vec<usize> = (1..=seeds)
|
|
.map(|seed| grow(&mut Rng::new(seed), depth).size())
|
|
.collect();
|
|
let total: usize = sizes.iter().sum();
|
|
println!(
|
|
"{seeds} trees at depth {depth} agree: {} widgets total, largest {}",
|
|
total,
|
|
sizes.iter().max().copied().unwrap_or(0)
|
|
);
|
|
}
|