Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1457,7 +1457,9 @@ dependencies = [
"kittest",
"open",
"pollster",
"serde",
"tempfile",
"toml",
"wgpu",
]

Expand Down Expand Up @@ -4036,6 +4038,15 @@ dependencies = [
"syn",
]

[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]

[[package]]
name = "serial_windows"
version = "0.1.0"
Expand Down Expand Up @@ -4491,11 +4502,26 @@ dependencies = [
"windows-sys 0.59.0",
]

[[package]]
name = "toml"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]

[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]

[[package]]
name = "toml_edit"
Expand All @@ -4504,6 +4530,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
Expand Down Expand Up @@ -5645,9 +5673,9 @@ dependencies = [

[[package]]
name = "winnow"
version = "0.7.3"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ syntect = { version = "5.3.0", default-features = false }
tempfile = "3.23.0"
thiserror = "2.0.17"
tokio = "1.47.1"
toml = "0.8"
type-map = "0.5.1"
unicode_names2 = { version = "2.0.0", default-features = false }
unicode-segmentation = "1.12.0"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions crates/egui_kittest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ x11 = ["eframe?/x11"]


[dependencies]
kittest.workspace = true
egui = { workspace = true, features = ["accesskit"] }
eframe = { workspace = true, optional = true }
egui = { workspace = true, features = ["accesskit"] }
kittest.workspace = true
serde.workspace = true
toml.workspace = true

# wgpu dependencies
egui-wgpu = { workspace = true, optional = true }
Expand Down
27 changes: 27 additions & 0 deletions crates/egui_kittest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,33 @@ fn main() {
}
```

## Configuration

You can configure test settings via a `kittest.toml` file in your workspace root.
All possible settings and their defaults:
```toml
# path to the snapshot directory
output_path = "tests/snapshots"

# default threshold for image comparison tests
threshold = 0.6

# default failed_pixel_count_threshold
failed_pixel_count_threshold = 0

[windows]
threshold = 0.6
failed_pixel_count_threshold = 0

[macos]
threshold = 0.6
failed_pixel_count_threshold = 0

[linux]
threshold = 0.6
failed_pixel_count_threshold = 0
```

## Snapshot testing
There is a snapshot testing feature. To create snapshot tests, enable the `snapshot` and `wgpu` features.
Once enabled, you can call `Harness::snapshot` to render the ui and save the image to the `tests/snapshots` directory.
Expand Down
131 changes: 131 additions & 0 deletions crates/egui_kittest/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use crate::OsThreshold;
use std::io;
use std::path::PathBuf;

/// Configuration for `egui_kittest`.
///
/// It's loaded once (per process) by searching for a `kittest.toml` file in the project root
/// (the directory containing `Cargo.lock`).
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
output_path: PathBuf,

threshold: f32,
failed_pixel_count_threshold: usize,

windows: OsConfig,
mac: OsConfig,
linux: OsConfig,
}

impl Default for Config {
fn default() -> Self {
Self {
output_path: PathBuf::from("tests/snapshots"),
threshold: 0.6,
failed_pixel_count_threshold: 0,
windows: Default::default(),
mac: Default::default(),
linux: Default::default(),
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct OsConfig {
threshold: Option<f32>,
failed_pixel_count_threshold: Option<usize>,
}

fn find_project_root() -> io::Result<std::path::PathBuf> {
let mut current_dir = std::env::current_dir()?;

loop {
// Check if Cargo.toml exists in this directory
if current_dir.join("Cargo.lock").exists() {
return Ok(current_dir);
}

// Move up one directory
if !current_dir.pop() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"Project root not found",
));
}
}
}

fn load_config() -> Config {
let project_root = find_project_root();

if let Ok(project_root) = project_root {
let config_path = project_root.join("kittest.toml");
if config_path.exists() {
let config_str =
std::fs::read_to_string(config_path).expect("Failed to read config file");
match toml::from_str(&config_str) {
Ok(config) => return config,
Err(e) => panic!("Failed to parse config file: {e}"),
};
}
}

Config::default()
}

pub fn config() -> &'static Config {
Config::get()
}

impl Config {
pub fn get() -> &'static Self {
static INSTANCE: std::sync::LazyLock<Config> = std::sync::LazyLock::new(load_config);
&INSTANCE
}

pub fn os_threshold(&self) -> OsThreshold<f32> {
let fallback = self.threshold;
OsThreshold {
windows: self.windows.threshold.unwrap_or(fallback),
macos: self.mac.threshold.unwrap_or(fallback),
linux: self.linux.threshold.unwrap_or(fallback),
fallback,
}
}

pub fn os_failed_pixel_count_threshold(&self) -> OsThreshold<usize> {
let fallback = self.failed_pixel_count_threshold;
OsThreshold {
windows: self
.windows
.failed_pixel_count_threshold
.unwrap_or(fallback),
macos: self.mac.failed_pixel_count_threshold.unwrap_or(fallback),
linux: self.linux.failed_pixel_count_threshold.unwrap_or(fallback),
fallback,
}
}

/// The threshold.
///
/// Default is 1.0.
pub fn threshold(&self) -> f32 {
self.os_threshold().threshold()
}

/// The number of pixels that can differ before the test is considered failed.
///
/// Default is 0.
pub fn failed_pixel_count_threshold(&self) -> usize {
self.os_failed_pixel_count_threshold().threshold()
}

/// The output path for image snapshots.
///
/// Default is "tests/snapshots".
pub fn output_path(&self) -> PathBuf {
self.output_path.clone()
}
}
2 changes: 2 additions & 0 deletions crates/egui_kittest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ use std::fmt::{Debug, Display, Formatter};
use std::time::Duration;

mod app_kind;
mod config;
mod node;
mod renderer;
#[cfg(feature = "wgpu")]
mod texture_to_image;
#[cfg(feature = "wgpu")]
pub mod wgpu;

pub(crate) use config::config;
pub use kittest;

use crate::app_kind::AppKind;
Expand Down
43 changes: 37 additions & 6 deletions crates/egui_kittest/src/snapshot.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
use crate::Harness;
use image::ImageError;
use std::fmt::Display;
use std::io::ErrorKind;
use std::path::PathBuf;

use image::ImageError;

use crate::{Harness, config};

pub type SnapshotResult = Result<(), SnapshotError>;

#[non_exhaustive]
#[derive(Clone, Debug)]
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 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,
}
Expand All @@ -30,7 +35,9 @@ pub struct SnapshotOptions {
///
/// This is useful if you want to set different thresholds for different operating systems.
///
/// The default values are 0 / 0.0
/// [`OsThreshold::default`] gets the default from the config file (`kittest.toml`).
/// For `usize`, it's the `failed_pixel_count_threshold` value.
/// For `f32`, it's the `threshold` value.
///
/// Example usage:
/// ```no_run
Expand All @@ -53,12 +60,36 @@ pub struct OsThreshold<T> {
pub fallback: T,
}

impl Default for OsThreshold<usize> {
/// Returns the default `failed_pixel_count_threshold` as configured in `kittest.toml`
///
/// The default is `0`.
fn default() -> Self {
config().os_failed_pixel_count_threshold()
}
}

impl Default for OsThreshold<f32> {
/// Returns the default `threshold` as configured in `kittest.toml`
///
/// The default is `0.6`.
fn default() -> Self {
config().os_threshold()
}
}

impl From<usize> for OsThreshold<usize> {
fn from(value: usize) -> Self {
Self::new(value)
}
}

impl From<f32> for OsThreshold<f32> {
fn from(value: f32) -> Self {
Self::new(value)
}
}

impl<T> OsThreshold<T>
where
T: Copy,
Expand Down Expand Up @@ -123,9 +154,9 @@ impl From<OsThreshold<Self>> for f32 {
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
threshold: config().threshold(),
output_path: config().output_path(),
failed_pixel_count_threshold: config().failed_pixel_count_threshold(),
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions kittest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
output_path = "tests/snapshots"

# Other oses get a higher threshold so they can still run tests locally without failures due to small rendering
# differences.
# To update snapshots, update them via ./scripts/update_snapshots_from_ci.sh or via kitdiff
threshold = 2.0

[mac]
# Since our CI runs snapshot tests on macOS, this is our source of truth.
threshold = 0.6
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to go lower here, but the redering test fails with a 0.6 between CI mac and my mac. The other tests are fine with 0.1 though