Skip to main content

cvkg_render_gpu/types/
golden.rs

1// =============================================================================
2// P2-29: Golden-Image Test Infrastructure
3// =============================================================================
4
5/// Configuration for golden-image comparison tests.
6#[derive(Clone, Debug)]
7pub struct GoldenImageConfig {
8    /// Per-pixel tolerance (0-255).
9    pub pixel_tolerance: u8,
10    /// Maximum percentage of differing pixels allowed.
11    pub max_diff_percent: f32,
12    /// Whether to update golden images on mismatch (for CI).
13    pub update_on_mismatch: bool,
14}
15
16impl Default for GoldenImageConfig {
17    fn default() -> Self {
18        Self {
19            pixel_tolerance: 3,
20            max_diff_percent: 0.1,
21            update_on_mismatch: false,
22        }
23    }
24}
25
26/// Result of a golden-image comparison.
27#[derive(Clone, Debug)]
28pub struct GoldenImageResult {
29    /// Whether the test passed.
30    pub passed: bool,
31    /// Percentage of pixels that differed.
32    pub diff_percent: f32,
33    /// Number of pixels that differed.
34    pub diff_count: u64,
35    /// Total number of pixels compared.
36    pub total_pixels: u64,
37}
38
39/// Golden-image comparator for render output validation.
40pub struct GoldenImageComparator;
41
42impl GoldenImageComparator {
43    /// Compare two RGBA pixel buffers and return the comparison result.
44    ///
45    /// # Contract
46    /// - Both buffers must have the same length. If lengths differ, the test fails with 100% diff.
47    /// - If buffers are empty, the test passes with 0% diff.
48    /// - Compares RGB channels only, skipping the alpha channel.
49    pub fn compare(
50        actual: &[u8],
51        expected: &[u8],
52        config: &GoldenImageConfig,
53    ) -> GoldenImageResult {
54        if actual.len() != expected.len() {
55            return GoldenImageResult {
56                passed: false,
57                diff_percent: 100.0,
58                diff_count: actual.len() as u64 / 4,
59                total_pixels: actual.len() as u64 / 4,
60            };
61        }
62
63        let total_pixels = (actual.len() / 4) as u64;
64        if total_pixels == 0 {
65            return GoldenImageResult {
66                passed: true,
67                diff_percent: 0.0,
68                diff_count: 0,
69                total_pixels: 0,
70            };
71        }
72
73        let mut diff_count = 0u64;
74        for i in 0..(actual.len() / 4) {
75            let base = i * 4;
76            let mut pixel_differs = false;
77            for ch in 0..3 {
78                // Compare RGB only (skip alpha)
79                if actual[base + ch].abs_diff(expected[base + ch]) > config.pixel_tolerance {
80                    pixel_differs = true;
81                    break;
82                }
83            }
84            if pixel_differs {
85                diff_count += 1;
86            }
87        }
88
89        let diff_percent = (diff_count as f32 / total_pixels as f32) * 100.0;
90        GoldenImageResult {
91            passed: diff_percent <= config.max_diff_percent,
92            diff_percent,
93            diff_count,
94            total_pixels,
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn golden_image_identical() {
105        let pixels = vec![255u8; 400]; // 10x10 white
106        let config = GoldenImageConfig::default();
107        let result = GoldenImageComparator::compare(&pixels, &pixels, &config);
108        assert!(result.passed);
109        assert_eq!(result.diff_percent, 0.0);
110    }
111
112    #[test]
113    fn golden_image_detects_difference() {
114        let mut actual = vec![255u8; 400];
115        let expected = vec![255u8; 400];
116        // Change one pixel significantly
117        actual[0] = 0;
118        let config = GoldenImageConfig::default();
119        let result = GoldenImageComparator::compare(&actual, &expected, &config);
120        assert!(!result.passed);
121        assert!(result.diff_percent > 0.0);
122    }
123
124    #[test]
125    fn golden_image_tolerance() {
126        let mut actual = vec![255u8; 400];
127        let expected = vec![255u8; 400];
128        // Small difference within tolerance
129        actual[0] = 253; // Within tolerance of 3
130        let config = GoldenImageConfig::default();
131        let result = GoldenImageComparator::compare(&actual, &expected, &config);
132        assert!(result.passed);
133    }
134
135    #[test]
136    fn golden_image_different_sizes() {
137        let actual = vec![255u8; 400];
138        let expected = vec![255u8; 800];
139        let config = GoldenImageConfig::default();
140        let result = GoldenImageComparator::compare(&actual, &expected, &config);
141        assert!(!result.passed);
142        assert_eq!(result.diff_percent, 100.0);
143    }
144
145    #[test]
146    fn golden_image_empty() {
147        let config = GoldenImageConfig::default();
148        let result = GoldenImageComparator::compare(&[], &[], &config);
149        assert!(result.passed);
150    }
151}