Files
iris/tests/generated.rs
T
iris-aiandClaude Opus 5 cdec29351a Grow scrolling into the random trees
Scrolling is the one thing in these trees that reads the pixel length of its
box, and the one that hands its child a box longer than its own, so a warm
layout under it has to be rebuilt where the rest can be carried over. A
sixth of the nodes at each level is now a scroll over a subtree, on either
axis.

Four of a hundred seeds now grow nothing but wrappers, so `reshuffled`
returns early where there is no span to shuffle: a case with nothing to do
is not the same as a shuffle that had no effect, which is what the assertion
below it is for.

50 tests, and the ignored sweep over 100 seeds and eight scenarios, 800
comparisons.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:35:20 -04:00

311 lines
10 KiB
Rust

//! Random trees, checked against building the same tree cold.
//!
//! A frame reaches its layout by keeping most of the last one: slots
//! rewritten, some widgets drawn again, the rest untouched. The property here
//! is that what comes out is the tree a cold start would have produced, so
//! anything the retained path carried over that it should not have shows up
//! as a difference in somebody's box.
//!
//! `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.
use std::collections::HashMap;
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
const DEPTH: usize = 4;
const SEEDS: [u64; 6] = [1, 2, 3, 5, 8, 13];
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree {
let (root, tree) = grow(&mut h.rsc, seed, DEPTH, edits);
h.state.root = Some(root);
h.frame();
tree
}
/// 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<usize, Lens> {
let mut edits = HashMap::new();
for _ in 0..4 {
let idx = rng.below(tree.sized.len());
let lens = [
Some(Len::abs(20.0 + rng.below(180) as f32)),
Some(Len::abs(20.0 + rng.below(180) as f32)),
];
edits.insert(idx, lens);
let sized = &mut h.rsc[tree.sized[idx]];
sized.x = lens[0];
sized.y = lens[1];
}
edits
}
/// A way of changing what a span holds. Each is a shape worth its own case:
/// taking a child out of the middle is not the same as emptying a span, and
/// adding one is not the same as adding three.
#[derive(Clone, Copy, Debug)]
enum Shuffle {
/// Every other child, so what is left is interleaved with what went.
EveryOther,
/// Everything but the first, which is the last step before empty.
AllButFirst,
/// Three more on the end at once.
AddThree,
/// The first out and three more on, so the count moves both ways.
SwapForThree,
/// One out of the middle and one on the end.
TradeOne,
}
const SHUFFLES: [Shuffle; 5] = [
Shuffle::EveryOther,
Shuffle::AllButFirst,
Shuffle::AddThree,
Shuffle::SwapForThree,
Shuffle::TradeOne,
];
impl Shuffle {
fn of(self, grown: usize) -> SpanEdit {
let all = |step: usize, from: usize| (from..grown).step_by(step).collect();
match self {
Self::EveryOther => SpanEdit {
detach: all(2, 0),
attach: 0,
},
Self::AllButFirst => SpanEdit {
detach: all(1, 1),
attach: 0,
},
Self::AddThree => SpanEdit {
detach: Vec::new(),
attach: 3,
},
Self::SwapForThree => SpanEdit {
detach: vec![0],
attach: 3,
},
Self::TradeOne => SpanEdit {
detach: vec![grown / 2],
attach: 1,
},
}
}
}
/// Applies `shuffle` to every third span, and says what it did so the cold
/// tree can be grown that way. The widgets it takes out are given back: the
/// last share of one must outlive the comparison, or its id is handed to
/// something else and the two trees stop lining up.
fn reshuffle(
h: &mut Harness,
tree: &mut Tree,
shuffle: Shuffle,
) -> (HashMap<usize, SpanEdit>, Vec<StrongWidget>) {
let mut edits = HashMap::new();
let mut detached = Vec::new();
for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) {
let span_edit = shuffle.of(span.grown);
let mut take = span_edit.detach.clone();
take.sort_unstable();
let children = &mut h.rsc[span.id].children;
// Highest first, so an index means the same child however many of
// its neighbours are going too.
for j in take.into_iter().rev() {
if j < children.len() {
detached.push(children.remove(j));
}
}
let attach = span_edit.attach.min(span.spares.len());
children.extend(span.spares.drain(..attach));
edits.insert(idx, span_edit);
}
(edits, detached)
}
/// Every widget in one tree against the matching widget in the other. A
/// mismatch prints the widget's ancestry, marking the ones that own a slot,
/// since where two trees disagree is rarely where the cause is.
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());
if 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 slot = match active.move_idx == active.parent_move {
true => "",
false => "*",
};
chain.push(format!("{}{slot}", wh.rsc.widgets().label(id)));
at = active.parent;
}
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());
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));
}
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.
if grown.spans.is_empty() {
return;
}
let before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect();
let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle);
warm.frame();
// Or the two trees would agree for want of anything having happened.
let after = grown.ids.iter().map(|id| warm.region(id));
let moved = before.iter().zip(after).filter(|(a, b)| *a != b).count();
assert!(moved > 0, "seed {seed}: {shuffle:?} changed nothing");
let mut cold = Harness::new((900, 1200));
let same = plant(
&mut cold,
seed,
&Edits {
spans,
..Default::default()
},
);
let what = format!("{shuffle:?}");
assert_same(seed, &what, (&warm, &grown), (&cold, &same));
}
fn resized(seed: u64) {
let mut warm = Harness::new((1920, 1200));
let grown = plant(&mut warm, seed, &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());
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_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);
}
#[test]
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
for shuffle in SHUFFLES {
for seed in SEEDS {
reshuffled(seed, shuffle);
}
}
}
/// Reproduces a divergence that predates the position chain: laying a tree out
/// again does not always land where growing it cold does.
///
/// Every one seen so far is a wrapping text on a span's *own* axis, where the
/// two draws do not agree. The span measures the child in the whole box, the
/// child shapes to that width and reports the width it used, the span then
/// places it in exactly that width -- which is a length change, so the child
/// shapes again, and its longest line is shorter than the box it was just
/// given. Each pass narrows it, so where the tree ends up depends on how many
/// passes it has had, and a warm tree has had a different number from a cold
/// one. Layout is supposed to be a function of the state alone.
///
/// A span whose axis is not the wrap axis is stable, which is every real
/// column of text, and why nothing else has run into this.
///
/// 7 of these 90 diverge on `db1751f`, before the chain; 30 do with it, since
/// a placed child reaches the second shaping more often. Both numbers are the
/// same defect, and it wants fixing where the two draws meet -- LAYOUT.md §4 --
/// rather than anywhere in the chain.
#[test]
#[ignore = "a hundred seeds, rather than the six the others check"]
fn a_long_run_of_seeds_agrees() {
for seed in 1..=100 {
changed_size(seed);
resized(seed);
resized_then_changed(seed);
for shuffle in SHUFFLES {
reshuffled(seed, shuffle);
}
}
}