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 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-15 00:27:41 -04:00
1 parent 1b1378b05a
commit 386a0d1b8f
1 file changed
+71 -4
+71 -4
View File
@@ -16,7 +16,20 @@ use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow}; 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<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; 7] = [1, 2, 3, 5, 8, 13, 98]; const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98];
const REGION_EPSILON_PX: f32 = 0.05; const REGION_EPSILON_PX: f32 = 0.05;
@@ -38,7 +51,7 @@ fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
} }
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree { 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.state.root = Some(root);
h.frame(); h.frame();
tree tree
@@ -158,6 +171,53 @@ fn reshuffle(
(edits, detached) (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<Len>| match l {
Some(l) => format!("{l}"),
None => "-".into(),
};
if let Some(w) = any.downcast_ref::<SetSize>() {
return format!("SetSize{{x:{},y:{}}}", len(&w.x), len(&w.y));
}
if let Some(w) = any.downcast_ref::<Span>() {
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::<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::<Aligned>() {
let a = |v: Option<AxisAlign>| 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::<Stack>() {
return format!("Stack{{n:{}}}", w.children.len());
}
label
}
/// Every widget in one tree against the matching widget in the other. A /// 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, /// mismatch prints the widget's ancestry, marking the ones that own a slot,
/// since where two trees disagree is rarely where the cause is. /// 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 => "", true => "",
false => "*", false => "*",
}; };
chain.push(format!("{}{slot}", wh.rsc.widgets().label(id))); chain.push(format!("{}{slot}", describe(id, wh)));
at = active.parent; at = active.parent;
} }
println!( println!(
@@ -202,6 +262,10 @@ fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness,
fn changed_size(seed: u64) { fn changed_size(seed: u64) {
let mut warm = Harness::new((900, 1200)); let mut warm = Harness::new((900, 1200));
let grown = plant(&mut warm, seed, &Edits::default()); 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 mut rng = Rng::new(seed ^ 0x5eed);
let sizes = edit(&mut warm, &grown, &mut rng); let sizes = edit(&mut warm, &grown, &mut rng);
@@ -320,6 +384,9 @@ fn resized(seed: u64) {
fn resized_then_changed(seed: u64) { fn resized_then_changed(seed: u64) {
let mut warm = Harness::new((1920, 1200)); let mut warm = Harness::new((1920, 1200));
let grown = plant(&mut warm, seed, &Edits::default()); let grown = plant(&mut warm, seed, &Edits::default());
if grown.sized.is_empty() {
return;
}
warm.resize((640, 900)); warm.resize((640, 900));
warm.frame(); warm.frame();
@@ -401,7 +468,7 @@ fn a_long_run_of_seeds_agrees() {
.ok() .ok()
.and_then(|seed| seed.parse().ok()) .and_then(|seed| seed.parse().ok())
.map(|seed| seed..=seed) .map(|seed| seed..=seed)
.unwrap_or(1..=100); .unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100));
for seed in seeds { for seed in seeds {
changed_size(seed); changed_size(seed);
changed_every_size(seed); changed_every_size(seed);