Branch on a measurement, so a wrong one shows as a different tree
Comparing boxes catches a widget that moved. It does not catch one that measured a child, was handed an answer a cold start would not have given, and took the other branch -- the same defect, arriving where a pixel comparison cannot see it. Branching on what the painter tells you is something a widget is allowed to do, so the library owes the same answer warm and cold; only a widget changing its own configuration is exempt. `random::Branch` measures a child and draws one of two others on the result, with both grown either way so the ids match whichever is drawn. It joins the generator, which makes every existing scenario a control-flow oracle as well as a geometric one. `tests/determinism.rs` is the same widget by hand across eight thresholds, including either side of the answer, and is the fast check -- the sweep is a fuzzer and confirms at the end rather than being iterated against. A span behind a branch nobody took is not drawn, so shuffling it cannot move anything; `reshuffled` now treats that as vacuous, the way it already treats a tree with no spans, rather than as a shuffle that had no effect. Both new tests pass, and the sweep passes at depth 4 and 5 over 200 seeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60175c3821
commit
1b1378b05a
3 files changed
+156
-2
No files matched your search
@@ -0,0 +1,101 @@
|
||||
//! A measurement that decides control flow.
|
||||
//!
|
||||
//! Comparing boxes catches a widget that moved. It does not catch a widget
|
||||
//! that measured a child, believed a different answer from the one a cold
|
||||
//! start would give, and took the other branch -- which is the same defect
|
||||
//! arriving somewhere it cannot be ignored. A widget here branches on what it
|
||||
//! measured, so a disagreement shows up as a different tree.
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
|
||||
/// Measures `probe` across `axis` and draws one of two children on the
|
||||
/// answer. Its own configuration never changes, so which child is drawn is a
|
||||
/// property of the layout alone.
|
||||
struct BranchesOnMeasurement {
|
||||
probe: StrongWidget,
|
||||
wide: StrongWidget,
|
||||
narrow: StrongWidget,
|
||||
threshold: f32,
|
||||
}
|
||||
|
||||
impl Widget for BranchesOnMeasurement {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let mut top = UiRegion::FULL;
|
||||
top.y.end = top.y.start.offset(40.0);
|
||||
let measured = painter.place(&self.probe, top).len(Axis::X);
|
||||
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
|
||||
|
||||
let mut rest = UiRegion::FULL;
|
||||
rest.y.start = rest.y.start.offset(40.0);
|
||||
match px > self.threshold {
|
||||
true => painter.place(&self.wide, rest),
|
||||
false => painter.place(&self.narrow, rest),
|
||||
};
|
||||
Size::REST
|
||||
}
|
||||
}
|
||||
|
||||
fn plant(h: &mut Harness, threshold: f32) -> (WidgetId, WidgetId) {
|
||||
let words = "the quick brown fox jumps over the lazy dog and keeps running";
|
||||
let probe = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||
let wide = rect(Color::RED).add(&mut h.rsc);
|
||||
let narrow = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let branch = BranchesOnMeasurement {
|
||||
probe: probe.add_strong(&mut h.rsc),
|
||||
wide: wide.add_strong(&mut h.rsc),
|
||||
narrow: narrow.add_strong(&mut h.rsc),
|
||||
threshold,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let side = rect(Color::GREEN).width(120).add(&mut h.rsc);
|
||||
h.set_root((side, branch).span(Dir::RIGHT));
|
||||
(wide.id(), narrow.id())
|
||||
}
|
||||
|
||||
/// Which of the two branches drew, as a pair a test can compare.
|
||||
fn taken(h: &Harness, wide: WidgetId, narrow: WidgetId) -> (bool, bool) {
|
||||
(h.region(&wide).is_some(), h.region(&narrow).is_some())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_branch_taken_on_a_measurement_holds_across_repaints() {
|
||||
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
|
||||
let mut h = Harness::new((900, 600));
|
||||
let (wide, narrow) = plant(&mut h, threshold);
|
||||
let first = taken(&h, wide, narrow);
|
||||
assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
|
||||
|
||||
for frame in 0..4 {
|
||||
h.rsc.widgets_mut().get_dyn_mut(wide);
|
||||
h.rsc.widgets_mut().get_dyn_mut(narrow);
|
||||
h.frame();
|
||||
assert_eq!(
|
||||
taken(&h, wide, narrow),
|
||||
first,
|
||||
"threshold {threshold}, repaint {frame}: the branch moved when nothing did"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
|
||||
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
|
||||
let mut warm = Harness::new((900, 600));
|
||||
let (wide, narrow) = plant(&mut warm, threshold);
|
||||
warm.resize((640, 480));
|
||||
warm.frame();
|
||||
warm.rsc.widgets_mut().get_dyn_mut(wide);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 480));
|
||||
let (cwide, cnarrow) = plant(&mut cold, threshold);
|
||||
|
||||
assert_eq!(
|
||||
taken(&warm, wide, narrow),
|
||||
taken(&cold, cwide, cnarrow),
|
||||
"threshold {threshold}: warm and cold took different branches"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user