#![cfg(feature = "snapshot")]
use std::io;
use std::path::PathBuf;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
output_path: PathBuf,
threshold: f32,
#[serde(alias = "failed_pixel_count_threshold")]
max_failed_pixels: usize,
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,
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>,
#[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");
if current_kittest.exists() {
return Ok(current_kittest);
}
if !current_dir.pop() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"kittest.toml not found",
));
}
}
}
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()
}
}
pub fn config() -> &'static Config {
Config::global()
}
impl Config {
pub fn global() -> &'static Self {
static INSTANCE: std::sync::LazyLock<Config> = std::sync::LazyLock::new(load_config);
&INSTANCE
}
pub fn output_path(&self) -> PathBuf {
self.output_path.clone()
}
}
#[cfg(feature = "snapshot")]
impl Config {
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,
}
}
pub fn threshold(&self) -> f32 {
self.os_threshold().threshold()
}
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));
}
}