Skip to main content

jugar_probar/
snapshot.rs

1//! Visual snapshot testing.
2//!
3//! Per spec Section 6.2: Visual Regression Testing
4
5/// Configuration for snapshot testing
6#[derive(Debug, Clone)]
7pub struct SnapshotConfig {
8    /// Whether to update snapshots on mismatch
9    pub update_snapshots: bool,
10    /// Difference threshold (0.0-1.0)
11    pub threshold: f64,
12    /// Directory to store snapshots
13    pub snapshot_dir: String,
14}
15
16impl Default for SnapshotConfig {
17    fn default() -> Self {
18        Self {
19            update_snapshots: false,
20            threshold: 0.01, // 1% difference allowed
21            snapshot_dir: String::from("__snapshots__"),
22        }
23    }
24}
25
26impl SnapshotConfig {
27    /// Set update mode
28    #[must_use]
29    pub const fn with_update(mut self, update: bool) -> Self {
30        self.update_snapshots = update;
31        self
32    }
33
34    /// Set threshold
35    #[must_use]
36    pub const fn with_threshold(mut self, threshold: f64) -> Self {
37        self.threshold = threshold;
38        self
39    }
40
41    /// Set snapshot directory
42    #[must_use]
43    pub fn with_dir(mut self, dir: impl Into<String>) -> Self {
44        self.snapshot_dir = dir.into();
45        self
46    }
47}
48
49/// A visual snapshot
50#[derive(Debug, Clone)]
51pub struct Snapshot {
52    /// Snapshot name/identifier
53    pub name: String,
54    /// Raw image data
55    pub data: Vec<u8>,
56    /// Image width
57    pub width: u32,
58    /// Image height
59    pub height: u32,
60}
61
62impl Snapshot {
63    /// Create a new snapshot
64    #[must_use]
65    pub fn new(name: impl Into<String>, data: Vec<u8>) -> Self {
66        Self {
67            name: name.into(),
68            data,
69            width: 0,
70            height: 0,
71        }
72    }
73
74    /// Create with dimensions
75    #[must_use]
76    pub const fn with_dimensions(mut self, width: u32, height: u32) -> Self {
77        self.width = width;
78        self.height = height;
79        self
80    }
81
82    /// Compare this snapshot to another
83    #[must_use]
84    pub fn diff(&self, other: &Self) -> SnapshotDiff {
85        // Simple byte-by-byte comparison
86        let mut difference_count = 0;
87        let max_len = self.data.len().max(other.data.len());
88
89        if max_len == 0 {
90            return SnapshotDiff {
91                identical: true,
92                difference_count: 0,
93                difference_percent: 0.0,
94                diff_data: Vec::new(),
95            };
96        }
97
98        for i in 0..max_len {
99            let a = self.data.get(i).copied().unwrap_or(0);
100            let b = other.data.get(i).copied().unwrap_or(0);
101            if a != b {
102                difference_count += 1;
103            }
104        }
105
106        #[allow(clippy::cast_precision_loss)] // Acceptable for percentage calculation
107        let difference_percent = (difference_count as f64 / max_len as f64) * 100.0;
108
109        SnapshotDiff {
110            identical: difference_count == 0,
111            difference_count,
112            difference_percent,
113            diff_data: Vec::new(), // Would contain visual diff in full impl
114        }
115    }
116
117    /// Get snapshot size in bytes
118    #[must_use]
119    pub fn size(&self) -> usize {
120        self.data.len()
121    }
122}
123
124/// Result of comparing two snapshots
125#[derive(Debug, Clone)]
126pub struct SnapshotDiff {
127    /// Whether snapshots are identical
128    pub identical: bool,
129    /// Number of differing bytes/pixels
130    pub difference_count: usize,
131    /// Percentage of difference
132    pub difference_percent: f64,
133    /// Visual diff data (highlighted differences)
134    pub diff_data: Vec<u8>,
135}
136
137impl SnapshotDiff {
138    /// Check if snapshots are identical
139    #[must_use]
140    pub const fn is_identical(&self) -> bool {
141        self.identical
142    }
143
144    /// Check if difference is within threshold
145    #[must_use]
146    pub fn within_threshold(&self, threshold: f64) -> bool {
147        self.difference_percent <= threshold * 100.0
148    }
149}
150
151#[cfg(test)]
152#[allow(clippy::unwrap_used, clippy::expect_used)]
153mod tests {
154    use super::*;
155
156    // =========================================================================
157    // H₀ EXTREME TDD: Snapshot Tests (Feature F P0)
158    // =========================================================================
159
160    mod h0_snapshot_config_tests {
161        use super::*;
162
163        #[test]
164        fn h0_snap_01_config_default_update_false() {
165            let config = SnapshotConfig::default();
166            assert!(!config.update_snapshots);
167        }
168
169        #[test]
170        fn h0_snap_02_config_default_threshold() {
171            let config = SnapshotConfig::default();
172            assert!((config.threshold - 0.01).abs() < 0.001);
173        }
174
175        #[test]
176        fn h0_snap_03_config_default_dir() {
177            let config = SnapshotConfig::default();
178            assert_eq!(config.snapshot_dir, "__snapshots__");
179        }
180
181        #[test]
182        fn h0_snap_04_config_with_update_true() {
183            let config = SnapshotConfig::default().with_update(true);
184            assert!(config.update_snapshots);
185        }
186
187        #[test]
188        fn h0_snap_05_config_with_update_false() {
189            let config = SnapshotConfig::default().with_update(false);
190            assert!(!config.update_snapshots);
191        }
192
193        #[test]
194        fn h0_snap_06_config_with_threshold() {
195            let config = SnapshotConfig::default().with_threshold(0.05);
196            assert!((config.threshold - 0.05).abs() < 0.001);
197        }
198
199        #[test]
200        fn h0_snap_07_config_with_dir() {
201            let config = SnapshotConfig::default().with_dir("custom_dir");
202            assert_eq!(config.snapshot_dir, "custom_dir");
203        }
204
205        #[test]
206        fn h0_snap_08_config_builder_chain() {
207            let config = SnapshotConfig::default()
208                .with_update(true)
209                .with_threshold(0.1)
210                .with_dir("test_snaps");
211            assert!(config.update_snapshots);
212            assert!((config.threshold - 0.1).abs() < 0.001);
213            assert_eq!(config.snapshot_dir, "test_snaps");
214        }
215
216        #[test]
217        fn h0_snap_09_config_clone() {
218            let config = SnapshotConfig::default().with_threshold(0.02);
219            let cloned = config;
220            assert!((cloned.threshold - 0.02).abs() < 0.001);
221        }
222
223        #[test]
224        fn h0_snap_10_config_zero_threshold() {
225            let config = SnapshotConfig::default().with_threshold(0.0);
226            assert!((config.threshold - 0.0).abs() < f64::EPSILON);
227        }
228    }
229
230    mod h0_snapshot_tests {
231        use super::*;
232
233        #[test]
234        fn h0_snap_11_snapshot_new() {
235            let snap = Snapshot::new("test", vec![1, 2, 3]);
236            assert_eq!(snap.name, "test");
237        }
238
239        #[test]
240        fn h0_snap_12_snapshot_new_data() {
241            let snap = Snapshot::new("test", vec![10, 20, 30]);
242            assert_eq!(snap.data, vec![10, 20, 30]);
243        }
244
245        #[test]
246        fn h0_snap_13_snapshot_default_dimensions() {
247            let snap = Snapshot::new("test", vec![]);
248            assert_eq!(snap.width, 0);
249            assert_eq!(snap.height, 0);
250        }
251
252        #[test]
253        fn h0_snap_14_snapshot_with_dimensions() {
254            let snap = Snapshot::new("test", vec![]).with_dimensions(100, 200);
255            assert_eq!(snap.width, 100);
256            assert_eq!(snap.height, 200);
257        }
258
259        #[test]
260        fn h0_snap_15_snapshot_size() {
261            let snap = Snapshot::new("test", vec![1, 2, 3, 4, 5]);
262            assert_eq!(snap.size(), 5);
263        }
264
265        #[test]
266        fn h0_snap_16_snapshot_size_empty() {
267            let snap = Snapshot::new("test", vec![]);
268            assert_eq!(snap.size(), 0);
269        }
270
271        #[test]
272        fn h0_snap_17_snapshot_clone() {
273            let snap = Snapshot::new("original", vec![1, 2, 3]);
274            let cloned = snap;
275            assert_eq!(cloned.name, "original");
276            assert_eq!(cloned.data, vec![1, 2, 3]);
277        }
278
279        #[test]
280        fn h0_snap_18_snapshot_string_name() {
281            let snap = Snapshot::new(String::from("string_name"), vec![]);
282            assert_eq!(snap.name, "string_name");
283        }
284
285        #[test]
286        fn h0_snap_19_snapshot_large_data() {
287            let data: Vec<u8> = (0..1000).map(|i| (i % 256) as u8).collect();
288            let snap = Snapshot::new("large", data);
289            assert_eq!(snap.size(), 1000);
290        }
291
292        #[test]
293        fn h0_snap_20_snapshot_dimensions_chain() {
294            let snap = Snapshot::new("test", vec![0; 100]).with_dimensions(10, 10);
295            assert_eq!(snap.width * snap.height, 100);
296        }
297    }
298
299    mod h0_snapshot_diff_tests {
300        use super::*;
301
302        #[test]
303        fn h0_snap_21_diff_identical() {
304            let a = Snapshot::new("a", vec![1, 2, 3]);
305            let b = Snapshot::new("b", vec![1, 2, 3]);
306            let diff = a.diff(&b);
307            assert!(diff.identical);
308        }
309
310        #[test]
311        fn h0_snap_22_diff_not_identical() {
312            let a = Snapshot::new("a", vec![1, 2, 3]);
313            let b = Snapshot::new("b", vec![1, 2, 4]);
314            let diff = a.diff(&b);
315            assert!(!diff.identical);
316        }
317
318        #[test]
319        fn h0_snap_23_diff_difference_count() {
320            let a = Snapshot::new("a", vec![1, 2, 3, 4]);
321            let b = Snapshot::new("b", vec![1, 0, 3, 0]);
322            let diff = a.diff(&b);
323            assert_eq!(diff.difference_count, 2);
324        }
325
326        #[test]
327        fn h0_snap_24_diff_difference_percent() {
328            let a = Snapshot::new("a", vec![1, 2, 3, 4]);
329            let b = Snapshot::new("b", vec![0, 0, 0, 0]);
330            let diff = a.diff(&b);
331            assert!((diff.difference_percent - 100.0).abs() < 0.001);
332        }
333
334        #[test]
335        fn h0_snap_25_diff_empty_snapshots() {
336            let a = Snapshot::new("a", vec![]);
337            let b = Snapshot::new("b", vec![]);
338            let diff = a.diff(&b);
339            assert!(diff.identical);
340        }
341
342        #[test]
343        fn h0_snap_26_diff_is_identical() {
344            let a = Snapshot::new("a", vec![1, 2, 3]);
345            let b = Snapshot::new("b", vec![1, 2, 3]);
346            let diff = a.diff(&b);
347            assert!(diff.is_identical());
348        }
349
350        #[test]
351        fn h0_snap_27_diff_not_is_identical() {
352            let a = Snapshot::new("a", vec![1, 2, 3]);
353            let b = Snapshot::new("b", vec![4, 5, 6]);
354            let diff = a.diff(&b);
355            assert!(!diff.is_identical());
356        }
357
358        #[test]
359        fn h0_snap_28_diff_within_threshold_true() {
360            let a = Snapshot::new("a", vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
361            let b = Snapshot::new("b", vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 11]);
362            let diff = a.diff(&b);
363            // 10% diff, threshold 0.15 (15%)
364            assert!(diff.within_threshold(0.15));
365        }
366
367        #[test]
368        fn h0_snap_29_diff_within_threshold_false() {
369            let a = Snapshot::new("a", vec![1, 2, 3, 4, 5]);
370            let b = Snapshot::new("b", vec![0, 0, 0, 0, 0]);
371            let diff = a.diff(&b);
372            // 100% diff, threshold 0.5 (50%)
373            assert!(!diff.within_threshold(0.5));
374        }
375
376        #[test]
377        fn h0_snap_30_diff_clone() {
378            let a = Snapshot::new("a", vec![1, 2, 3]);
379            let b = Snapshot::new("b", vec![1, 2, 4]);
380            let diff = a.diff(&b);
381            let cloned = diff.clone();
382            assert_eq!(cloned.difference_count, diff.difference_count);
383        }
384    }
385
386    mod h0_snapshot_comparison_tests {
387        use super::*;
388
389        #[test]
390        fn h0_snap_31_diff_different_lengths_longer_b() {
391            let a = Snapshot::new("a", vec![1, 2]);
392            let b = Snapshot::new("b", vec![1, 2, 3, 4]);
393            let diff = a.diff(&b);
394            assert!(!diff.identical);
395        }
396
397        #[test]
398        fn h0_snap_32_diff_different_lengths_longer_a() {
399            let a = Snapshot::new("a", vec![1, 2, 3, 4]);
400            let b = Snapshot::new("b", vec![1, 2]);
401            let diff = a.diff(&b);
402            assert!(!diff.identical);
403        }
404
405        #[test]
406        fn h0_snap_33_diff_zero_percent_identical() {
407            let a = Snapshot::new("a", vec![1, 2, 3]);
408            let b = Snapshot::new("b", vec![1, 2, 3]);
409            let diff = a.diff(&b);
410            assert!(diff.difference_percent < 0.001);
411        }
412
413        #[test]
414        fn h0_snap_34_diff_fifty_percent() {
415            let a = Snapshot::new("a", vec![1, 1]);
416            let b = Snapshot::new("b", vec![1, 2]);
417            let diff = a.diff(&b);
418            assert!((diff.difference_percent - 50.0).abs() < 0.001);
419        }
420
421        #[test]
422        fn h0_snap_35_diff_data_empty() {
423            let a = Snapshot::new("a", vec![1, 2, 3]);
424            let b = Snapshot::new("b", vec![1, 2, 4]);
425            let diff = a.diff(&b);
426            assert!(diff.diff_data.is_empty());
427        }
428
429        #[test]
430        fn h0_snap_36_within_threshold_zero() {
431            let a = Snapshot::new("a", vec![1, 2, 3]);
432            let b = Snapshot::new("b", vec![1, 2, 3]);
433            let diff = a.diff(&b);
434            assert!(diff.within_threshold(0.0));
435        }
436
437        #[test]
438        fn h0_snap_37_within_threshold_one() {
439            let a = Snapshot::new("a", vec![1, 2, 3]);
440            let b = Snapshot::new("b", vec![4, 5, 6]);
441            let diff = a.diff(&b);
442            assert!(diff.within_threshold(1.0));
443        }
444
445        #[test]
446        fn h0_snap_38_single_byte_diff() {
447            let a = Snapshot::new("a", vec![255]);
448            let b = Snapshot::new("b", vec![0]);
449            let diff = a.diff(&b);
450            assert_eq!(diff.difference_count, 1);
451            assert!((diff.difference_percent - 100.0).abs() < 0.001);
452        }
453
454        #[test]
455        fn h0_snap_39_single_byte_same() {
456            let a = Snapshot::new("a", vec![128]);
457            let b = Snapshot::new("b", vec![128]);
458            let diff = a.diff(&b);
459            assert!(diff.identical);
460        }
461
462        #[test]
463        fn h0_snap_40_large_snapshot_identical() {
464            let data: Vec<u8> = vec![100; 10000];
465            let a = Snapshot::new("a", data.clone());
466            let b = Snapshot::new("b", data);
467            let diff = a.diff(&b);
468            assert!(diff.identical);
469        }
470    }
471
472    mod h0_snapshot_edge_cases {
473        use super::*;
474
475        #[test]
476        fn h0_snap_41_config_high_threshold() {
477            let config = SnapshotConfig::default().with_threshold(1.0);
478            assert!((config.threshold - 1.0).abs() < f64::EPSILON);
479        }
480
481        #[test]
482        fn h0_snap_42_snapshot_with_zero_dimensions() {
483            let snap = Snapshot::new("test", vec![1, 2, 3]).with_dimensions(0, 0);
484            assert_eq!(snap.width, 0);
485            assert_eq!(snap.height, 0);
486        }
487
488        #[test]
489        fn h0_snap_43_snapshot_dimension_overflow_check() {
490            let snap = Snapshot::new("test", vec![]).with_dimensions(u32::MAX, 1);
491            assert_eq!(snap.width, u32::MAX);
492        }
493
494        #[test]
495        fn h0_snap_44_diff_all_zeros() {
496            let a = Snapshot::new("a", vec![0, 0, 0]);
497            let b = Snapshot::new("b", vec![0, 0, 0]);
498            let diff = a.diff(&b);
499            assert!(diff.identical);
500        }
501
502        #[test]
503        fn h0_snap_45_diff_all_max() {
504            let a = Snapshot::new("a", vec![255, 255, 255]);
505            let b = Snapshot::new("b", vec![255, 255, 255]);
506            let diff = a.diff(&b);
507            assert!(diff.identical);
508        }
509
510        #[test]
511        fn h0_snap_46_snapshot_name_empty() {
512            let snap = Snapshot::new("", vec![1, 2, 3]);
513            assert_eq!(snap.name, "");
514        }
515
516        #[test]
517        fn h0_snap_47_snapshot_name_unicode() {
518            let snap = Snapshot::new("テスト_스냅샷", vec![1, 2, 3]);
519            assert_eq!(snap.name, "テスト_스냅샷");
520        }
521
522        #[test]
523        fn h0_snap_48_config_empty_dir() {
524            let config = SnapshotConfig::default().with_dir("");
525            assert_eq!(config.snapshot_dir, "");
526        }
527
528        #[test]
529        fn h0_snap_49_diff_one_empty_one_full() {
530            let a = Snapshot::new("a", vec![]);
531            let b = Snapshot::new("b", vec![1, 2, 3]);
532            let diff = a.diff(&b);
533            assert!(!diff.identical);
534            assert_eq!(diff.difference_count, 3);
535        }
536
537        #[test]
538        fn h0_snap_50_diff_full_one_empty() {
539            let a = Snapshot::new("a", vec![1, 2, 3]);
540            let b = Snapshot::new("b", vec![]);
541            let diff = a.diff(&b);
542            assert!(!diff.identical);
543            assert_eq!(diff.difference_count, 3);
544        }
545    }
546}