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(
18        reference_dir: PathBuf,
19        pixel_tolerance: u8,
20        max_mismatched_percentage: f64,
21    ) -> Self {
22        Self {
23            reference_dir,
24            pixel_tolerance,
25            max_mismatched_percentage,
26        }
27    }
28
29    /// Compares a captured PNG byte buffer against a named golden reference file.
30    ///
31    /// If the reference image file does not exist, this function writes the captured PNG
32    /// as the new reference (acting in recording mode) and returns `true`.
33    pub fn verify_frame(&self, test_name: &str, captured_png: &[u8]) -> bool {
34        let reference_path = self.reference_dir.join(format!("{}.png", test_name));
35        if !reference_path.exists() {
36            log::info!(
37                "Golden reference for '{}' not found. Recording current capture as reference.",
38                test_name
39            );
40            if let Some(parent) = reference_path.parent() {
41                let _ = std::fs::create_dir_all(parent);
42            }
43            if let Err(e) = std::fs::write(&reference_path, captured_png) {
44                log::error!("Failed to write golden image: {}", e);
45                return false;
46            }
47            return true;
48        }
49
50        // Load reference image
51        let ref_img =
52            match image::load_from_memory(&std::fs::read(&reference_path).unwrap_or_default()) {
53                Ok(img) => img.to_rgba8(),
54                Err(e) => {
55                    log::error!("Failed to decode reference image: {}", e);
56                    return false;
57                }
58            };
59
60        // Load captured image
61        let cap_img = match image::load_from_memory(captured_png) {
62            Ok(img) => img.to_rgba8(),
63            Err(e) => {
64                log::error!("Failed to decode captured image: {}", e);
65                return false;
66            }
67        };
68
69        if ref_img.dimensions() != cap_img.dimensions() {
70            log::warn!(
71                "Dimensions mismatch for test '{}': ref {:?}, cap {:?}",
72                test_name,
73                ref_img.dimensions(),
74                cap_img.dimensions()
75            );
76            return false;
77        }
78
79        let (width, height) = ref_img.dimensions();
80        let total_pixels = width as f64 * height as f64;
81        let mut mismatched_pixels = 0;
82
83        for (x, y, ref_pixel) in ref_img.enumerate_pixels() {
84            let cap_pixel = cap_img.get_pixel(x, y);
85            let mut pixel_differs = false;
86            for c in 0..4 {
87                let diff = (ref_pixel[c] as i16 - cap_pixel[c] as i16).abs();
88                if diff > self.pixel_tolerance as i16 {
89                    pixel_differs = true;
90                    break;
91                }
92            }
93            if pixel_differs {
94                mismatched_pixels += 1;
95            }
96        }
97
98        let mismatch_pct = (mismatched_pixels as f64 / total_pixels) * 100.0;
99        if mismatch_pct > self.max_mismatched_percentage {
100            log::warn!(
101                "Visual regression detected in test '{}': {:.2}% mismatched pixels (max allowed {:.2}%)",
102                test_name,
103                mismatch_pct,
104                self.max_mismatched_percentage
105            );
106            false
107        } else {
108            true
109        }
110    }
111}