Change `Harness::run` to run until no more repaints are requested (#5580)

Previously, `Harness::run` just called `Harness::step` 3 times. If that
wasn't enough, tests would often call run multiple times so all
animations would finish properly.

Also, I introduced `HarnessBuilder::with_step_dt` to customize with how
big of a dt each frame is called. I set the default to 1.0 / 6.0 (~6fps)
so we don't waste cpu in tests waiting on animations.

`HarnessBuilder::max_steps` allows us to control how many steps
`Harness::run` should run before panicing.
The default is 6, so we run for up to 1.0 logical seconds (six frames at
6 fps), which should be enough to finish most animations.

Turns out a lot of snapshots where rendered before fully shown and had a
light opacity, those are now fixed.

* [x] I have followed the instructions in the PR template
This commit is contained in:
lucasmerlin 2025-01-07 08:33:44 +01:00 committed by GitHub
parent 35860418ac
commit 52060c0c41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 230 additions and 97 deletions

View File

@ -775,7 +775,7 @@ impl Context {
writer(&mut self.0.write())
}
/// Run the ui code for one 1.
/// Run the ui code for one frame.
///
/// At most [`Options::max_passes`] calls will be issued to `run_ui`,
/// and only on the rare occasion that [`Context::request_discard`] is called.

View File

@ -49,7 +49,7 @@ fn test_demo_app() {
// Load a local image where we know it exists and loads quickly
#[cfg(feature = "image_viewer")]
Anchor::ImageViewer => {
harness.run();
harness.step();
harness
.get_by_role_and_label(Role::TextInput, "URI:")
@ -65,7 +65,8 @@ fn test_demo_app() {
_ => {}
}
harness.run();
// Can't use Harness::run because fractal clock keeps requesting repaints
harness.run_steps(2);
if let Err(e) = harness.try_snapshot(&anchor.to_string()) {
results.push(e);

View File

@ -397,7 +397,7 @@ mod tests {
harness.set_size(Vec2::new(size.width as f32, size.height as f32));
// Run the app for some more frames...
harness.run();
harness.run_ok();
let mut options = SnapshotOptions::default();
// The Bézier Curve demo needs a threshold of 2.1 to pass on linux

View File

@ -183,12 +183,13 @@ mod tests {
harness.get_by_role(Role::ComboBox).click();
harness.run();
// Harness::run would fail because we keep requesting repaints to simulate progress.
harness.run_ok();
assert!(harness.ctx.memory(|mem| mem.any_popup_open()));
assert!(harness.state().user_modal_open);
harness.press_key(Key::Escape);
harness.run();
harness.run_ok();
assert!(!harness.ctx.memory(|mem| mem.any_popup_open()));
assert!(harness.state().user_modal_open);
}
@ -238,17 +239,11 @@ mod tests {
results.push(harness.try_snapshot("modals_1"));
harness.get_by_label("Save").click();
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
harness.run_ok();
results.push(harness.try_snapshot("modals_2"));
harness.get_by_label("Yes Please").click();
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
harness.run_ok();
results.push(harness.try_snapshot("modals_3"));
for result in results {
@ -272,14 +267,11 @@ mod tests {
initial_state,
);
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
harness.run_ok();
harness.get_by_label("Yes Please").simulate_click();
harness.run();
harness.run_ok();
// This snapshots should show the progress bar modal on top of the save modal.
harness.snapshot("modals_backdrop_should_prevent_focusing_lower_area");

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e89c730b462c2b60b90f2ac15fe9576e878a4906c223317c51344a0ec2b6d993
size 27564
oid sha256:4631f841b9e23833505af39eb1c45908013d3b1e1278d477bcedf6a460c71802
size 27163

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ea2c944af8bc1be42ec7c00be58dfaa23c92bca8957eda94f2ff10f5b4242562
size 83358
oid sha256:96e750ebfcc6ec2c130674407388c30cce844977cde19adfebf351fd08698a4f
size 81726

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c401ff91fff4051042528d398d2b2270a4ae924570e6332cf8f2c6774c845160
size 11826
oid sha256:b03613e597631da3b922ef3d696c4ce74cec41f86a2779fc5b084a316fc9e8e8
size 11764

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7efc1ff3e4e5bfd4216394f94ee7486c272a9ca1c980789f4ad143f89b0a7103
size 21073
oid sha256:76d77e2df39af248d004a023b238bb61ed598ab2bea3e0c6f2885f9537ec1103
size 25988

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d9c48cf928a17dd0980ba086aa004bde3a0040dcb82752d138c1df34f1ef3d2f
size 21167
oid sha256:003d905893b80ffc132a783155971ad3054229e9d6c046e2760c912559a66b3d
size 20869

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc69c76eaa121e9e7782cfbbb68b5a23004d79862bae4af2e3ca3a29eff04bea
size 136467
oid sha256:99c94a09c0f6984909481189f9a1415ea90bd7c45e42b4b3763ee36f3d831a65
size 133231

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:23187a9fb12a3ab7df4e2321aa25b493559923d61e82802f843ee29dcd932f7b
size 24985
oid sha256:7d8135b745cb95a7e7c7a26e73e160742f88ec177a2fa262215c4886d98ff172
size 24403

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f433f3e8bff38a0aafd7e6cba5c5efe1abf484550a6f9e90008f8f5ea891497
size 18113
oid sha256:7b38828e423195628dcea09b0cbdd66aa4c077334ab7082efd358c7a3297009d
size 17827

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e5105ecf77852412c0dd904b96f0fec752f22e416df9932df4499d6d5a776f46
size 22865
oid sha256:c53403996451da14f7991ea61bd20b96dbc67eb67dd2041975dd6ce5015a6980
size 22485

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ebf0403bd599e5c00c2831f9c4032e8d20420212c9cd7fa875f1ae1cbbc8d3a7
size 65902
oid sha256:63673b070951246b98ca07613fa81496dbfdd10627bac3c9c4356ebff1a36b20
size 64319

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4e690dc73873ab75c030d3c0238e9d5b840f976dd8f4882dc1e930024d217838
size 33323
oid sha256:374b4095a3c7b827b80d6ab01b945458ae0128a2974c2af8aaf6b7e9fef6b459
size 32554

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e760210371dbf2a197f96a78d01b7480f0ae05d46bbb4e642276b2eb30847ec2
size 37075
oid sha256:5e756c90069803bb4d2821fff723877bffffd44b26613f5b06c8a042d6901ca4
size 36578

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fd02b208d0e4e306bbc9a54f25f5a3d20875a12182cef3224e6daa309b6cf453
size 17898
oid sha256:64fe3ef34aaf3104931954f4a39760b99944f42da13f866622ca0222b750f6be
size 17731

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:341958da648a7db3374c4337cf057ae8e81c08c4a6de7e4f1cbe9c5b049f2e62
size 25727
oid sha256:dfa05d7f8c36b51c054253fbe8483c38637a12dde4a9d6051b226680820db319
size 25097

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8934cff7203d19b38df9d91729091ff5d1ad6c8d445fd9c1cb62b6df1bb8cb80
size 263547
oid sha256:eb631bd2608aec6c83f8815b9db0b28627bf82692fd2b1cb793119083b4f8ad1
size 264496

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d61f58138798d701bb8dda2c3240eef69eb350df3168fb3aa4148e4fef3f77a
size 24077
oid sha256:e0870e9e1c9dc718690124a4d490f1061414f15fa40571e571d9319c3b65e74e
size 23709

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e4006e93663d02fe0f4485d2c163ab2b6feded787bee87ea15616fc0b36136d0
size 188875
oid sha256:532dbcbec94bb9c9fb8cc0665646790a459088e98077118b5fbb2112898e1a43
size 183854

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:70b170ba7b8e51d9d9f766d7ce25068fa4265c4127e729af4f1adaacbb745d19
size 120947
oid sha256:3fd584643b68084ec4b65369e08d229e2d389551bbefa59c84ad4b58621593f7
size 117754

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:157353a8c9bcb638a8be485489e4a232f348eae3cc4ceefe227d7970c7d1f8b3
size 26256
oid sha256:7e2b854d99c9b17d15ef92188cdac521d7c0635aef9ba457cd3801884a784009
size 26159

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf0ddd39a45519dcf9027f691e856921c736d18e2eeafd16f0e086720121b6a7
size 72286
oid sha256:f0b7dc029de8e669d76f3be5e0800e973c354edcf7cefa239faed07c2cd5e0d5
size 70452

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:914d37e326087f770994bcf3867a27d88050c57887a2b42c68414d311fa96003
size 67698
oid sha256:64ba40810a6480e511e8d198b0dfb39e8b220eb2f5592877e27b17ee8d47b9c3
size 66387

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b0fdf8ce329883450e071e4733c3904577999d18ac61c922c7caacbec09dfda7
size 21661
oid sha256:0a8151f5bd01b2cb5183c532d11f57bbb7e8cc1e77a3c4b890537d157922c961
size 21261

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:375b71a8ac5b0e48f3c67a089ef0e8a4fd17f8eb21fa535422099c29c2747e27
size 59991
oid sha256:96ce36fcc334793801ab724c711f592faf086a9c98c812684e6b76010e9d1974
size 59714

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aa96b1e3733e4af94a6cb6ec765c3f3581df2175e75831eb00bd42df2e7a2708
size 13285
oid sha256:8155d93b78692ced67ddee4011526ef627838ede78351871df5feef8aa512857
size 13141

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:18880dfaf5d198876c4db97ebd6313d59755a3e8298567f2b2fa91dcc21699c5
size 35607
oid sha256:bf6da022ab97f9d4b862cc8e91bdfd7f9785a3ab0540aa1c2bd2646bd30a3450
size 35115

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d626b310439bff13487548bbba8b516886c13049824a7f5dd902f6dffb3c5ba4
size 48234
oid sha256:035b35ed053cabd5022d9a97f7d4d5a79d194c9f14594e7602ecd9ef94c24ae5
size 48053

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ed2a356452d792e32bea57f044da9d86da27fd8504826dd6b87618a53519ea6a
size 522556
oid sha256:3562bb87b155b2c406a72a651ffb1991305aa1e15273ce9f32cedc5766318537
size 554922

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0d04a5854528c6141f7def6f9229c51c6d2d4c87e2f656be4d149e7b2b852976
size 729056
oid sha256:ba6c0bd127ab02375f2ae5fbc3eeef33a1bdf074cbb180de2733c700b81df3e5
size 771069

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e6e5c1a745e357faa7b98f7a2cd1ca139c4a14be154b9d21feb8030933acfdb7
size 867552
oid sha256:d85ab6d04059009fd2c3ad8001332b27e710c46c9300f2f1f409b882c49211dc
size 918967

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d9f4a37541fd1a0754c1cb1f3a2d4a76f03d67ca4e5596c8e6982d691d29dea
size 980286
oid sha256:a64f1bdec565357fe4dee3acb46b12eeb0492b522fb3bb9697d39dadce2e8c21
size 1039455

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d1c99867202e16500146b7146a32fd83d70d60f5ac94aae4ca405a6377e4625
size 1066559
oid sha256:5ca008dca03372bb334564e55fa2d1d25a36751a43df6001a1c1cf3e4db9bcd4
size 1130930

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08fc1c89fd2d04aa12c75a1829dacfff090e322c65ad969648799833e1b072eb
size 1235574
oid sha256:7f695127e7fe6cb3b752a0acd71db85d51d2de68e45051a7afe91f4d960acf27
size 1311641

View File

@ -8,6 +8,8 @@ use std::marker::PhantomData;
pub struct HarnessBuilder<State = ()> {
pub(crate) screen_rect: Rect,
pub(crate) pixels_per_point: f32,
pub(crate) max_steps: u64,
pub(crate) step_dt: f32,
pub(crate) state: PhantomData<State>,
pub(crate) renderer: Box<dyn TestRenderer>,
}
@ -19,6 +21,8 @@ impl<State> Default for HarnessBuilder<State> {
pixels_per_point: 1.0,
state: PhantomData,
renderer: Box::new(LazyRenderer::default()),
max_steps: 4,
step_dt: 1.0 / 4.0,
}
}
}
@ -40,6 +44,26 @@ impl<State> HarnessBuilder<State> {
self
}
/// Set the maximum number of steps to run when calling [`Harness::run`].
///
/// Default is 4.
/// With the default `step_dt`, this means 1 second of simulation.
#[inline]
pub fn with_max_steps(mut self, max_steps: u64) -> Self {
self.max_steps = max_steps;
self
}
/// Set the time delta for a single [`Harness::step`].
///
/// Default is 1.0 / 4.0 (4fps).
/// The default is low so we don't waste cpu waiting for animations.
#[inline]
pub fn with_step_dt(mut self, step_dt: f32) -> Self {
self.step_dt = step_dt;
self
}
/// Set the [`TestRenderer`] to use for rendering.
///
/// By default, a [`LazyRenderer`] is used.

View File

@ -10,7 +10,9 @@ mod snapshot;
#[cfg(feature = "snapshot")]
pub use snapshot::*;
use std::fmt::{Debug, Formatter};
use std::fmt::{Debug, Display, Formatter};
use std::time::Duration;
mod app_kind;
mod renderer;
#[cfg(feature = "wgpu")]
@ -26,9 +28,26 @@ use crate::event::EventState;
pub use builder::*;
pub use renderer::*;
use egui::{Modifiers, Pos2, Rect, Vec2, ViewportId};
use egui::{Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId};
use kittest::{Node, Queryable};
pub struct ExceededMaxStepsError {
pub max_steps: u64,
pub repaint_causes: Vec<RepaintCause>,
}
impl Display for ExceededMaxStepsError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Harness::run exceeded max_steps ({}). If your expect your ui to keep repainting \
(e.g. when showing a spinner) call Harness::step or Harness::run_steps instead.\
\nRepaint causes: {:#?}",
self.max_steps, self.repaint_causes,
)
}
}
/// The test Harness. This contains everything needed to run the test.
/// Create a new Harness using [`Harness::new`] or [`Harness::builder`].
///
@ -45,6 +64,8 @@ pub struct Harness<'a, State = ()> {
response: Option<egui::Response>,
state: State,
renderer: Box<dyn TestRenderer>,
max_steps: u64,
step_dt: f32,
}
impl<'a, State> Debug for Harness<'a, State> {
@ -60,14 +81,24 @@ impl<'a, State> Harness<'a, State> {
mut state: State,
ctx: Option<egui::Context>,
) -> Self {
let HarnessBuilder {
screen_rect,
pixels_per_point,
max_steps,
step_dt,
state: _,
mut renderer,
} = builder;
let ctx = ctx.unwrap_or_default();
ctx.enable_accesskit();
// Disable cursor blinking so it doesn't interfere with snapshots
ctx.all_styles_mut(|style| style.visuals.text_cursor.blink = false);
let mut input = egui::RawInput {
screen_rect: Some(builder.screen_rect),
screen_rect: Some(screen_rect),
..Default::default()
};
let viewport = input.viewports.get_mut(&ViewportId::ROOT).unwrap();
viewport.native_pixels_per_point = Some(builder.pixels_per_point);
viewport.native_pixels_per_point = Some(pixels_per_point);
let mut response = None;
@ -77,7 +108,6 @@ impl<'a, State> Harness<'a, State> {
response = app.run(ctx, &mut state, false);
});
let mut renderer = builder.renderer;
renderer.handle_delta(&output.textures_delta);
let mut harness = Self {
@ -96,9 +126,11 @@ impl<'a, State> Harness<'a, State> {
event_state: EventState::default(),
state,
renderer,
max_steps,
step_dt,
};
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
harness.run();
harness.run_ok();
harness
}
@ -188,7 +220,8 @@ impl<'a, State> Harness<'a, State> {
}
/// Run a frame.
/// This will call the app closure with the current context and update the Harness.
/// This will call the app closure with the queued events and current context and
/// update the Harness.
pub fn step(&mut self) {
self._step(false);
}
@ -200,6 +233,8 @@ impl<'a, State> Harness<'a, State> {
}
}
self.input.predicted_dt = self.step_dt;
let mut output = self.ctx.run(self.input.take(), |ctx| {
self.response = self.app.run(ctx, &mut self.state, sizing_pass);
});
@ -215,22 +250,95 @@ impl<'a, State> Harness<'a, State> {
}
/// Resize the test harness to fit the contents. This only works when creating the Harness via
/// [`Harness::new_ui`] or [`HarnessBuilder::build_ui`].
/// [`Harness::new_ui`] / [`Harness::new_ui_state`] or
/// [`HarnessBuilder::build_ui`] / [`HarnessBuilder::build_ui_state`].
pub fn fit_contents(&mut self) {
self._step(true);
if let Some(response) = &self.response {
self.set_size(response.rect.size());
}
self.run();
self.run_ok();
}
/// Run a few frames.
/// This will soon be changed to run the app until it is "stable", meaning
/// Run until
/// - all animations are done
/// - no more repaints are requested
pub fn run(&mut self) {
const STEPS: usize = 2;
for _ in 0..STEPS {
///
/// Returns the number of frames that were run.
///
/// # Panics
/// Panics if the number of steps exceeds the maximum number of steps set
/// in [`HarnessBuilder::with_max_steps`].
///
/// See also:
/// - [`Harness::try_run`].
/// - [`Harness::run_ok`].
/// - [`Harness::step`].
/// - [`Harness::run_steps`].
#[track_caller]
pub fn run(&mut self) -> u64 {
match self.try_run() {
Ok(steps) => steps,
Err(err) => {
panic!("{err}");
}
}
}
/// Run until
/// - all animations are done
/// - no more repaints are requested
/// - the maximum number of steps is reached (See [`HarnessBuilder::with_max_steps`])
///
/// Returns the number of steps that were run.
///
/// # Errors
/// Returns an error if the maximum number of steps is exceeded.
///
/// See also:
/// - [`Harness::run`].
/// - [`Harness::run_ok`].
/// - [`Harness::step`].
/// - [`Harness::run_steps`].
pub fn try_run(&mut self) -> Result<u64, ExceededMaxStepsError> {
let mut steps = 0;
loop {
steps += 1;
self.step();
// We only care about immediate repaints
if self.root_viewport_output().repaint_delay != Duration::ZERO {
break;
}
if steps > self.max_steps {
return Err(ExceededMaxStepsError {
max_steps: self.max_steps,
repaint_causes: self.ctx.repaint_causes(),
});
}
}
Ok(steps)
}
/// Run until
/// - all animations are done
/// - no more repaints are requested
/// - the maximum number of steps is reached (See [`HarnessBuilder::with_max_steps`])
///
/// Returns the number of steps that were run, or None if the maximum number of steps was exceeded.
///
/// See also:
/// - [`Harness::run`].
/// - [`Harness::try_run`].
/// - [`Harness::step`].
/// - [`Harness::run_steps`].
pub fn run_ok(&mut self) -> Option<u64> {
self.try_run().ok()
}
/// Run a number of steps.
/// Equivalent to calling [`Harness::step`] x times.
pub fn run_steps(&mut self, steps: usize) {
for _ in 0..steps {
self.step();
}
}
@ -297,6 +405,14 @@ impl<'a, State> Harness<'a, State> {
pub fn render(&mut self) -> Result<image::RgbaImage, String> {
self.renderer.render(&self.ctx, &self.output)
}
/// Get the root viewport output
fn root_viewport_output(&self) -> &egui::ViewportOutput {
self.output
.viewport_output
.get(&ViewportId::ROOT)
.expect("Missing root viewport")
}
}
/// Utilities for stateless harnesses.