`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:
parent
ba577602a4
commit
77df407f50
|
|
@ -373,7 +373,7 @@ mod tests {
|
||||||
use crate::{Demo as _, demo::demo_app_windows::DemoGroups};
|
use crate::{Demo as _, demo::demo_app_windows::DemoGroups};
|
||||||
|
|
||||||
use egui_kittest::kittest::{NodeT as _, Queryable as _};
|
use egui_kittest::kittest::{NodeT as _, Queryable as _};
|
||||||
use egui_kittest::{Harness, SnapshotOptions, SnapshotResults};
|
use egui_kittest::{Harness, OsThreshold, SnapshotOptions, SnapshotResults};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn demos_should_match_snapshot() {
|
fn demos_should_match_snapshot() {
|
||||||
|
|
@ -410,9 +410,10 @@ mod tests {
|
||||||
harness.run_ok();
|
harness.run_ok();
|
||||||
|
|
||||||
let mut options = SnapshotOptions::default();
|
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" {
|
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));
|
results.add(harness.try_snapshot_options(&format!("demos/{name}"), &options));
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,117 @@ pub struct SnapshotOptions {
|
||||||
/// wgpu backends).
|
/// wgpu backends).
|
||||||
pub threshold: f32,
|
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 path where the snapshots will be saved.
|
||||||
/// The default is `tests/snapshots`.
|
/// The default is `tests/snapshots`.
|
||||||
pub output_path: PathBuf,
|
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 {
|
impl Default for SnapshotOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
threshold: 0.6,
|
threshold: 0.6,
|
||||||
output_path: PathBuf::from("tests/snapshots"),
|
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
|
/// The default is `0.6` (which is enough for most egui tests to pass across different
|
||||||
/// wgpu backends).
|
/// wgpu backends).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn threshold(mut self, threshold: f32) -> Self {
|
pub fn threshold(mut self, threshold: impl Into<f32>) -> Self {
|
||||||
self.threshold = threshold;
|
self.threshold = threshold.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +150,20 @@ impl SnapshotOptions {
|
||||||
self.output_path = output_path.into();
|
self.output_path = output_path.into();
|
||||||
self
|
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)]
|
#[derive(Debug)]
|
||||||
|
|
@ -58,7 +173,7 @@ pub enum SnapshotError {
|
||||||
/// Name of the test
|
/// Name of the test
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
/// Count of pixels that were different
|
/// Count of pixels that were different (above the per-pixel threshold).
|
||||||
diff: i32,
|
diff: i32,
|
||||||
|
|
||||||
/// Path where the diff image was saved
|
/// Path where the diff image was saved
|
||||||
|
|
@ -201,6 +316,7 @@ pub fn try_image_snapshot_options(
|
||||||
let SnapshotOptions {
|
let SnapshotOptions {
|
||||||
threshold,
|
threshold,
|
||||||
output_path,
|
output_path,
|
||||||
|
failed_pixel_count_threshold,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
let parent_path = if let Some(parent) = PathBuf::from(name).parent() {
|
let parent_path = if let Some(parent) = PathBuf::from(name).parent() {
|
||||||
|
|
@ -280,19 +396,24 @@ pub fn try_image_snapshot_options(
|
||||||
let result =
|
let result =
|
||||||
dify::diff::get_results(previous, new.clone(), *threshold, true, None, &None, &None);
|
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
|
result_image
|
||||||
.save(diff_path.clone())
|
.save(diff_path.clone())
|
||||||
.map_err(|err| SnapshotError::WriteSnapshot {
|
.map_err(|err| SnapshotError::WriteSnapshot {
|
||||||
path: diff_path.clone(),
|
path: diff_path.clone(),
|
||||||
err,
|
err,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if should_update_snapshots() {
|
if should_update_snapshots() {
|
||||||
update_snapshot()
|
update_snapshot()
|
||||||
} else {
|
} else {
|
||||||
|
if num_wrong_pixels as i64 <= *failed_pixel_count_threshold as i64 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
Err(SnapshotError::Diff {
|
Err(SnapshotError::Diff {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
diff,
|
diff: num_wrong_pixels,
|
||||||
diff_path,
|
diff_path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue