WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit de90761

Browse files
authored
Enforce consistent snapshot updates (#7744)
* Closes #7647 This collects SnapshotResults within the Harness and adds a check to enforce snapshot results are merged in case multiple Harnesses are constructed within a test. This should make snapshot updates via kitdiff/accept_snapshots.sh way more useful since it should now always update all snapshots instead of only the first one per test.
1 parent a19629e commit de90761

File tree

7 files changed

+100
-17
lines changed

7 files changed

+100
-17
lines changed

crates/egui_demo_lib/src/demo/tests/tessellation_test.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,13 @@ fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) {
357357
#[cfg(test)]
358358
mod tests {
359359
use crate::View as _;
360+
use egui_kittest::SnapshotResults;
360361

361362
use super::*;
362363

363364
#[test]
364365
fn snapshot_tessellation_test() {
366+
let mut results = SnapshotResults::new();
365367
for (name, shape) in TessellationTest::interesting_shapes() {
366368
let mut test = TessellationTest {
367369
shape,
@@ -375,6 +377,7 @@ mod tests {
375377
harness.run();
376378

377379
harness.snapshot(format!("tessellation_test/{name}"));
380+
results.extend_harness(&mut harness);
378381
}
379382
}
380383
}

crates/egui_demo_lib/src/demo/widget_gallery.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ mod tests {
310310
use super::*;
311311
use crate::View as _;
312312
use egui::Vec2;
313-
use egui_kittest::Harness;
313+
use egui_kittest::{Harness, SnapshotResults};
314314

315315
#[test]
316316
pub fn should_match_screenshot() {
@@ -320,6 +320,8 @@ mod tests {
320320
..Default::default()
321321
};
322322

323+
let mut results = SnapshotResults::new();
324+
323325
for pixels_per_point in [1, 2] {
324326
for theme in [egui::Theme::Light, egui::Theme::Dark] {
325327
let mut harness = Harness::builder()
@@ -339,6 +341,7 @@ mod tests {
339341
};
340342
let image_name = format!("widget_gallery_{theme_name}_x{pixels_per_point}");
341343
harness.snapshot(&image_name);
344+
results.extend_harness(&mut harness);
342345
}
343346
}
344347
}

crates/egui_demo_lib/tests/image_blending.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use egui_kittest::Harness;
33

44
#[test]
55
fn test_image_blending() {
6+
let mut results = egui_kittest::SnapshotResults::new();
67
for pixels_per_point in [1.0, 2.0] {
78
let mut harness = Harness::builder()
89
.with_pixels_per_point(pixels_per_point)
@@ -21,5 +22,6 @@ fn test_image_blending() {
2122
harness.run();
2223
harness.fit_contents();
2324
harness.snapshot(format!("image_blending/image_x{pixels_per_point}"));
25+
results.extend_harness(&mut harness);
2426
}
2527
}

crates/egui_demo_lib/tests/misc.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use egui_kittest::{Harness, kittest::Queryable as _};
33

44
#[test]
55
fn test_kerning() {
6+
let mut results = egui_kittest::SnapshotResults::new();
67
for pixels_per_point in [1.0, 2.0] {
78
for theme in [egui::Theme::Dark, egui::Theme::Light] {
89
let mut harness = Harness::builder()
@@ -24,12 +25,14 @@ fn test_kerning() {
2425
egui::Theme::Light => "light",
2526
}
2627
));
28+
results.extend_harness(&mut harness);
2729
}
2830
}
2931
}
3032

3133
#[test]
3234
fn test_italics() {
35+
let mut results = egui_kittest::SnapshotResults::new();
3336
for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] {
3437
for theme in [egui::Theme::Dark, egui::Theme::Light] {
3538
let mut harness = Harness::builder()
@@ -49,6 +52,7 @@ fn test_italics() {
4952
egui::Theme::Light => "light",
5053
}
5154
));
55+
results.extend_harness(&mut harness);
5256
}
5357
}
5458
}

crates/egui_kittest/src/builder.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ impl<State> HarnessBuilder<State> {
166166
///
167167
/// assert_eq!(*harness.state(), true);
168168
/// ```
169+
#[track_caller]
169170
pub fn build_state<'a>(
170171
self,
171172
app: impl FnMut(&egui::Context, &mut State) + 'a,
@@ -195,6 +196,7 @@ impl<State> HarnessBuilder<State> {
195196
///
196197
/// assert_eq!(*harness.state(), true);
197198
/// ```
199+
#[track_caller]
198200
pub fn build_ui_state<'a>(
199201
self,
200202
app: impl FnMut(&mut egui::Ui, &mut State) + 'a,
@@ -206,6 +208,7 @@ impl<State> HarnessBuilder<State> {
206208
/// Create a new [Harness] from the given eframe creation closure.
207209
/// The app can be accessed via the [`Harness::state`] / [`Harness::state_mut`] methods.
208210
#[cfg(feature = "eframe")]
211+
#[track_caller]
209212
pub fn build_eframe<'a>(
210213
self,
211214
build: impl FnOnce(&mut eframe::CreationContext<'a>) -> State,
@@ -247,6 +250,7 @@ impl HarnessBuilder {
247250
/// });
248251
/// ```
249252
#[must_use]
253+
#[track_caller]
250254
pub fn build<'a>(self, app: impl FnMut(&egui::Context) + 'a) -> Harness<'a> {
251255
Harness::from_builder(self, AppKind::Context(Box::new(app)), (), None)
252256
}
@@ -267,6 +271,7 @@ impl HarnessBuilder {
267271
/// });
268272
/// ```
269273
#[must_use]
274+
#[track_caller]
270275
pub fn build_ui<'a>(self, app: impl FnMut(&mut egui::Ui) + 'a) -> Harness<'a> {
271276
Harness::from_builder(self, AppKind::Ui(Box::new(app)), (), None)
272277
}

crates/egui_kittest/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ pub struct Harness<'a, State = ()> {
8484

8585
#[cfg(feature = "snapshot")]
8686
default_snapshot_options: SnapshotOptions,
87+
#[cfg(feature = "snapshot")]
88+
snapshot_results: SnapshotResults,
8789
}
8890

8991
impl<State> Debug for Harness<'_, State> {
@@ -93,6 +95,7 @@ impl<State> Debug for Harness<'_, State> {
9395
}
9496

9597
impl<'a, State> Harness<'a, State> {
98+
#[track_caller]
9699
pub(crate) fn from_builder(
97100
builder: HarnessBuilder<State>,
98101
mut app: AppKind<'a, State>,
@@ -162,6 +165,9 @@ impl<'a, State> Harness<'a, State> {
162165

163166
#[cfg(feature = "snapshot")]
164167
default_snapshot_options,
168+
169+
#[cfg(feature = "snapshot")]
170+
snapshot_results: SnapshotResults::default(),
165171
};
166172
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
167173
harness.run_ok();
@@ -197,6 +203,7 @@ impl<'a, State> Harness<'a, State> {
197203
///
198204
/// assert_eq!(*harness.state(), true);
199205
/// ```
206+
#[track_caller]
200207
pub fn new_state(app: impl FnMut(&egui::Context, &mut State) + 'a, state: State) -> Self {
201208
Self::builder().build_state(app, state)
202209
}
@@ -222,12 +229,14 @@ impl<'a, State> Harness<'a, State> {
222229
///
223230
/// assert_eq!(*harness.state(), true);
224231
/// ```
232+
#[track_caller]
225233
pub fn new_ui_state(app: impl FnMut(&mut egui::Ui, &mut State) + 'a, state: State) -> Self {
226234
Self::builder().build_ui_state(app, state)
227235
}
228236

229237
/// Create a new [Harness] from the given eframe creation closure.
230238
#[cfg(feature = "eframe")]
239+
#[track_caller]
231240
pub fn new_eframe(builder: impl FnOnce(&mut eframe::CreationContext<'a>) -> State) -> Self
232241
where
233242
State: eframe::App,
@@ -725,6 +734,7 @@ impl<'a> Harness<'a> {
725734
/// });
726735
/// });
727736
/// ```
737+
#[track_caller]
728738
pub fn new(app: impl FnMut(&egui::Context) + 'a) -> Self {
729739
Self::builder().build(app)
730740
}
@@ -745,6 +755,7 @@ impl<'a> Harness<'a> {
745755
/// ui.label("Hello, world!");
746756
/// });
747757
/// ```
758+
#[track_caller]
748759
pub fn new_ui(app: impl FnMut(&mut egui::Ui) + 'a) -> Self {
749760
Self::builder().build_ui(app)
750761
}

crates/egui_kittest/src/snapshot.rs

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -663,16 +663,16 @@ impl<State> Harness<'_, State> {
663663
/// If the new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
664664
///
665665
/// # Panics
666-
/// Panics if the image does not match the snapshot, if there was an error reading or writing the
666+
/// The result is added to the [`Harness`]'s internal [`SnapshotResults`].
667+
///
668+
/// The harness will panic when dropped if there were any snapshot errors.
669+
///
670+
/// Errors happen if the image does not match the snapshot, if there was an error reading or writing the
667671
/// snapshot, if the rendering fails or if no default renderer is available.
668672
#[track_caller]
669673
pub fn snapshot_options(&mut self, name: impl Into<String>, options: &SnapshotOptions) {
670-
match self.try_snapshot_options(name, options) {
671-
Ok(_) => {}
672-
Err(err) => {
673-
panic!("{err}");
674-
}
675-
}
674+
let result = self.try_snapshot_options(name, options);
675+
self.snapshot_results.add(result);
676676
}
677677

678678
/// Render an image using the setup [`crate::TestRenderer`] and compare it to the snapshot.
@@ -688,12 +688,8 @@ impl<State> Harness<'_, State> {
688688
/// snapshot, if the rendering fails or if no default renderer is available.
689689
#[track_caller]
690690
pub fn snapshot(&mut self, name: impl Into<String>) {
691-
match self.try_snapshot(name) {
692-
Ok(_) => {}
693-
Err(err) => {
694-
panic!("{err}");
695-
}
696-
}
691+
let result = self.try_snapshot(name);
692+
self.snapshot_results.add(result);
697693
}
698694

699695
/// Render a snapshot, save it to a temp file and open it in the default image viewer.
@@ -739,6 +735,12 @@ impl<State> Harness<'_, State> {
739735
}
740736
}
741737
}
738+
739+
/// This removes the snapshot results from the harness. Useful if you e.g. want to merge it
740+
/// with the results from another harness (using [`SnapshotResults::add`]).
741+
pub fn take_snapshot_results(&mut self) -> SnapshotResults {
742+
std::mem::take(&mut self.snapshot_results)
743+
}
742744
}
743745

744746
/// Utility to collect snapshot errors and display them at the end of the test.
@@ -765,9 +767,22 @@ impl<State> Harness<'_, State> {
765767
/// Panics if there are any errors when dropped (this way it is impossible to forget to call `unwrap`).
766768
/// If you don't want to panic, you can use [`SnapshotResults::into_result`] or [`SnapshotResults::into_inner`].
767769
/// If you want to panic early, you can use [`SnapshotResults::unwrap`].
768-
#[derive(Debug, Default)]
770+
#[derive(Debug)]
769771
pub struct SnapshotResults {
770772
errors: Vec<SnapshotError>,
773+
handled: bool,
774+
location: std::panic::Location<'static>,
775+
}
776+
777+
impl Default for SnapshotResults {
778+
#[track_caller]
779+
fn default() -> Self {
780+
Self {
781+
errors: Vec::new(),
782+
handled: true, // If no snapshots were added, we should consider this handled.
783+
location: *std::panic::Location::caller(),
784+
}
785+
}
771786
}
772787

773788
impl Display for SnapshotResults {
@@ -785,17 +800,30 @@ impl Display for SnapshotResults {
785800
}
786801

787802
impl SnapshotResults {
803+
#[track_caller]
788804
pub fn new() -> Self {
789805
Default::default()
790806
}
791807

792808
/// Check if the result is an error and add it to the list of errors.
793809
pub fn add(&mut self, result: SnapshotResult) {
810+
self.handled = false;
794811
if let Err(err) = result {
795812
self.errors.push(err);
796813
}
797814
}
798815

816+
/// Add all errors from another `SnapshotResults`.
817+
pub fn extend(&mut self, other: Self) {
818+
self.handled = false;
819+
self.errors.extend(other.into_inner());
820+
}
821+
822+
/// Add all errors from a [`Harness`].
823+
pub fn extend_harness<T>(&mut self, harness: &mut Harness<'_, T>) {
824+
self.extend(harness.take_snapshot_results());
825+
}
826+
799827
/// Check if there are any errors.
800828
pub fn has_errors(&self) -> bool {
801829
!self.errors.is_empty()
@@ -807,13 +835,14 @@ impl SnapshotResults {
807835
if self.has_errors() { Err(self) } else { Ok(()) }
808836
}
809837

838+
/// Consume this and return the list of errors.
810839
pub fn into_inner(mut self) -> Vec<SnapshotError> {
840+
self.handled = true;
811841
std::mem::take(&mut self.errors)
812842
}
813843

814844
/// Panics if there are any errors, displaying each.
815845
#[expect(clippy::unused_self)]
816-
#[track_caller]
817846
pub fn unwrap(self) {
818847
// Panic is handled in drop
819848
}
@@ -826,7 +855,6 @@ impl From<SnapshotResults> for Vec<SnapshotError> {
826855
}
827856

828857
impl Drop for SnapshotResults {
829-
#[track_caller]
830858
fn drop(&mut self) {
831859
// Don't panic if we are already panicking (the test probably failed for another reason)
832860
if std::thread::panicking() {
@@ -836,5 +864,32 @@ impl Drop for SnapshotResults {
836864
if self.has_errors() {
837865
panic!("{}", self);
838866
}
867+
868+
thread_local! {
869+
static UNHANDLED_SNAPSHOT_RESULTS_COUNTER: std::cell::RefCell<usize> = const { std::cell::RefCell::new(0) };
870+
}
871+
872+
if !self.handled {
873+
let count = UNHANDLED_SNAPSHOT_RESULTS_COUNTER.with(|counter| {
874+
let mut count = counter.borrow_mut();
875+
*count += 1;
876+
*count
877+
});
878+
879+
#[expect(clippy::manual_assert)]
880+
if count >= 2 {
881+
panic!(
882+
r#"
883+
Multiple SnapshotResults were dropped without being handled.
884+
885+
In order to allow consistent snapshot updates, all snapshot results within a test should be merged in a single SnapshotResults instance.
886+
Usually this is handled internally in a harness. If you have multiple harnesses, you can merge the results using `Harness::take_snapshot_results` and `SnapshotResults::extend`.
887+
888+
The SnapshotResult was constructed at {}
889+
"#,
890+
self.location
891+
);
892+
}
893+
}
839894
}
840895
}

0 commit comments

Comments
 (0)