Skip to content

Commit

Permalink
egui_kittest: Allow customizing the snapshot threshold and path (#5304)
Browse files Browse the repository at this point in the history
This adds `egui_kittest::try_image_snapshot_options` and
`egui_kittest::image_snapshot_options`, as well as
`Harness::wgpu_snapshot_options` and
`Harness::try_wgpu_snapshot_options`

* [X] I have followed the instructions in the PR template
  • Loading branch information
lucasmerlin authored Oct 29, 2024
1 parent 759a0b2 commit dafcfda
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 36 deletions.
10 changes: 8 additions & 2 deletions crates/egui_demo_lib/src/demo/demo_app_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ mod tests {
use crate::demo::demo_app_windows::Demos;
use egui::Vec2;
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
use egui_kittest::{Harness, SnapshotOptions};

#[test]
fn demos_should_match_snapshot() {
Expand Down Expand Up @@ -417,7 +417,13 @@ mod tests {
// Run the app for some more frames...
harness.run();

let result = harness.try_wgpu_snapshot(&format!("demos/{name}"));
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;
}

let result = harness.try_wgpu_snapshot_options(&format!("demos/{name}"), &options);
if let Err(err) = result {
errors.push(err);
}
Expand Down
6 changes: 6 additions & 0 deletions crates/egui_kittest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,9 @@ Running with `UPDATE_SNAPSHOTS=true` will still cause the tests to fail, but on
If you want to have multiple snapshots in the same test, it makes sense to collect the results in a `Vec`
([look here](https://github.com/emilk/egui/blob/70a01138b77f9c5724a35a6ef750b9ae1ab9f2dc/crates/egui_demo_lib/src/demo/demo_app_windows.rs#L388-L427) for an example).
This way they can all be updated at the same time.

You should add the following to your `.gitignore`:
```gitignore
**/tests/snapshots/**/*.diff.png
**/tests/snapshots/**/*.new.png
```
217 changes: 183 additions & 34 deletions crates/egui_kittest/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,51 @@ use std::fmt::Display;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};

#[non_exhaustive]
pub struct SnapshotOptions {
/// The threshold for the image comparison.
/// The default is `0.6` (which is enough for most egui tests to pass across different
/// wgpu backends).
pub threshold: f32,

/// The path where the snapshots will be saved.
/// The default is `tests/snapshots`.
pub output_path: PathBuf,
}

impl Default for SnapshotOptions {
fn default() -> Self {
Self {
threshold: 0.6,
output_path: PathBuf::from("tests/snapshots"),
}
}
}

impl SnapshotOptions {
/// Create a new [`SnapshotOptions`] with the default values.
pub fn new() -> Self {
Default::default()
}

/// Change the threshold for the image comparison.
/// 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;
self
}

/// Change the path where the snapshots will be saved.
/// The default is `tests/snapshots`.
#[inline]
pub fn output_path(mut self, output_path: impl Into<PathBuf>) -> Self {
self.output_path = output_path.into();
self
}
}

#[derive(Debug)]
pub enum SnapshotError {
/// Image did not match snapshot
Expand Down Expand Up @@ -79,22 +124,57 @@ impl Display for SnapshotError {
}
}

/// Image snapshot test.
/// The snapshot will be saved under `tests/snapshots/{name}.png`.
/// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`.
/// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`.
fn should_update_snapshots() -> bool {
std::env::var("UPDATE_SNAPSHOTS").is_ok()
}

fn maybe_update_snapshot(
snapshot_path: &Path,
current: &image::RgbaImage,
) -> Result<(), SnapshotError> {
if should_update_snapshots() {
current
.save(snapshot_path)
.map_err(|err| SnapshotError::WriteSnapshot {
err,
path: snapshot_path.into(),
})?;
println!("Updated snapshot: {snapshot_path:?}");
}
Ok(())
}

/// Image snapshot test with custom options.
///
/// If you want to change the default options for your whole project, it's recommended to create a
/// new `my_image_snapshot` function in your project that calls this function with the desired options.
/// You could additionally use the
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
/// lint to disable use of the [`image_snapshot`] to prevent accidentally using the wrong defaults.
///
/// The snapshot files will be saved under [`SnapshotOptions::output_path`].
/// The snapshot will be saved under `{output_path}/{name}.png`.
/// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`.
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
///
/// # Errors
/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error
/// reading or writing the snapshot.
pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> Result<(), SnapshotError> {
let snapshots_path = Path::new("tests/snapshots");
pub fn try_image_snapshot_options(
current: &image::RgbaImage,
name: &str,
options: &SnapshotOptions,
) -> Result<(), SnapshotError> {
let SnapshotOptions {
threshold,
output_path,
} = options;

let path = snapshots_path.join(format!("{name}.png"));
let path = output_path.join(format!("{name}.png"));
std::fs::create_dir_all(path.parent().expect("Could not get snapshot folder")).ok();

let diff_path = snapshots_path.join(format!("{name}.diff.png"));
let current_path = snapshots_path.join(format!("{name}.new.png"));
let diff_path = output_path.join(format!("{name}.diff.png"));
let current_path = output_path.join(format!("{name}.new.png"));

current
.save(&current_path)
Expand All @@ -119,18 +199,10 @@ pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> Result<(),
});
}

// Looking at dify's source code, the threshold is based on the distance between two colors in
// YIQ color space.
// The default is 0.1.
// We currently need 2.1 because there are slight rendering differences between the different
// wgpu rendering backends, graphics cards and/or operating systems.
// After some testing it seems like 0.6 should be enough for almost all tests to pass.
// Only the `Bézier Curve` demo seems to need a threshold of 2.1.
let threshold = 2.1;
let result = dify::diff::get_results(
previous,
current.clone(),
threshold,
*threshold,
true,
None,
&None,
Expand All @@ -154,24 +226,47 @@ pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> Result<(),
Ok(())
}

fn should_update_snapshots() -> bool {
std::env::var("UPDATE_SNAPSHOTS").is_ok()
/// Image snapshot test.
///
/// This uses the default [`SnapshotOptions`]. Use [`try_image_snapshot_options`] if you want to
/// e.g. change the threshold or output path.
///
/// The snapshot files will be saved under [`SnapshotOptions::output_path`].
/// The snapshot will be saved under `{output_path}/{name}.png`.
/// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`.
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
///
/// # Errors
/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error
/// reading or writing the snapshot.
pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> Result<(), SnapshotError> {
try_image_snapshot_options(current, name, &SnapshotOptions::default())
}

fn maybe_update_snapshot(
snapshot_path: &Path,
current: &image::RgbaImage,
) -> Result<(), SnapshotError> {
if should_update_snapshots() {
current
.save(snapshot_path)
.map_err(|err| SnapshotError::WriteSnapshot {
err,
path: snapshot_path.into(),
})?;
println!("Updated snapshot: {snapshot_path:?}");
/// Image snapshot test with custom options.
///
/// If you want to change the default options for your whole project, it's recommended to create a
/// new `my_image_snapshot` function in your project that calls this function with the desired options.
/// You could additionally use the
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
/// lint to disable use of the [`image_snapshot`] to prevent accidentally using the wrong defaults.
///
/// The snapshot files will be saved under [`SnapshotOptions::output_path`].
/// The snapshot will be saved under `{output_path}/{name}.png`.
/// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`.
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
///
/// # Panics
/// Panics if the image does not match the snapshot or if there was an error reading or writing the
/// snapshot.
#[track_caller]
pub fn image_snapshot_options(current: &image::RgbaImage, name: &str, options: &SnapshotOptions) {
match try_image_snapshot_options(current, name, options) {
Ok(_) => {}
Err(err) => {
panic!("{}", err);
}
}
Ok(())
}

/// Image snapshot test.
Expand All @@ -194,6 +289,33 @@ pub fn image_snapshot(current: &image::RgbaImage, name: &str) {

#[cfg(feature = "wgpu")]
impl Harness<'_> {
/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot
/// with custom options.
///
/// If you want to change the default options for your whole project, you could create an
/// [extension trait](http://xion.io/post/code/rust-extension-traits.html) to create a
/// new `my_image_snapshot` function on the Harness that calls this function with the desired options.
/// You could additionally use the
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
/// lint to disable use of the [`Harness::wgpu_snapshot`] to prevent accidentally using the wrong defaults.
///
/// The snapshot files will be saved under [`SnapshotOptions::output_path`].
/// The snapshot will be saved under `{output_path}/{name}.png`.
/// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`.
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
///
/// # Errors
/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error
/// reading or writing the snapshot.
pub fn try_wgpu_snapshot_options(
&self,
name: &str,
options: &SnapshotOptions,
) -> Result<(), SnapshotError> {
let image = crate::wgpu::TestRenderer::new().render(self);
try_image_snapshot_options(&image, name, options)
}

/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot.
/// The snapshot will be saved under `tests/snapshots/{name}.png`.
/// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`.
Expand All @@ -202,12 +324,39 @@ impl Harness<'_> {
/// # Errors
/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error
/// reading or writing the snapshot.
#[track_caller]
pub fn try_wgpu_snapshot(&self, name: &str) -> Result<(), SnapshotError> {
let image = crate::wgpu::TestRenderer::new().render(self);
try_image_snapshot(&image, name)
}

/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot
/// with custom options.
///
/// If you want to change the default options for your whole project, you could create an
/// [extension trait](http://xion.io/post/code/rust-extension-traits.html) to create a
/// new `my_image_snapshot` function on the Harness that calls this function with the desired options.
/// You could additionally use the
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
/// lint to disable use of the [`Harness::wgpu_snapshot`] to prevent accidentally using the wrong defaults.
///
/// The snapshot files will be saved under [`SnapshotOptions::output_path`].
/// The snapshot will be saved under `{output_path}/{name}.png`.
/// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`.
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
///
/// # Panics
/// Panics if the image does not match the snapshot or if there was an error reading or writing the
/// snapshot.
#[track_caller]
pub fn wgpu_snapshot_options(&self, name: &str, options: &SnapshotOptions) {
match self.try_wgpu_snapshot_options(name, options) {
Ok(_) => {}
Err(err) => {
panic!("{}", err);
}
}
}

/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot.
/// The snapshot will be saved under `tests/snapshots/{name}.png`.
/// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`.
Expand Down

0 comments on commit dafcfda

Please sign in to comment.