565 lines
18 KiB
Rust
565 lines
18 KiB
Rust
//! Random trees, checked against building the same tree cold.
|
|
//!
|
|
//! 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.
|
|
//!
|
|
//! `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::{Aligns, Edits, Lens, Rng, SpanEdit, Tree, grow};
|
|
|
|
/// 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
|
|
/// reachable by raising this -- it buys more overlap between dependency
|
|
/// paths, not more ancestry.
|
|
fn depth() -> usize {
|
|
env("IRIS_GENERATED_DEPTH", 4)
|
|
}
|
|
|
|
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
std::env::var(name)
|
|
.ok()
|
|
.and_then(|value| value.parse().ok())
|
|
.unwrap_or(fallback)
|
|
}
|
|
const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98];
|
|
const REGION_EPSILON_PX: f32 = 0.05;
|
|
|
|
fn same_coordinate(got: f32, want: f32) -> bool {
|
|
(got - want).abs() <= REGION_EPSILON_PX
|
|
}
|
|
|
|
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
|
match (got, want) {
|
|
(Some(got), Some(want)) => {
|
|
same_coordinate(got.top_left.x, want.top_left.x)
|
|
&& same_coordinate(got.top_left.y, want.top_left.y)
|
|
&& same_coordinate(got.bot_right.x, want.bot_right.x)
|
|
&& same_coordinate(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(Len::px(20.0 + rng.below(180) as f32)),
|
|
Some(Len::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<usize, Lens> {
|
|
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<usize, Lens> {
|
|
(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<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)
|
|
}
|
|
|
|
/// 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.known() {
|
|
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::<Span>() {
|
|
let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" };
|
|
return format!(
|
|
"Span{{dir:{:?}{sign},gap:{},ortho:{:?},n:{}}}",
|
|
w.dir.axis,
|
|
w.gap,
|
|
w.ortho,
|
|
w.children.len()
|
|
);
|
|
}
|
|
if let Some(w) = any.downcast_ref::<Pad>() {
|
|
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::<Stack>() {
|
|
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;
|
|
}
|
|
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<usize, bool> = (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);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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"]
|
|
fn a_long_run_of_seeds_agrees() {
|
|
let seeds = 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));
|
|
for seed in seeds {
|
|
changed_size(seed);
|
|
changed_every_size(seed);
|
|
repainted_together(seed);
|
|
resized(seed);
|
|
resized_then_changed(seed);
|
|
for shuffle in SHUFFLES {
|
|
reshuffled(seed, shuffle);
|
|
}
|
|
}
|
|
}
|