From 77df407f50230da3a920315eb1061cd0e6811b45 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 3 Jul 2025 14:23:15 +0200 Subject: [PATCH] `egui_kittest`: add `failed_pixel_count_threshold` (#7092) I thought about this - so we have two options here: 1. adding it to `SnapshotOptions` 2. adding it to every function which I do not like as this would be a huge breaking change ## Summary This pull request introduces a new feature to the `SnapshotOptions` struct in the `egui_kittest` crate, allowing users to specify a permissible percentage of pixel differences (`diff_percentage`) before a snapshot comparison is considered a failure. This feature provides more flexibility in handling minor visual discrepancies during snapshot testing. ### Additions to `SnapshotOptions`: * Added a new field `diff_percentage` of type `Option` to the `SnapshotOptions` struct. This field allows users to define a tolerance for pixel differences, with a default value of `None` (interpreted as 0% tolerance). * Updated the `Default` implementation of `SnapshotOptions` to initialize `diff_percentage` to `None`. ### Integration into snapshot comparison logic: * Updated the `try_image_snapshot_options` function to handle the new `diff_percentage` field. If a `diff_percentage` is specified, the function calculates the percentage of differing pixels and allows the snapshot to pass if the difference is within the specified tolerance. [[1]](diffhunk://#diff-6f481b5866b82a4fe126b7df2e6c9669040c79d1d200d76b87f376de5dec5065R204) [[2]](diffhunk://#diff-6f481b5866b82a4fe126b7df2e6c9669040c79d1d200d76b87f376de5dec5065R294-R301) * Closes * [x] I have followed the instructions in the PR template --------- Co-authored-by: lucasmerlin Co-authored-by: Emil Ernerfeldt --- .../src/demo/demo_app_windows.rs | 7 +- crates/egui_kittest/src/snapshot.rs | 131 +++++++++++++++++- 2 files changed, 130 insertions(+), 8 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 0e3a0d2c..4414a957 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -373,7 +373,7 @@ mod tests { use crate::{Demo as _, demo::demo_app_windows::DemoGroups}; use egui_kittest::kittest::{NodeT as _, Queryable as _}; - use egui_kittest::{Harness, SnapshotOptions, SnapshotResults}; + use egui_kittest::{Harness, OsThreshold, SnapshotOptions, SnapshotResults}; #[test] fn demos_should_match_snapshot() { @@ -410,9 +410,10 @@ mod tests { harness.run_ok(); let mut options = SnapshotOptions::default(); - // The Bézier Curve demo needs a threshold of 2.1 to pass on linux + if name == "Bézier Curve" { - options.threshold = 2.1; + // The Bézier Curve demo needs a threshold of 2.1 to pass on linux: + options = options.threshold(OsThreshold::new(0.0).linux(2.1)); } results.add(harness.try_snapshot_options(&format!("demos/{name}"), &options)); diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 3c7dc265..e53615d9 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -13,16 +13,117 @@ pub struct SnapshotOptions { /// wgpu backends). pub threshold: f32, + /// The number of pixels that can differ before the snapshot is considered a failure. + /// Preferably, you should use `threshold` to control the sensitivity of the image comparison. + /// As a last resort, you can use this to allow a certain number of pixels to differ. + /// If `None`, the default is `0` (meaning no pixels can differ). + /// If `Some`, the value can be set per OS + pub failed_pixel_count_threshold: usize, + /// The path where the snapshots will be saved. /// The default is `tests/snapshots`. pub output_path: PathBuf, } +/// Helper struct to define the number of pixels that can differ before the snapshot is considered a failure. +/// This is useful if you want to set different thresholds for different operating systems. +/// +/// The default values are 0 / 0.0 +/// +/// Example usage: +/// ```no_run +/// use egui_kittest::{OsThreshold, SnapshotOptions}; +/// let mut harness = egui_kittest::Harness::new_ui(|ui| { +/// ui.label("Hi!"); +/// }); +/// harness.snapshot_options( +/// "os_threshold_example", +/// &SnapshotOptions::new() +/// .threshold(OsThreshold::new(0.0).windows(10.0)) +/// .failed_pixel_count_threshold(OsThreshold::new(0).windows(10).macos(53) +/// )) +/// ``` +#[derive(Debug, Clone, Copy)] +pub struct OsThreshold { + pub windows: T, + pub macos: T, + pub linux: T, + pub fallback: T, +} + +impl From for OsThreshold { + fn from(value: usize) -> Self { + Self::new(value) + } +} + +impl OsThreshold +where + T: Copy, +{ + /// Use the same value for all + pub fn new(same: T) -> Self { + Self { + windows: same, + macos: same, + linux: same, + fallback: same, + } + } + + /// Set the threshold for Windows. + #[inline] + pub fn windows(mut self, threshold: T) -> Self { + self.windows = threshold; + self + } + + /// Set the threshold for macOS. + #[inline] + pub fn macos(mut self, threshold: T) -> Self { + self.macos = threshold; + self + } + + /// Set the threshold for Linux. + #[inline] + pub fn linux(mut self, threshold: T) -> Self { + self.linux = threshold; + self + } + + /// Get the threshold for the current operating system. + pub fn threshold(&self) -> T { + if cfg!(target_os = "windows") { + self.windows + } else if cfg!(target_os = "macos") { + self.macos + } else if cfg!(target_os = "linux") { + self.linux + } else { + self.fallback + } + } +} + +impl From> for usize { + fn from(threshold: OsThreshold) -> Self { + threshold.threshold() + } +} + +impl From> for f32 { + fn from(threshold: OsThreshold) -> Self { + threshold.threshold() + } +} + impl Default for SnapshotOptions { fn default() -> Self { Self { threshold: 0.6, output_path: PathBuf::from("tests/snapshots"), + failed_pixel_count_threshold: 0, // Default is 0, meaning no pixels can differ } } } @@ -37,8 +138,8 @@ impl SnapshotOptions { /// The default is `0.6` (which is enough for most egui tests to pass across different /// wgpu backends). #[inline] - pub fn threshold(mut self, threshold: f32) -> Self { - self.threshold = threshold; + pub fn threshold(mut self, threshold: impl Into) -> Self { + self.threshold = threshold.into(); self } @@ -49,6 +150,20 @@ impl SnapshotOptions { self.output_path = output_path.into(); self } + + /// Change the number of pixels that can differ before the snapshot is considered a failure. + /// + /// Preferably, you should use [`Self::threshold`] to control the sensitivity of the image comparison. + /// As a last resort, you can use this to allow a certain number of pixels to differ. + #[inline] + pub fn failed_pixel_count_threshold( + mut self, + failed_pixel_count_threshold: impl Into>, + ) -> Self { + let failed_pixel_count_threshold = failed_pixel_count_threshold.into().threshold(); + self.failed_pixel_count_threshold = failed_pixel_count_threshold; + self + } } #[derive(Debug)] @@ -58,7 +173,7 @@ pub enum SnapshotError { /// Name of the test name: String, - /// Count of pixels that were different + /// Count of pixels that were different (above the per-pixel threshold). diff: i32, /// Path where the diff image was saved @@ -201,6 +316,7 @@ pub fn try_image_snapshot_options( let SnapshotOptions { threshold, output_path, + failed_pixel_count_threshold, } = options; let parent_path = if let Some(parent) = PathBuf::from(name).parent() { @@ -280,19 +396,24 @@ pub fn try_image_snapshot_options( let result = dify::diff::get_results(previous, new.clone(), *threshold, true, None, &None, &None); - if let Some((diff, result_image)) = result { + if let Some((num_wrong_pixels, result_image)) = result { result_image .save(diff_path.clone()) .map_err(|err| SnapshotError::WriteSnapshot { path: diff_path.clone(), err, })?; + if should_update_snapshots() { update_snapshot() } else { + if num_wrong_pixels as i64 <= *failed_pixel_count_threshold as i64 { + return Ok(()); + } + Err(SnapshotError::Diff { name: name.to_owned(), - diff, + diff: num_wrong_pixels, diff_path, }) }