From 386a0d1b8f73cb46f91c6bedc2c37cd6cf616d62 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Tue, 15 Sep 2026 00:27:41 -0400 Subject: [PATCH] Steer the fuzzer, and print enough of a failure to rebuild it by hand `DEPTH` was a constant at 4, and the generator widens two to four ways per level, so raising it buys overlap between dependency paths rather than ancestry. `IRIS_GENERATED_DEPTH` and `IRIS_GENERATED_SEEDS` select the load; the default is what it was. Depth 4 was hiding divergences. At depth 5 and beyond the sweep fails on the tree as it stands, with no `Branch` node and every span filling across its axis, so it is neither of the things I suspected -- it predates both. A failure printed a chain of type names, which is not enough to write the tree out again, and hand-reconstruction from one has failed three times now. `describe` prints what each ancestor was configured with, so a run says `Text < SetSize{x:34 px;} < Aligned{x:neg,y:pos} < SetSize{x:35 px;} < Stack{n:2}` and the fast test that replaces the seed can be built from that. `Widget: Any`, so this needs no new plumbing. Two fixtures assumed every tree grows a declared size to change, and one assumed a span it shuffles is drawn -- a span behind a branch nobody took is not. Both are vacuous seeds rather than failures. Co-Authored-By: Claude Opus 5 --- tests/generated.rs | 75 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/tests/generated.rs b/tests/generated.rs index e77f99e..df0cb6f 100644 --- a/tests/generated.rs +++ b/tests/generated.rs @@ -16,7 +16,20 @@ use iris::harness::Harness; use iris::prelude::*; use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow}; -const DEPTH: usize = 4; +/// 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; 7] = [1, 2, 3, 5, 8, 13, 98]; const REGION_EPSILON_PX: f32 = 0.05; @@ -38,7 +51,7 @@ fn same_region(got: Option, want: Option) -> bool { } fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree { - let (root, tree) = grow(&mut h.rsc, seed, DEPTH, edits); + let (root, tree) = grow(&mut h.rsc, seed, depth(), edits); h.state.root = Some(root); h.frame(); tree @@ -158,6 +171,53 @@ fn reshuffle( (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 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; + let len = |l: &Option| match l { + Some(l) => format!("{l}"), + None => "-".into(), + }; + if let Some(w) = any.downcast_ref::() { + return format!("SetSize{{x:{},y:{}}}", len(&w.x), len(&w.y)); + } + if let Some(w) = any.downcast_ref::() { + let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" }; + return format!( + "Span{{dir:{:?}{sign},gap:{},n:{}}}", + w.dir.axis, + w.gap, + 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 the ones that own a slot, /// since where two trees disagree is rarely where the cause is. @@ -186,7 +246,7 @@ fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, true => "", false => "*", }; - chain.push(format!("{}{slot}", wh.rsc.widgets().label(id))); + chain.push(format!("{}{slot}", describe(id, wh))); at = active.parent; } println!( @@ -202,6 +262,10 @@ fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, 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); @@ -320,6 +384,9 @@ fn resized(seed: u64) { 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(); @@ -401,7 +468,7 @@ fn a_long_run_of_seeds_agrees() { .ok() .and_then(|seed| seed.parse().ok()) .map(|seed| seed..=seed) - .unwrap_or(1..=100); + .unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100)); for seed in seeds { changed_size(seed); changed_every_size(seed);