mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 14:20:04 -04:00
This allows you to see how many more steps it would have taken for the harness to settle. Useful to see if you're in a infinite repaint loop or just need a `harness.run_steps(3)` to give it some more frames to settle. Should make it easier to make the default step count tighter, which should make tests faster.
238 lines
7.2 KiB
Rust
238 lines
7.2 KiB
Rust
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 {
|
|
/// The output path for image snapshots.
|
|
///
|
|
/// Default is "tests/snapshots" (relative to the working directory / crate root).
|
|
output_path: PathBuf,
|
|
|
|
/// The maximum weighted squared YIQ color distance between two corresponding pixels.
|
|
///
|
|
/// Pixels that differ by more than this are counted as failing.
|
|
/// This is an absolute, per-pixel value, and does not depend on the image dimensions.
|
|
///
|
|
/// Default is 0.6.
|
|
threshold: f32,
|
|
|
|
/// The number of pixels that may fail the [`Self::threshold`] before the test is
|
|
/// considered failed.
|
|
///
|
|
/// Default is 0.
|
|
#[serde(alias = "failed_pixel_count_threshold")]
|
|
max_failed_pixels: usize,
|
|
|
|
/// How far past `max_steps` [`crate::Harness::try_run`] keeps stepping to find out how many
|
|
/// steps the ui would have needed.
|
|
///
|
|
/// This tells a budget which is slightly too tight apart from a ui that never stops repainting.
|
|
///
|
|
/// Default is 100.
|
|
diagnostic_max_steps: u64,
|
|
|
|
windows: OsConfig,
|
|
mac: OsConfig,
|
|
linux: OsConfig,
|
|
}
|
|
|
|
impl Default for Config {
|
|
fn default() -> Self {
|
|
Self {
|
|
output_path: PathBuf::from("tests/snapshots"),
|
|
threshold: 0.6,
|
|
max_failed_pixels: 0,
|
|
diagnostic_max_steps: 100,
|
|
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 {
|
|
/// Override the per-pixel threshold for this OS.
|
|
threshold: Option<f32>,
|
|
|
|
/// Override the maximum number of failing pixels for this OS.
|
|
#[serde(alias = "failed_pixel_count_threshold")]
|
|
max_failed_pixels: Option<usize>,
|
|
}
|
|
|
|
fn find_kittest_toml() -> io::Result<std::path::PathBuf> {
|
|
let mut current_dir = std::env::current_dir()?;
|
|
|
|
loop {
|
|
let current_kittest = current_dir.join("kittest.toml");
|
|
// Check if Cargo.toml exists in this directory
|
|
if current_kittest.exists() {
|
|
return Ok(current_kittest);
|
|
}
|
|
|
|
// Move up one directory
|
|
if !current_dir.pop() {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
"kittest.toml not found",
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The old name of `max_failed_pixels` is still accepted, but warned about.
|
|
fn warn_about_deprecated_keys(config_str: &str) {
|
|
let Ok(config) = toml::from_str::<toml::Table>(config_str) else {
|
|
return;
|
|
};
|
|
|
|
let mut sections = vec![("", &config)];
|
|
for name in ["windows", "mac", "linux"] {
|
|
if let Some(table) = config.get(name).and_then(toml::Value::as_table) {
|
|
sections.push((name, table));
|
|
}
|
|
}
|
|
|
|
for (section, table) in sections {
|
|
if table.contains_key("failed_pixel_count_threshold") {
|
|
let prefix = if section.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!("{section}.")
|
|
};
|
|
log::warn!(
|
|
"`{prefix}failed_pixel_count_threshold` in kittest.toml is deprecated; \
|
|
use `{prefix}max_failed_pixels` instead."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn load_config() -> Config {
|
|
if let Ok(config_path) = find_kittest_toml() {
|
|
match std::fs::read_to_string(&config_path) {
|
|
Ok(config_str) => {
|
|
warn_about_deprecated_keys(&config_str);
|
|
match toml::from_str(&config_str) {
|
|
Ok(config) => config,
|
|
Err(err) => panic!("Failed to parse {}: {err}", config_path.display()),
|
|
}
|
|
}
|
|
Err(err) => {
|
|
panic!("Failed to read {}: {}", config_path.display(), err);
|
|
}
|
|
}
|
|
} else {
|
|
Config::default()
|
|
}
|
|
}
|
|
|
|
/// Get the global configuration.
|
|
///
|
|
/// See [`Config::global`] for details.
|
|
pub fn config() -> &'static Config {
|
|
Config::global()
|
|
}
|
|
|
|
impl Config {
|
|
/// Get or load the global configuration.
|
|
///
|
|
/// This is either
|
|
/// - Based on a `kittest.toml`, found by searching from the current working directory
|
|
/// (for tests that is the crate root) upwards.
|
|
/// - The default [Config], if no `kittest.toml` is found.
|
|
pub fn global() -> &'static Self {
|
|
static INSTANCE: std::sync::LazyLock<Config> = std::sync::LazyLock::new(load_config);
|
|
&INSTANCE
|
|
}
|
|
|
|
/// How far past `max_steps` [`crate::Harness::try_run`] keeps stepping to find out how many
|
|
/// steps the ui would have needed.
|
|
///
|
|
/// Default is 100.
|
|
pub fn diagnostic_max_steps(&self) -> u64 {
|
|
self.diagnostic_max_steps
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "snapshot")]
|
|
impl Config {
|
|
/// The output path for image snapshots.
|
|
///
|
|
/// Default is "tests/snapshots".
|
|
pub fn output_path(&self) -> PathBuf {
|
|
self.output_path.clone()
|
|
}
|
|
|
|
pub fn os_threshold(&self) -> crate::OsThreshold<f32> {
|
|
let fallback = self.threshold;
|
|
crate::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_max_failed_pixels(&self) -> crate::OsThreshold<usize> {
|
|
let fallback = self.max_failed_pixels;
|
|
crate::OsThreshold {
|
|
windows: self.windows.max_failed_pixels.unwrap_or(fallback),
|
|
macos: self.mac.max_failed_pixels.unwrap_or(fallback),
|
|
linux: self.linux.max_failed_pixels.unwrap_or(fallback),
|
|
fallback,
|
|
}
|
|
}
|
|
|
|
/// The maximum weighted squared YIQ color distance between two corresponding pixels.
|
|
///
|
|
/// This is an absolute, per-pixel value, and does not depend on the image dimensions.
|
|
///
|
|
/// Default is 0.6.
|
|
pub fn threshold(&self) -> f32 {
|
|
self.os_threshold().threshold()
|
|
}
|
|
|
|
/// The number of pixels that may fail the [`Self::threshold`] before the test is
|
|
/// considered failed.
|
|
///
|
|
/// Default is 0.
|
|
pub fn max_failed_pixels(&self) -> usize {
|
|
self.os_max_failed_pixels().threshold()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::Config;
|
|
|
|
#[test]
|
|
fn deprecated_failed_pixel_count_threshold_key_is_accepted() {
|
|
let config: Config = toml::from_str(
|
|
r"
|
|
failed_pixel_count_threshold = 1
|
|
|
|
[windows]
|
|
failed_pixel_count_threshold = 2
|
|
|
|
[mac]
|
|
failed_pixel_count_threshold = 3
|
|
|
|
[linux]
|
|
failed_pixel_count_threshold = 4
|
|
",
|
|
)
|
|
.unwrap_or_else(|err| panic!("Failed to parse config: {err}"));
|
|
|
|
assert_eq!(config.max_failed_pixels, 1);
|
|
assert_eq!(config.windows.max_failed_pixels, Some(2));
|
|
assert_eq!(config.mac.max_failed_pixels, Some(3));
|
|
assert_eq!(config.linux.max_failed_pixels, Some(4));
|
|
}
|
|
}
|