`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<f64>` 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 <https://github.com/emilk/egui/issues/5683>
* [x] I have followed the instructions in the PR template

---------

Co-authored-by: lucasmerlin <hi@lucasmerlin.me>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Nicolas 2025-07-03 14:23:15 +02:00 committed by GitHub
parent ba577602a4
commit 77df407f50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 130 additions and 8 deletions

View File

@ -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));

View File

@ -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<T> {
pub windows: T,
pub macos: T,
pub linux: T,
pub fallback: T,
}
impl From<usize> for OsThreshold<usize> {
fn from(value: usize) -> Self {
Self::new(value)
}
}
impl<T> OsThreshold<T>
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<OsThreshold<Self>> for usize {
fn from(threshold: OsThreshold<Self>) -> Self {
threshold.threshold()
}
}
impl From<OsThreshold<Self>> for f32 {
fn from(threshold: OsThreshold<Self>) -> 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<f32>) -> 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<OsThreshold<usize>>,
) -> 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,
})
}