use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tolerance {
pub max_abs: u8,
pub mean_abs: f64,
}
impl Tolerance {
pub const JPEG_TIGHT: Self = Self {
max_abs: 1,
mean_abs: 0.05,
};
pub const TOLERANT: Self = Self {
max_abs: 4,
mean_abs: 1.0,
};
}
#[derive(Debug, Clone)]
pub struct CompareReport {
pub bytewise_equal_rate: f64,
pub max_abs: u8,
pub mean_abs: f64,
pub psnr_db: f64,
pub passed: bool,
pub diff_dump: Option<PathBuf>,
}
pub fn compare_rgba(actual: &[u8], expected: &[u8], tol: Tolerance) -> CompareReport {
assert_eq!(
actual.len(),
expected.len(),
"compare_rgba: length mismatch ({} vs {})",
actual.len(),
expected.len()
);
assert!(
actual.len().is_multiple_of(4),
"compare_rgba: not RGBA-aligned"
);
if actual.is_empty() {
return CompareReport {
bytewise_equal_rate: 1.0,
max_abs: 0,
mean_abs: 0.0,
psnr_db: f64::INFINITY,
passed: true,
diff_dump: None,
};
}
let mut equal = 0u64;
let mut max_abs = 0u8;
let mut sum_abs = 0u64;
let mut sum_sq = 0u64;
for (actual, expected) in actual.iter().zip(expected.iter()) {
if actual == expected {
equal += 1;
}
let d = actual.abs_diff(*expected);
max_abs = max_abs.max(d);
sum_abs += u64::from(d);
sum_sq += u64::from(d) * u64::from(d);
}
let n = actual.len() as f64;
let mean_abs = sum_abs as f64 / n;
let mse = sum_sq as f64 / n;
let psnr_db = if mse == 0.0 {
f64::INFINITY
} else {
10.0 * (255.0_f64 * 255.0 / mse).log10()
};
let passed = max_abs <= tol.max_abs && mean_abs <= tol.mean_abs;
CompareReport {
bytewise_equal_rate: equal as f64 / n,
max_abs,
mean_abs,
psnr_db,
passed,
diff_dump: None,
}
}
pub fn tolerance_failure(label: &str, report: &CompareReport) -> Option<String> {
if report.passed {
return None;
}
Some(format!(
"{label}: exceeds tolerance (max_abs={} mean_abs={:.4} psnr={:.2}dB equal_rate={:.4})",
report.max_abs, report.mean_abs, report.psnr_db, report.bytewise_equal_rate
))
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[test]
fn identical_buffers_pass_with_psnr_inf() {
let a = vec![10u8, 20, 30, 255, 40, 50, 60, 255];
let r = compare_rgba(&a, &a, Tolerance::JPEG_TIGHT);
assert!(r.passed);
assert_eq!(r.max_abs, 0);
assert_eq!(r.mean_abs, 0.0);
assert!(r.psnr_db.is_infinite());
assert_eq!(r.bytewise_equal_rate, 1.0);
}
#[test]
fn off_by_one_passes_jpeg_tight() {
let a = vec![10u8; 32];
let mut b = a.clone();
b[0] = 11;
let r = compare_rgba(&a, &b, Tolerance::JPEG_TIGHT);
assert!(r.passed);
assert_eq!(r.max_abs, 1);
}
#[test]
fn off_by_two_fails_jpeg_tight_passes_tolerant() {
let a = vec![10u8, 20, 30, 255, 40, 50, 60, 255];
let b = vec![12u8, 20, 30, 255, 40, 50, 60, 255];
let tight = compare_rgba(&a, &b, Tolerance::JPEG_TIGHT);
assert!(!tight.passed);
let tol = compare_rgba(&a, &b, Tolerance::TOLERANT);
assert!(tol.passed);
}
#[test]
fn tolerance_failure_formats_failed_report() {
let a = vec![10u8, 20, 30, 255, 40, 50, 60, 255];
let b = vec![12u8, 20, 30, 255, 40, 50, 60, 255];
let report = compare_rgba(&a, &b, Tolerance::JPEG_TIGHT);
let failure = tolerance_failure("svs-001 level=0 signinum-vs-reference", &report)
.expect("failed report should produce gate failure");
assert!(failure.contains("svs-001 level=0 signinum-vs-reference"));
assert!(failure.contains("max_abs=2"));
}
}