1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-02 23:00:04 -04:00

Add SnapshotResults struct to egui_kittest (#5672)

I got annoyed by all the slightly different variations of "collect
snapshot results and unwrap them at the end of test" I've written, so I
added a struct to make this nice and simple.

One controversial thing: It panics when dropped. I wanted to ensure
people cannot forget to unwrap the results at the end, and this was the
best thing I could come up with. I don't think this is possible via
clippy lint or something like that.

* [x] I have followed the instructions in the PR template
This commit is contained in:
lucasmerlin
2025-02-04 14:01:32 +01:00
committed by GitHub
parent 23ed49334e
commit b8051cc301
10 changed files with 135 additions and 53 deletions

View File

@@ -4,6 +4,8 @@ use std::fmt::Display;
use std::io::ErrorKind;
use std::path::PathBuf;
pub type SnapshotResult = Result<(), SnapshotError>;
#[non_exhaustive]
pub struct SnapshotOptions {
/// The threshold for the image comparison.
@@ -189,7 +191,7 @@ pub fn try_image_snapshot_options(
new: &image::RgbaImage,
name: &str,
options: &SnapshotOptions,
) -> Result<(), SnapshotError> {
) -> SnapshotResult {
let SnapshotOptions {
threshold,
output_path,
@@ -306,7 +308,7 @@ pub fn try_image_snapshot_options(
/// # 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> {
pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> SnapshotResult {
try_image_snapshot_options(current, name, &SnapshotOptions::default())
}
@@ -378,7 +380,7 @@ impl<State> Harness<'_, State> {
&mut self,
name: &str,
options: &SnapshotOptions,
) -> Result<(), SnapshotError> {
) -> SnapshotResult {
let image = self
.render()
.map_err(|err| SnapshotError::RenderError { err })?;
@@ -393,7 +395,7 @@ impl<State> Harness<'_, State> {
/// # Errors
/// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an
/// error reading or writing the snapshot, if the rendering fails or if no default renderer is available.
pub fn try_snapshot(&mut self, name: &str) -> Result<(), SnapshotError> {
pub fn try_snapshot(&mut self, name: &str) -> SnapshotResult {
let image = self
.render()
.map_err(|err| SnapshotError::RenderError { err })?;
@@ -460,7 +462,7 @@ impl<State> Harness<'_, State> {
&mut self,
name: &str,
options: &SnapshotOptions,
) -> Result<(), SnapshotError> {
) -> SnapshotResult {
self.try_snapshot_options(name, options)
}
@@ -468,7 +470,7 @@ impl<State> Harness<'_, State> {
since = "0.31.0",
note = "Use `try_snapshot` instead. This function will be removed in 0.32"
)]
pub fn try_wgpu_snapshot(&mut self, name: &str) -> Result<(), SnapshotError> {
pub fn try_wgpu_snapshot(&mut self, name: &str) -> SnapshotResult {
self.try_snapshot(name)
}
@@ -488,3 +490,105 @@ impl<State> Harness<'_, State> {
self.snapshot(name);
}
}
/// Utility to collect snapshot errors and display them at the end of the test.
///
/// # Example
/// ```
/// # let harness = MockHarness;
/// # struct MockHarness;
/// # impl MockHarness {
/// # fn try_snapshot(&self, _: &str) -> Result<(), egui_kittest::SnapshotError> { Ok(()) }
/// # }
///
/// // [...] Construct a Harness
///
/// let mut results = egui_kittest::SnapshotResults::new();
///
/// // Call add for each snapshot in your test
/// results.add(harness.try_snapshot("my_test"));
///
/// // If there are any errors, SnapshotResults will panic once dropped.
/// ```
///
/// # Panics
/// Panics if there are any errors when dropped (this way it is impossible to forget to call `unwrap`).
/// If you don't want to panic, you can use [`SnapshotResults::into_result`] or [`SnapshotResults::into_inner`].
/// If you want to panic early, you can use [`SnapshotResults::unwrap`].
#[derive(Debug, Default)]
pub struct SnapshotResults {
errors: Vec<SnapshotError>,
}
impl Display for SnapshotResults {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.errors.is_empty() {
write!(f, "All snapshots passed")
} else {
writeln!(f, "Snapshot errors:")?;
for error in &self.errors {
writeln!(f, " {error}")?;
}
Ok(())
}
}
}
impl SnapshotResults {
pub fn new() -> Self {
Default::default()
}
/// Check if the result is an error and add it to the list of errors.
pub fn add(&mut self, result: SnapshotResult) {
if let Err(err) = result {
self.errors.push(err);
}
}
/// Check if there are any errors.
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
/// Convert this into a `Result<(), Self>`.
#[allow(clippy::missing_errors_doc)]
pub fn into_result(self) -> Result<(), Self> {
if self.has_errors() {
Err(self)
} else {
Ok(())
}
}
pub fn into_inner(mut self) -> Vec<SnapshotError> {
std::mem::take(&mut self.errors)
}
/// Panics if there are any errors, displaying each.
#[allow(clippy::unused_self)]
#[track_caller]
pub fn unwrap(self) {
// Panic is handled in drop
}
}
impl From<SnapshotResults> for Vec<SnapshotError> {
fn from(results: SnapshotResults) -> Self {
results.into_inner()
}
}
impl Drop for SnapshotResults {
#[track_caller]
fn drop(&mut self) {
// Don't panic if we are already panicking (the test probably failed for another reason)
if std::thread::panicking() {
return;
}
#[allow(clippy::manual_assert)]
if self.has_errors() {
panic!("{}", self);
}
}
}