Traced what was left of the warm-against-cold difference after fixed point. It is not accumulation and not one place: it is the same box reached two ways, and each way rounds where the other does not. `Scroll` was writing a box it had been given back out as its own length in pixels. That is the same box in another form, and centring a part in `rel 1` lands a step from centring it in `px 900`, because halving a difference is not halving each part of it. Content that fills the viewport and has not been scrolled is now handed back as it came, which makes the shrinker's `repaint` and `resize-repaint` cases agree exactly rather than within a step. `Span` placed each child a step from where the last one ended, so the rounding of every share was carried along the row. A position is now the fixed parts before it -- a sum, exact -- plus one rounded share of the room. Measured: two hundred equal shares of a 1000 px row ended at 999.999 and now end at 1000, and `tests/cases/layout.rs` pins it at 2, 3, 7, 64 and 200. What is left is a step per level of nesting between the two ways, which is what the fuzzers now allow: four of the five shrinker cases pass at one step and the fifth is five spans deep. Closing it needs one way of asking where a box is, which is a bigger change than this. Checked: fmt, clippy, 103 tests, all five shrinker cases at 300 seeds, 100 generated seeds, five examples byte-identical at 1920x1200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
620 lines
22 KiB
Rust
620 lines
22 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);
|
|
/// 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<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),
|
|
}
|
|
.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(Len::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<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 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 {
|
|
loop {
|
|
let Some(next) = node
|
|
.smaller()
|
|
.into_iter()
|
|
.find(|small| diverges(small, case).is_some())
|
|
else {
|
|
return node;
|
|
};
|
|
node = next;
|
|
}
|
|
}
|
|
|
|
/// 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<u64>, 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<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,
|
|
};
|
|
|
|
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");
|
|
});
|
|
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)
|
|
);
|
|
}
|