//! 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::{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(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, want: Option) -> 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 { 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 { (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, Vec) { 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(), }; // A size rule is a property of whatever carries it, so it prints with // that widget rather than as one of its own. match (rules.x, rules.y) { (SizeRule::Free, SizeRule::Free) => describe_widget(id, h), (x, y) => format!("{}[x:{},y:{}]", describe_widget(id, h), rule(x), rule(y)), } } 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::() { 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::() { 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::() { let a = |v: Option| match v { None => "-", Some(AxisAlign::Neg) => "neg", Some(AxisAlign::Center) => "mid", Some(AxisAlign::Pos) => "pos", }; return format!("Aligned{{x:{},y:{}}}", a(w.align.x), a(w.align.y)); } if let Some(w) = any.downcast_ref::() { 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)); } 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 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); } } }