Skip to main content

cvkg_render_native/
regression.rs

1use std::path::PathBuf;
2
3/// Native Visual Regression Testing infrastructure.
4/// Captures and compares frames to detect platform-specific visual differences.
5#[derive(Debug, Clone)]
6pub struct VisualRegressionTracker {
7    /// Path to directory where reference "golden" images are located.
8    reference_dir: PathBuf,
9    /// Absolute threshold difference tolerance per pixel component (0 to 255).
10    pixel_tolerance: u8,
11    /// Percentage threshold of allowed mismatched pixels (0.0 to 100.0).
12    max_mismatched_percentage: f64,
13}
14
15impl VisualRegressionTracker {
16    /// Creates a new `VisualRegressionTracker` with specified reference folder and tolerances.
17    pub fn new(reference_dir: PathBuf, pixel_tolerance: u8, max_mismatched_percentage: f64) -> Self {
18        Self {
19            reference_dir,
20            pixel_tolerance,
21            max_mismatched_percentage,
22        }
23    }
24
25    /// Compares a captured PNG byte buffer against a named golden reference file.
26    ///
27    /// If the reference image file does not exist, this function writes the captured PNG
28    /// as the new reference (acting in recording mode) and returns `true`.
29    pub fn verify_frame(&self, test_name: &str, captured_png: &[u8]) -> bool {
30        let reference_path = self.reference_dir.join(format!("{}.png", test_name));
31        if !reference_path.exists() {
32            log::info!("Golden reference for '{}' not found. Recording current capture as reference.", test_name);
33            if let Some(parent) = reference_path.parent() {
34                let _ = std::fs::create_dir_all(parent);
35            }
36            if let Err(e) = std::fs::write(&reference_path, captured_png) {
37                log::error!("Failed to write golden image: {}", e);
38                return false;
39            }
40            return true;
41        }
42
43        // Load reference image
44        let ref_img = match image::load_from_memory(&std::fs::read(&reference_path).unwrap_or_default()) {
45            Ok(img) => img.to_rgba8(),
46            Err(e) => {
47                log::error!("Failed to decode reference image: {}", e);
48                return false;
49            }
50        };
51
52        // Load captured image
53        let cap_img = match image::load_from_memory(captured_png) {
54            Ok(img) => img.to_rgba8(),
55            Err(e) => {
56                log::error!("Failed to decode captured image: {}", e);
57                return false;
58            }
59        };
60
61        if ref_img.dimensions() != cap_img.dimensions() {
62            log::warn!("Dimensions mismatch for test '{}': ref {:?}, cap {:?}", test_name, ref_img.dimensions(), cap_img.dimensions());
63            return false;
64        }
65
66        let (width, height) = ref_img.dimensions();
67        let total_pixels = width as f64 * height as f64;
68        let mut mismatched_pixels = 0;
69
70        for (x, y, ref_pixel) in ref_img.enumerate_pixels() {
71            let cap_pixel = cap_img.get_pixel(x, y);
72            let mut pixel_differs = false;
73            for c in 0..4 {
74                let diff = (ref_pixel[c] as i16 - cap_pixel[c] as i16).abs();
75                if diff > self.pixel_tolerance as i16 {
76                    pixel_differs = true;
77                    break;
78                }
79            }
80            if pixel_differs {
81                mismatched_pixels += 1;
82            }
83        }
84
85        let mismatch_pct = (mismatched_pixels as f64 / total_pixels) * 100.0;
86        if mismatch_pct > self.max_mismatched_percentage {
87            log::warn!("Visual regression detected in test '{}': {:.2}% mismatched pixels (max allowed {:.2}%)",
88                test_name, mismatch_pct, self.max_mismatched_percentage);
89            false
90        } else {
91            true
92        }
93    }
94}