use std::path::Path;
use crate::config::Result;
#[derive(Debug)]
pub struct ValidationResult {
pub match_pct: f64,
pub passed: bool,
pub diff_path: Option<String>,
}
pub fn validate(
actual: &Path,
golden: &Path,
tolerance: f64,
diff_output: Option<&Path>,
) -> Result<ValidationResult> {
let golden_img = image::open(golden).map_err(|e| {
crate::config::TestbedError::Qcow2Error {
message: format!("loading golden image {golden:?}: {e}"),
}
})?;
let actual_img = image::open(actual).map_err(|e| {
crate::config::TestbedError::Qcow2Error {
message: format!("loading actual image {actual:?}: {e}"),
}
})?;
let (gw, gh) = (golden_img.width(), golden_img.height());
let actual_resized = actual_img.resize_exact(gw, gh, image::imageops::FilterType::Lanczos3);
let golden_rgba = golden_img.to_rgba8();
let actual_rgba = actual_resized.to_rgba8();
let total_pixels = (gw as u64) * (gh as u64);
let mut matching_pixels: u64 = 0;
let mut diff_img = if diff_output.is_some() {
Some(image::RgbaImage::new(gw, gh))
} else {
None
};
for y in 0..gh {
for x in 0..gw {
let gp = golden_rgba.get_pixel(x, y);
let ap = actual_rgba.get_pixel(x, y);
let matches = gp.0.iter().zip(ap.0.iter()).all(|(g, a)| {
(*g as i16 - *a as i16).abs() < 16
});
if matches {
matching_pixels += 1;
if let Some(ref mut diff) = diff_img {
diff.put_pixel(x, y, image::Rgba([0, 255, 0, 255])); }
} else {
if let Some(ref mut diff) = diff_img {
diff.put_pixel(x, y, image::Rgba([255, 0, 0, 255])); }
}
}
}
let match_pct = (matching_pixels as f64 / total_pixels as f64) * 100.0;
let passed = match_pct >= tolerance;
let diff_path = if passed {
None
} else if let (Some(diff), Some(out_path)) = (diff_img, diff_output) {
let path_str = out_path.to_string_lossy().to_string();
diff.save(out_path).map_err(|e| {
crate::config::TestbedError::Qcow2Error {
message: format!("saving diff image {out_path:?}: {e}"),
}
})?;
Some(path_str)
} else {
None
};
Ok(ValidationResult {
match_pct,
passed,
diff_path,
})
}