#![deny(missing_docs)]
#[cfg(feature = "h264")]
mod h264;
#[cfg(feature = "h264")]
pub use h264::assert_h264_frame;
const CRATE_ENV_VAR: &str = "TWENTY_TWENTY";
#[derive(Default, PartialEq)]
enum Mode {
#[default]
Default,
Overwrite,
UpdateOnMismatch,
StoreArtifact,
StoreArtifactOnMismatch,
}
impl std::str::FromStr for Mode {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Ok(match s {
"overwrite" => Mode::Overwrite,
"update" => Mode::UpdateOnMismatch,
"store-artifact" => Mode::StoreArtifact,
"store-artifact-on-mismatch" => Mode::StoreArtifactOnMismatch,
_ => Mode::Default,
})
}
}
#[track_caller]
pub fn assert_image<P: AsRef<std::path::Path>>(path: P, actual: &image::DynamicImage, min_permissible_similarity: f64) {
if let Err(e) = assert_image_impl(path, actual, min_permissible_similarity) {
panic!("assertion failed: {e}")
}
}
pub(crate) fn assert_image_impl<P: AsRef<std::path::Path>>(
path: P,
actual: &image::DynamicImage,
min_permissible_similarity: f64,
) -> anyhow::Result<()> {
let path = path.as_ref();
let var = std::env::var_os(CRATE_ENV_VAR);
let mode: Mode = var
.as_deref()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or_default()
.parse()
.unwrap_or_default();
if mode == Mode::Overwrite {
if let Err(e) = actual.save_with_format(path, image::ImageFormat::Png) {
panic!("unable to write image to {}: {}", path.display(), e);
}
return Ok(());
}
let expected = match image::io::Reader::open(path) {
Ok(s) => s.decode().expect("decoding image from path failed"),
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => image::DynamicImage::new_rgba16(actual.width(), actual.height()),
_ => panic!("unable to read contents of {}: {}", path.display(), e),
},
};
let result = match image_compare::rgba_hybrid_compare(&expected.to_rgba8(), &actual.to_rgba8()) {
Ok(result) => result,
Err(err) => {
panic!("could not compare the images {err}")
}
};
let image_mismatch = result.score < min_permissible_similarity;
if mode == Mode::StoreArtifact || (mode == Mode::StoreArtifactOnMismatch && image_mismatch) {
let artifact_path = std::path::Path::new("artifacts/").join(path);
if let Some(parent) = artifact_path.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
panic!("unable to create directory {}: {e}", parent.display());
}
}
if let Err(e) = actual.save_with_format(artifact_path, image::ImageFormat::Png) {
panic!("unable to write image to {}: {}", path.display(), e);
}
}
if image_mismatch {
if mode == Mode::UpdateOnMismatch {
if let Err(e) = actual.save_with_format(path, image::ImageFormat::Png) {
panic!("unable to write image to {}: {}", path.display(), e);
}
return Ok(());
}
anyhow::bail!(
r#"image (`{}`) score is `{}` which is less than min_permissible_similarity `{}`
set {}=overwrite if these changes are intentional"#,
path.display(),
result.score,
min_permissible_similarity,
CRATE_ENV_VAR
)
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::assert_image;
#[test]
fn test_overwrite_mode() {
std::fs::create_dir_all("tests/tmp").unwrap();
std::fs::copy("tests/dog1.png", "tests/tmp/initial-grid.png").unwrap();
let expected_image = image::io::Reader::open("tests/initial-grid.png")
.unwrap()
.decode()
.unwrap();
std::env::set_var("TWENTY_TWENTY", "overwrite");
assert_image("tests/tmp/initial-grid.png", &expected_image, 1.0);
std::env::set_var("TWENTY_TWENTY", "");
assert_image("tests/tmp/initial-grid.png", &expected_image, 1.0);
}
#[test]
fn test_store_artifact_mode() {
let expected_image = image::io::Reader::open("tests/initial-grid.png")
.unwrap()
.decode()
.unwrap();
std::env::set_var("TWENTY_TWENTY", "store-artifact");
assert_image("tests/initial-grid.png", &expected_image, 1.0);
std::env::set_var("TWENTY_TWENTY", "");
assert_image("artifacts/tests/initial-grid.png", &expected_image, 1.0);
}
#[test]
fn test_store_artifact_if_mismatch_mode() {
let expected_image = image::io::Reader::open("tests/initial-grid.png")
.unwrap()
.decode()
.unwrap();
std::env::set_var("TWENTY_TWENTY", "store-artifact-on-mismatch");
let _result = std::panic::catch_unwind(|| {
assert_image("tests/multiple-frames.png", &expected_image, 1.0);
});
std::env::set_var("TWENTY_TWENTY", "");
assert_image("artifacts/tests/multiple-frames.png", &expected_image, 1.0);
}
}