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:
1 parent
1b1378b05a
commit
386a0d1b8f
1 file changed
+71
-4
+71
-4
@@ -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<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 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 {
|
||||
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<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
|
||||
/// 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);
|
||||
|
||||
Reference in new issue
Block a user