use std::{
fs, io,
path::{Path, PathBuf},
};
use image::{GenericImageView, ImageError};
use log::warn;
use oicana::{Template, TemplateInitializationError};
use oicana_export::png::{export_merged_png, EncodingError};
use oicana_files::native::{package_data_dir, NativeTemplate};
use oicana_template::manifest::TemplateManifest;
use oicana_world::{CompiledDocument, TemplateCompilationFailure};
use thiserror::Error;
use crate::Test;
pub struct TestRunnerContext {
packages: PathBuf,
}
impl TestRunnerContext {
pub fn new() -> Result<Self, CreateTestRunnerError> {
let packages = package_data_dir().ok_or(CreateTestRunnerError::NoPackageDirectory)?;
Ok(TestRunnerContext { packages })
}
pub fn get_runner(
&self,
path: &Path,
manifest: &TemplateManifest,
) -> Result<TestRunner, TemplateInitializationError> {
Ok(TestRunner {
instance: Template::<NativeTemplate>::from(path, &self.packages, manifest.clone())?,
})
}
}
pub struct TestRunner {
instance: Template<NativeTemplate>,
}
impl TestRunner {
pub fn run(&mut self, test: Test) -> Result<Vec<String>, TestExecutionError> {
let CompiledDocument { document, warnings } = self.instance.compile(test.inputs)?;
let mut warnings = if let Some(warning) = warnings {
vec![warning]
} else {
vec![]
};
let image = export_merged_png(&document, 1.)?;
match test.snapshot {
crate::Snapshot::Missing(path) => {
warnings.push(format!(
"Writing snapshot file at {path:?}, because it was missing"
));
fs::write(path, image)?;
}
crate::Snapshot::Some(path) => {
if !compare_images(&path, &image, 1)? {
let mut compare_path = path.clone();
if let Some(stem) = compare_path.file_stem() {
let mut new_name = stem.to_os_string();
new_name.push(".compare.png");
compare_path.set_file_name(new_name);
} else {
warn!("Snapshot file had no file stem");
}
fs::write(compare_path, image)?;
return Err(TestExecutionError::SnapshotMismatch);
}
}
_ => (),
}
Ok(warnings)
}
}
pub fn compare_images(path: &Path, data: &[u8], tolerance: u8) -> Result<bool, ImageError> {
let img1 = image::open(path)?;
let img2 = image::load_from_memory(data)?;
if img1.dimensions() != img2.dimensions() {
return Ok(false);
}
let pixels1 = img1.pixels();
let pixels2 = img2.pixels();
for ((_, _, p1), (_, _, p2)) in pixels1.zip(pixels2) {
let diff =
p1.0.iter()
.zip(p2.0.iter())
.map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u8)
.max()
.unwrap_or(0);
if diff > tolerance {
return Ok(false);
}
}
Ok(true)
}
#[derive(Debug, Error)]
pub enum CreateTestRunnerError {
#[error("Failed to get Typst package directory.")]
NoPackageDirectory,
}
#[derive(Debug, Error)]
pub enum TestExecutionError {
#[error("{0}")]
CompilationError(#[from] TemplateCompilationFailure),
#[error("{0}")]
ExportError(#[from] EncodingError),
#[error("Failed to write or read snapshot image: {0}")]
Io(#[from] io::Error),
#[error("Failed to compare the snapshot: {0}")]
Comparison(#[from] ImageError),
#[error("The snapshot does not match the result.")]
SnapshotMismatch,
}