Link the ordinary tests once, and keep their debug info to line tables
Eleven `tests/*.rs` were eleven binaries, each linking the whole graph -- `wgpu` and all -- to run a handful of cases. They are modules of one target now, under `tests/cases/`, and `cargo test --test suite layout::` still picks one out. The fuzzers and the `*_cost` measurements stay their own targets: they are run on their own and want to be selectable without building the rest. `profile.test` takes `debug = "line-tables-only"`, which is what a backtrace here actually reads; the type and variable information was the bulk of what the linker was writing. Measured on this machine, rebuilding `iris`'s test targets after a change to the crate: 14.3 s before, 9.8 s with one target, 7.7 s with both. `target/` went from 45 GB to 13 GB. The suite still passes 102 tests, and the binary still carries `.debug_line`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
39e4ca20e6
commit
cb955f1023
13 files changed
+35
No files matched your search
@@ -0,0 +1,405 @@
|
||||
//! The smallest trees that laid out differently warm than cold, each shrunk
|
||||
//! by `tests/shrink.rs` from hundreds of widgets. The first two are a cold
|
||||
//! frame that had not settled: a wrapping text shaped at a width it was
|
||||
//! measured in rather than the one it was given. The rest are a widget
|
||||
//! measured again in a box its own answer had decided, where the old answer
|
||||
//! is a fixed point whatever the content now says. The last is neither: one
|
||||
//! box length, composed two ways, landing either side of the boundary that
|
||||
//! decided whether a child was drawn at all.
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
|
||||
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
||||
/// the tree changes -- every widget is marked for redraw and the frame is
|
||||
/// taken again -- so no box may move, and a warm frame has to land where a
|
||||
/// cold one does.
|
||||
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
||||
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
||||
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
||||
let sized = wrapped.width(76).add(&mut h.rsc);
|
||||
let aligned = sized;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
||||
let stack = Stack {
|
||||
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
vec![
|
||||
plain.id(),
|
||||
wrapped.id(),
|
||||
sized.id(),
|
||||
aligned.id(),
|
||||
stack.id(),
|
||||
root.id(),
|
||||
]
|
||||
}
|
||||
|
||||
/// The first frame does not reach the layout a second one does, so "cold" is
|
||||
/// not a fixed point and comparing against it compares against a tree that
|
||||
/// has not settled.
|
||||
#[test]
|
||||
fn one_frame_is_enough() {
|
||||
let mut h = Harness::new((640, 900));
|
||||
let ids = plant(&mut h);
|
||||
let first = h.region(&ids[1]).unwrap();
|
||||
for _ in 0..3 {
|
||||
for &id in &ids {
|
||||
h.rsc.widgets_mut().get_dyn_mut(id);
|
||||
}
|
||||
h.frame();
|
||||
}
|
||||
let settled = h.region(&ids[1]).unwrap();
|
||||
println!(
|
||||
"first frame {} tall, settled {} tall",
|
||||
first.bot_right.y - first.top_left.y,
|
||||
settled.bot_right.y - settled.top_left.y
|
||||
);
|
||||
assert_eq!(
|
||||
first.bot_right.y - first.top_left.y,
|
||||
settled.bot_right.y - settled.top_left.y,
|
||||
"the first frame had not finished laying out"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repainting_everything_moves_nothing() {
|
||||
let mut warm = Harness::new((640, 900));
|
||||
let ids = plant(&mut warm);
|
||||
for &id in &ids {
|
||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||
}
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let cold_ids = plant(&mut cold);
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
|
||||
/// is the same size whatever the output is, so a resize may not change any of
|
||||
/// it -- but the text comes out 3.92px narrower warm than cold.
|
||||
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||
let aligned = text;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
||||
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||
vec![
|
||||
text.id(),
|
||||
aligned.id(),
|
||||
inner.id(),
|
||||
sized.id(),
|
||||
filler.id(),
|
||||
root.id(),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
|
||||
let mut warm = Harness::new((1920, 1200));
|
||||
let ids = plant_fixed(&mut warm);
|
||||
warm.frame();
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let cold_ids = plant_fixed(&mut cold);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
|
||||
/// moving them, cold by growing them that way. Same widgets, same sizes, one
|
||||
/// ends up 29.9px from where the other does.
|
||||
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
|
||||
let wrapped = wtext("Wrapping shapes one source into as many lines")
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.add(&mut h.rsc);
|
||||
let plain = wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
.wrap(false)
|
||||
.add(&mut h.rsc);
|
||||
let first: StrongWidget = wrapped.add_strong(&mut h.rsc);
|
||||
let second: StrongWidget = plain.add_strong(&mut h.rsc);
|
||||
let children = match swapped {
|
||||
true => vec![second, first],
|
||||
false => vec![first, second],
|
||||
};
|
||||
let span = Span {
|
||||
children,
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let span_handle = span;
|
||||
let aligned = span;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(span, Axis::X, AxisAlign::CENTER);
|
||||
h.state.root = Some(aligned.add_strong(&mut h.rsc));
|
||||
(
|
||||
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
|
||||
span_handle,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swapping_two_children_lands_where_growing_them_that_way_does() {
|
||||
let mut warm = Harness::new((640, 900));
|
||||
let (ids, span) = plant_pair(&mut warm, false);
|
||||
warm.frame();
|
||||
warm.rsc[span].children.rotate_left(1);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let (cold_ids, _) = plant_pair(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
|
||||
/// content from what the content says, and hands that box down through a
|
||||
/// pass-through; the span under it was given that box once, so nothing at its
|
||||
/// own edge says the box was its own answer.
|
||||
fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
||||
let words = "Wrapping shapes one source into as many lines as the box leaves room for,";
|
||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||
let mut inner_children: Vec<StrongWidget> =
|
||||
vec![text.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)];
|
||||
if swapped {
|
||||
inner_children.rotate_left(1);
|
||||
}
|
||||
let inner = Span {
|
||||
children: inner_children,
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let block = rect(Color::RED).add(&mut h.rsc);
|
||||
let fixed = block.width(87).add(&mut h.rsc);
|
||||
let mut outer_children: Vec<StrongWidget> =
|
||||
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
||||
if swapped {
|
||||
outer_children.rotate_left(1);
|
||||
}
|
||||
let outer = Span {
|
||||
children: outer_children,
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
// Carried no rule even before rules were a property: it is here to be a
|
||||
// widget between the span and the scroll, not to declare anything.
|
||||
let through = (outer,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let scroll = Scroll::new(through.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||
h.state.root = Some(scroll.add_strong(&mut h.rsc));
|
||||
(
|
||||
vec![
|
||||
text.id(),
|
||||
filler.id(),
|
||||
inner.id(),
|
||||
block.id(),
|
||||
fixed.id(),
|
||||
outer.id(),
|
||||
through.id(),
|
||||
scroll.id(),
|
||||
],
|
||||
[inner, outer],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() {
|
||||
let mut warm = Harness::new((640, 900));
|
||||
let (ids, spans) = plant_scrolled(&mut warm, false);
|
||||
warm.frame();
|
||||
for span in spans {
|
||||
warm.rsc[span].children.rotate_left(1);
|
||||
}
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let (cold_ids, _) = plant_scrolled(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Reports a width derived from the box it is asked in. Reading through the
|
||||
/// painter is its declaration that the answer holds for that width only.
|
||||
struct Wider {
|
||||
extra: f32,
|
||||
}
|
||||
|
||||
impl Widget for Wider {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
Size {
|
||||
x: Len {
|
||||
px: painter.px_len(Axis::X) + Px::from_f32(self.extra),
|
||||
..Len::ZERO
|
||||
},
|
||||
y: Len::LEFTOVER,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
|
||||
let content = Wider { extra }.add(&mut h.rsc);
|
||||
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||
let root = scroll;
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(scroll, Axis::X, AxisAlign::NEG);
|
||||
h.set_root(root);
|
||||
(content, scroll.id())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scrolls_retained_answer_is_the_one_a_cold_layout_asks_for() {
|
||||
let mut warm = Harness::new((100, 100));
|
||||
let (content, scroll) = plant_wider(&mut warm, 50.0);
|
||||
warm.rsc[content].extra = 70.0;
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((100, 100));
|
||||
let (_, cold_scroll) = plant_wider(&mut cold, 70.0);
|
||||
|
||||
assert_eq!(warm.region(&scroll), cold.region(&cold_scroll));
|
||||
}
|
||||
|
||||
/// Six widgets, shrunk from 266. `measured`'s box is exactly the height of its
|
||||
/// one fixed child, which is the box a parent sizing itself from that answer
|
||||
/// hands back -- so whether its leftover-only child was drawn at all came down
|
||||
/// to the 0.00003 px the composed length differs by, one way warm and the
|
||||
/// other cold.
|
||||
fn plant_boundary(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||
let plain = wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
.wrap(false)
|
||||
.add(&mut h.rsc);
|
||||
let mut pair: Vec<StrongWidget> =
|
||||
vec![filler.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
|
||||
if swapped {
|
||||
pair.rotate_left(1);
|
||||
}
|
||||
let measured = Span {
|
||||
children: pair,
|
||||
dir: Dir::DOWN,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
// Takes the whole box on its own, so the span above has nothing left to
|
||||
// divide and `measured` is given exactly the text's height.
|
||||
let whole = rect(Color::RED).add(&mut h.rsc);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(whole, None, Some(Len::rel(1.0)));
|
||||
let mut inner_children: Vec<StrongWidget> = vec![
|
||||
measured.add_strong(&mut h.rsc),
|
||||
whole.add_strong(&mut h.rsc),
|
||||
];
|
||||
if swapped {
|
||||
inner_children.rotate_left(1);
|
||||
}
|
||||
let inner = Span {
|
||||
children: inner_children,
|
||||
dir: Dir::DOWN,
|
||||
gap: Px::ZERO,
|
||||
ortho: OrthoSize::Children,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(inner, None, Some(Len::px(198.0)));
|
||||
// One more span above it: without a box composed through it, both trees
|
||||
// round the same way and the boundary is never crossed.
|
||||
let outer = (inner,).span(Dir::DOWN).add(&mut h.rsc);
|
||||
h.set_root(outer);
|
||||
(
|
||||
vec![
|
||||
filler.id(),
|
||||
plain.id(),
|
||||
measured.id(),
|
||||
whole.id(),
|
||||
inner.id(),
|
||||
outer.id(),
|
||||
],
|
||||
[measured, inner],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
|
||||
let mut warm = Harness::new((640, 900));
|
||||
let (ids, spans) = plant_boundary(&mut warm, false);
|
||||
warm.frame();
|
||||
for span in spans {
|
||||
warm.rsc[span].children.rotate_left(1);
|
||||
}
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let (cold_ids, _) = plant_boundary(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
Reference in new issue
Block a user