/home/noah/src/trueno/src/simulation/visual.rs
Line | Count | Source |
1 | | //! Visual Regression Testing (Genchi Genbutsu: Go and See) |
2 | | //! |
3 | | //! Provides pixel-perfect validation of compute outputs through |
4 | | //! heatmap rendering and golden baseline comparison. |
5 | | |
6 | | use std::path::PathBuf; |
7 | | |
8 | | /// RGB color for visualization |
9 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
10 | | pub struct Rgb { |
11 | | /// Red component |
12 | | pub r: u8, |
13 | | /// Green component |
14 | | pub g: u8, |
15 | | /// Blue component |
16 | | pub b: u8, |
17 | | } |
18 | | |
19 | | impl Rgb { |
20 | | /// Create new RGB color |
21 | | #[must_use] |
22 | 0 | pub const fn new(r: u8, g: u8, b: u8) -> Self { |
23 | 0 | Self { r, g, b } |
24 | 0 | } |
25 | | |
26 | | /// Magenta for NaN values |
27 | | pub const NAN_COLOR: Self = Self::new(255, 0, 255); |
28 | | /// White for +Infinity |
29 | | pub const INF_COLOR: Self = Self::new(255, 255, 255); |
30 | | /// Black for -Infinity |
31 | | pub const NEG_INF_COLOR: Self = Self::new(0, 0, 0); |
32 | | } |
33 | | |
34 | | /// Color palette for heatmap rendering |
35 | | #[derive(Debug, Clone)] |
36 | | pub struct ColorPalette { |
37 | | pub(crate) colors: Vec<Rgb>, |
38 | | } |
39 | | |
40 | | impl Default for ColorPalette { |
41 | 0 | fn default() -> Self { |
42 | 0 | Self::viridis() |
43 | 0 | } |
44 | | } |
45 | | |
46 | | impl ColorPalette { |
47 | | /// Viridis colorblind-friendly palette |
48 | | #[must_use] |
49 | 0 | pub fn viridis() -> Self { |
50 | 0 | Self { |
51 | 0 | colors: vec![ |
52 | 0 | Rgb::new(68, 1, 84), |
53 | 0 | Rgb::new(59, 82, 139), |
54 | 0 | Rgb::new(33, 145, 140), |
55 | 0 | Rgb::new(94, 201, 98), |
56 | 0 | Rgb::new(253, 231, 37), |
57 | 0 | ], |
58 | 0 | } |
59 | 0 | } |
60 | | |
61 | | /// Grayscale palette |
62 | | #[must_use] |
63 | 0 | pub fn grayscale() -> Self { |
64 | 0 | Self { |
65 | 0 | colors: vec![ |
66 | 0 | Rgb::new(0, 0, 0), |
67 | 0 | Rgb::new(128, 128, 128), |
68 | 0 | Rgb::new(255, 255, 255), |
69 | 0 | ], |
70 | 0 | } |
71 | 0 | } |
72 | | |
73 | | /// Interpolate color at position t (0.0 to 1.0) |
74 | | #[must_use] |
75 | | #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] |
76 | 0 | pub fn interpolate(&self, t: f32) -> Rgb { |
77 | 0 | let t = t.clamp(0.0, 1.0); |
78 | 0 | let n = self.colors.len() - 1; |
79 | 0 | let idx = (t * n as f32).floor() as usize; |
80 | 0 | let idx = idx.min(n - 1); |
81 | 0 | let local_t = t * n as f32 - idx as f32; |
82 | | |
83 | 0 | let c1 = &self.colors[idx]; |
84 | 0 | let c2 = &self.colors[idx + 1]; |
85 | | |
86 | 0 | Rgb { |
87 | 0 | r: (c1.r as f32 + (c2.r as f32 - c1.r as f32) * local_t) as u8, |
88 | 0 | g: (c1.g as f32 + (c2.g as f32 - c1.g as f32) * local_t) as u8, |
89 | 0 | b: (c1.b as f32 + (c2.b as f32 - c1.b as f32) * local_t) as u8, |
90 | 0 | } |
91 | 0 | } |
92 | | } |
93 | | |
94 | | /// Visual regression test configuration (Genchi Genbutsu) |
95 | | #[derive(Debug, Clone)] |
96 | | pub struct VisualRegressionConfig { |
97 | | /// Golden baseline directory |
98 | | pub golden_dir: PathBuf, |
99 | | /// Output directory for test results |
100 | | pub output_dir: PathBuf, |
101 | | /// Maximum allowed different pixels (percentage) |
102 | | pub max_diff_pct: f64, |
103 | | /// Color palette for visualization |
104 | | pub palette: ColorPalette, |
105 | | } |
106 | | |
107 | | impl Default for VisualRegressionConfig { |
108 | 0 | fn default() -> Self { |
109 | 0 | Self { |
110 | 0 | golden_dir: PathBuf::from("golden"), |
111 | 0 | output_dir: PathBuf::from("test_output"), |
112 | 0 | max_diff_pct: 0.0, // Exact match by default |
113 | 0 | palette: ColorPalette::default(), |
114 | 0 | } |
115 | 0 | } |
116 | | } |
117 | | |
118 | | impl VisualRegressionConfig { |
119 | | /// Create new config with custom golden directory |
120 | | #[must_use] |
121 | 0 | pub fn new(golden_dir: impl Into<PathBuf>) -> Self { |
122 | 0 | Self { |
123 | 0 | golden_dir: golden_dir.into(), |
124 | 0 | ..Default::default() |
125 | 0 | } |
126 | 0 | } |
127 | | |
128 | | /// Set output directory |
129 | | #[must_use] |
130 | 0 | pub fn with_output_dir(mut self, dir: impl Into<PathBuf>) -> Self { |
131 | 0 | self.output_dir = dir.into(); |
132 | 0 | self |
133 | 0 | } |
134 | | |
135 | | /// Set maximum diff percentage |
136 | | #[must_use] |
137 | 0 | pub const fn with_max_diff_pct(mut self, pct: f64) -> Self { |
138 | 0 | self.max_diff_pct = pct; |
139 | 0 | self |
140 | 0 | } |
141 | | |
142 | | /// Set color palette |
143 | | #[must_use] |
144 | 0 | pub fn with_palette(mut self, palette: ColorPalette) -> Self { |
145 | 0 | self.palette = palette; |
146 | 0 | self |
147 | 0 | } |
148 | | } |
149 | | |
150 | | /// Pixel diff result for visual regression testing |
151 | | #[derive(Debug, Clone)] |
152 | | pub struct PixelDiffResult { |
153 | | /// Number of pixels that differ |
154 | | pub different_pixels: usize, |
155 | | /// Total number of pixels |
156 | | pub total_pixels: usize, |
157 | | /// Maximum color difference found |
158 | | pub max_diff: u32, |
159 | | } |
160 | | |
161 | | impl PixelDiffResult { |
162 | | /// Calculate percentage of different pixels |
163 | | #[must_use] |
164 | 0 | pub fn diff_percentage(&self) -> f64 { |
165 | 0 | if self.total_pixels == 0 { |
166 | 0 | 0.0 |
167 | | } else { |
168 | 0 | (self.different_pixels as f64 / self.total_pixels as f64) * 100.0 |
169 | | } |
170 | 0 | } |
171 | | |
172 | | /// Check if images match within threshold |
173 | | #[must_use] |
174 | 0 | pub fn matches(&self, threshold_pct: f64) -> bool { |
175 | 0 | self.diff_percentage() <= threshold_pct |
176 | 0 | } |
177 | | |
178 | | /// Create a passing result (no differences) |
179 | | #[must_use] |
180 | 0 | pub const fn pass(total_pixels: usize) -> Self { |
181 | 0 | Self { |
182 | 0 | different_pixels: 0, |
183 | 0 | total_pixels, |
184 | 0 | max_diff: 0, |
185 | 0 | } |
186 | 0 | } |
187 | | } |
188 | | |
189 | | /// Simple buffer renderer for SIMD output visualization |
190 | | /// |
191 | | /// Converts f32 buffers to raw RGBA bytes for testing |
192 | | #[derive(Debug, Clone)] |
193 | | pub struct BufferRenderer { |
194 | | palette: ColorPalette, |
195 | | pub(crate) range: Option<(f32, f32)>, |
196 | | } |
197 | | |
198 | | impl Default for BufferRenderer { |
199 | 0 | fn default() -> Self { |
200 | 0 | Self::new() |
201 | 0 | } |
202 | | } |
203 | | |
204 | | impl BufferRenderer { |
205 | | /// Create renderer with auto-normalization |
206 | | #[must_use] |
207 | 0 | pub fn new() -> Self { |
208 | 0 | Self { |
209 | 0 | palette: ColorPalette::default(), |
210 | 0 | range: None, |
211 | 0 | } |
212 | 0 | } |
213 | | |
214 | | /// Set fixed range for normalization |
215 | | #[must_use] |
216 | 0 | pub const fn with_range(mut self, min: f32, max: f32) -> Self { |
217 | 0 | self.range = Some((min, max)); |
218 | 0 | self |
219 | 0 | } |
220 | | |
221 | | /// Set color palette |
222 | | #[must_use] |
223 | 0 | pub fn with_palette(mut self, palette: ColorPalette) -> Self { |
224 | 0 | self.palette = palette; |
225 | 0 | self |
226 | 0 | } |
227 | | |
228 | | /// Render f32 buffer to raw RGBA bytes |
229 | | /// |
230 | | /// Returns Vec<u8> with RGBA pixels (4 bytes per pixel) |
231 | | #[must_use] |
232 | 0 | pub fn render_to_rgba(&self, buffer: &[f32], width: u32, height: u32) -> Vec<u8> { |
233 | 0 | assert_eq!(buffer.len(), (width * height) as usize); |
234 | | |
235 | 0 | let (min_val, max_val) = self.range.unwrap_or_else(|| { |
236 | 0 | let valid: Vec<f32> = buffer.iter().copied().filter(|v| v.is_finite()).collect(); |
237 | 0 | if valid.is_empty() { |
238 | 0 | (0.0, 1.0) |
239 | | } else { |
240 | 0 | let min = valid.iter().copied().fold(f32::INFINITY, f32::min); |
241 | 0 | let max = valid.iter().copied().fold(f32::NEG_INFINITY, f32::max); |
242 | 0 | (min, max.max(min + f32::EPSILON)) |
243 | | } |
244 | 0 | }); |
245 | | |
246 | 0 | let mut rgba = Vec::with_capacity(buffer.len() * 4); |
247 | | |
248 | 0 | for &value in buffer { |
249 | 0 | let color = if value.is_nan() { |
250 | 0 | Rgb::NAN_COLOR |
251 | 0 | } else if value.is_infinite() { |
252 | 0 | if value > 0.0 { |
253 | 0 | Rgb::INF_COLOR |
254 | | } else { |
255 | 0 | Rgb::NEG_INF_COLOR |
256 | | } |
257 | | } else { |
258 | 0 | let t = (value - min_val) / (max_val - min_val); |
259 | 0 | self.palette.interpolate(t) |
260 | | }; |
261 | | |
262 | 0 | rgba.push(color.r); |
263 | 0 | rgba.push(color.g); |
264 | 0 | rgba.push(color.b); |
265 | 0 | rgba.push(255); // Alpha |
266 | | } |
267 | | |
268 | 0 | rgba |
269 | 0 | } |
270 | | |
271 | | /// Compare two RGBA buffers and return diff result |
272 | | #[must_use] |
273 | 0 | pub fn compare_rgba(&self, a: &[u8], b: &[u8], tolerance: u8) -> PixelDiffResult { |
274 | 0 | if a == b { |
275 | 0 | return PixelDiffResult::pass(a.len() / 4); |
276 | 0 | } |
277 | | |
278 | 0 | let min_len = a.len().min(b.len()); |
279 | 0 | let mut different = 0; |
280 | 0 | let mut max_diff: u32 = 0; |
281 | | |
282 | | // Compare pixels (4 bytes each: RGBA) |
283 | 0 | for i in (0..min_len).step_by(4) { |
284 | 0 | let mut pixel_diff = false; |
285 | 0 | for j in 0..4 { |
286 | 0 | if i + j < min_len { |
287 | 0 | let diff = (a[i + j] as i32 - b[i + j] as i32).unsigned_abs(); |
288 | 0 | if diff > tolerance as u32 { |
289 | 0 | pixel_diff = true; |
290 | 0 | max_diff = max_diff.max(diff); |
291 | 0 | } |
292 | 0 | } |
293 | | } |
294 | 0 | if pixel_diff { |
295 | 0 | different += 1; |
296 | 0 | } |
297 | | } |
298 | | |
299 | | // Count size difference as pixel differences |
300 | 0 | if a.len() != b.len() { |
301 | 0 | different += a.len().abs_diff(b.len()) / 4; |
302 | 0 | } |
303 | | |
304 | 0 | PixelDiffResult { |
305 | 0 | different_pixels: different, |
306 | 0 | total_pixels: min_len.max(a.len()).max(b.len()) / 4, |
307 | 0 | max_diff, |
308 | 0 | } |
309 | 0 | } |
310 | | } |
311 | | |
312 | | /// Golden baseline manager for visual regression testing |
313 | | #[derive(Debug, Clone)] |
314 | | pub struct GoldenBaseline { |
315 | | config: VisualRegressionConfig, |
316 | | } |
317 | | |
318 | | impl GoldenBaseline { |
319 | | /// Create new golden baseline manager |
320 | | #[must_use] |
321 | 0 | pub fn new(config: VisualRegressionConfig) -> Self { |
322 | 0 | Self { config } |
323 | 0 | } |
324 | | |
325 | | /// Get path for a golden baseline file |
326 | | #[must_use] |
327 | 0 | pub fn golden_path(&self, name: &str) -> PathBuf { |
328 | 0 | self.config.golden_dir.join(format!("{name}.golden")) |
329 | 0 | } |
330 | | |
331 | | /// Get path for an output file |
332 | | #[must_use] |
333 | 0 | pub fn output_path(&self, name: &str) -> PathBuf { |
334 | 0 | self.config.output_dir.join(format!("{name}.output")) |
335 | 0 | } |
336 | | |
337 | | /// Get the config |
338 | | #[must_use] |
339 | 0 | pub const fn config(&self) -> &VisualRegressionConfig { |
340 | 0 | &self.config |
341 | 0 | } |
342 | | } |
343 | | |
344 | | #[cfg(test)] |
345 | | mod tests { |
346 | | use super::*; |
347 | | |
348 | | #[test] |
349 | | fn test_rgb_color_creation() { |
350 | | let color = Rgb::new(255, 128, 64); |
351 | | assert_eq!(color.r, 255); |
352 | | assert_eq!(color.g, 128); |
353 | | assert_eq!(color.b, 64); |
354 | | } |
355 | | |
356 | | #[test] |
357 | | fn test_rgb_special_colors() { |
358 | | assert_eq!(Rgb::NAN_COLOR, Rgb::new(255, 0, 255)); |
359 | | assert_eq!(Rgb::INF_COLOR, Rgb::new(255, 255, 255)); |
360 | | assert_eq!(Rgb::NEG_INF_COLOR, Rgb::new(0, 0, 0)); |
361 | | } |
362 | | |
363 | | #[test] |
364 | | fn test_color_palette_viridis() { |
365 | | let palette = ColorPalette::viridis(); |
366 | | assert_eq!(palette.colors.len(), 5); |
367 | | |
368 | | // Test interpolation at boundaries |
369 | | let at_0 = palette.interpolate(0.0); |
370 | | let at_1 = palette.interpolate(1.0); |
371 | | |
372 | | // Viridis starts dark purple, ends yellow |
373 | | assert_eq!(at_0, Rgb::new(68, 1, 84)); |
374 | | assert_eq!(at_1, Rgb::new(253, 231, 37)); |
375 | | } |
376 | | |
377 | | #[test] |
378 | | fn test_color_palette_grayscale() { |
379 | | let palette = ColorPalette::grayscale(); |
380 | | assert_eq!(palette.colors.len(), 3); |
381 | | |
382 | | let at_0 = palette.interpolate(0.0); |
383 | | let at_1 = palette.interpolate(1.0); |
384 | | |
385 | | assert_eq!(at_0, Rgb::new(0, 0, 0)); |
386 | | assert_eq!(at_1, Rgb::new(255, 255, 255)); |
387 | | } |
388 | | |
389 | | #[test] |
390 | | fn test_color_palette_interpolation_midpoint() { |
391 | | let palette = ColorPalette::grayscale(); |
392 | | let at_mid = palette.interpolate(0.5); |
393 | | |
394 | | // Should be close to gray |
395 | | assert_eq!(at_mid, Rgb::new(128, 128, 128)); |
396 | | } |
397 | | |
398 | | #[test] |
399 | | fn test_color_palette_clamping() { |
400 | | let palette = ColorPalette::viridis(); |
401 | | |
402 | | // Values outside [0, 1] should be clamped |
403 | | let at_neg = palette.interpolate(-0.5); |
404 | | let at_over = palette.interpolate(1.5); |
405 | | |
406 | | assert_eq!(at_neg, palette.interpolate(0.0)); |
407 | | assert_eq!(at_over, palette.interpolate(1.0)); |
408 | | } |
409 | | |
410 | | #[test] |
411 | | fn test_visual_regression_config_default() { |
412 | | let config = VisualRegressionConfig::default(); |
413 | | |
414 | | assert_eq!(config.golden_dir, PathBuf::from("golden")); |
415 | | assert_eq!(config.output_dir, PathBuf::from("test_output")); |
416 | | assert_eq!(config.max_diff_pct, 0.0); |
417 | | } |
418 | | |
419 | | #[test] |
420 | | fn test_visual_regression_config_builder() { |
421 | | let config = VisualRegressionConfig::new("my_golden") |
422 | | .with_output_dir("my_output") |
423 | | .with_max_diff_pct(1.5) |
424 | | .with_palette(ColorPalette::grayscale()); |
425 | | |
426 | | assert_eq!(config.golden_dir, PathBuf::from("my_golden")); |
427 | | assert_eq!(config.output_dir, PathBuf::from("my_output")); |
428 | | assert_eq!(config.max_diff_pct, 1.5); |
429 | | } |
430 | | |
431 | | #[test] |
432 | | fn test_pixel_diff_result_percentage() { |
433 | | let result = PixelDiffResult { |
434 | | different_pixels: 10, |
435 | | total_pixels: 100, |
436 | | max_diff: 50, |
437 | | }; |
438 | | |
439 | | assert_eq!(result.diff_percentage(), 10.0); |
440 | | assert!(!result.matches(5.0)); |
441 | | assert!(result.matches(10.0)); |
442 | | assert!(result.matches(15.0)); |
443 | | } |
444 | | |
445 | | #[test] |
446 | | fn test_pixel_diff_result_zero_total() { |
447 | | let result = PixelDiffResult { |
448 | | different_pixels: 0, |
449 | | total_pixels: 0, |
450 | | max_diff: 0, |
451 | | }; |
452 | | |
453 | | assert_eq!(result.diff_percentage(), 0.0); |
454 | | } |
455 | | |
456 | | #[test] |
457 | | fn test_pixel_diff_result_pass() { |
458 | | let result = PixelDiffResult::pass(100); |
459 | | |
460 | | assert_eq!(result.different_pixels, 0); |
461 | | assert_eq!(result.total_pixels, 100); |
462 | | assert_eq!(result.max_diff, 0); |
463 | | assert!(result.matches(0.0)); |
464 | | } |
465 | | |
466 | | #[test] |
467 | | fn test_buffer_renderer_default() { |
468 | | let renderer = BufferRenderer::default(); |
469 | | assert!(renderer.range.is_none()); |
470 | | } |
471 | | |
472 | | #[test] |
473 | | fn test_buffer_renderer_with_range() { |
474 | | let renderer = BufferRenderer::new().with_range(0.0, 10.0); |
475 | | assert_eq!(renderer.range, Some((0.0, 10.0))); |
476 | | } |
477 | | |
478 | | #[test] |
479 | | fn test_buffer_renderer_with_palette() { |
480 | | let renderer = BufferRenderer::new().with_palette(ColorPalette::grayscale()); |
481 | | assert_eq!(renderer.palette.colors.len(), 3); |
482 | | } |
483 | | |
484 | | #[test] |
485 | | fn test_buffer_renderer_rgba_output() { |
486 | | let renderer = BufferRenderer::new(); |
487 | | let buffer: Vec<f32> = (0..4).map(|i| i as f32 / 3.0).collect(); |
488 | | let rgba = renderer.render_to_rgba(&buffer, 2, 2); |
489 | | |
490 | | // 4 pixels * 4 bytes = 16 bytes |
491 | | assert_eq!(rgba.len(), 16); |
492 | | |
493 | | // Check alpha channel is always 255 |
494 | | for i in (3..16).step_by(4) { |
495 | | assert_eq!(rgba[i], 255); |
496 | | } |
497 | | } |
498 | | |
499 | | #[test] |
500 | | fn test_buffer_renderer_nan_handling() { |
501 | | let renderer = BufferRenderer::new(); |
502 | | let buffer = vec![0.0, f32::NAN, 1.0, 0.5]; |
503 | | let rgba = renderer.render_to_rgba(&buffer, 2, 2); |
504 | | |
505 | | // Second pixel should be NAN_COLOR (magenta: 255, 0, 255) |
506 | | assert_eq!(rgba[4], 255); // R |
507 | | assert_eq!(rgba[5], 0); // G |
508 | | assert_eq!(rgba[6], 255); // B |
509 | | assert_eq!(rgba[7], 255); // A |
510 | | } |
511 | | |
512 | | #[test] |
513 | | fn test_buffer_renderer_inf_handling() { |
514 | | let renderer = BufferRenderer::new(); |
515 | | let buffer = vec![f32::INFINITY, f32::NEG_INFINITY, 0.5, 0.5]; |
516 | | let rgba = renderer.render_to_rgba(&buffer, 2, 2); |
517 | | |
518 | | // First pixel: +INF should be white |
519 | | assert_eq!(rgba[0], 255); |
520 | | assert_eq!(rgba[1], 255); |
521 | | assert_eq!(rgba[2], 255); |
522 | | |
523 | | // Second pixel: -INF should be black |
524 | | assert_eq!(rgba[4], 0); |
525 | | assert_eq!(rgba[5], 0); |
526 | | assert_eq!(rgba[6], 0); |
527 | | } |
528 | | |
529 | | #[test] |
530 | | fn test_buffer_renderer_compare_identical() { |
531 | | let renderer = BufferRenderer::new(); |
532 | | let buffer: Vec<f32> = (0..16).map(|i| i as f32 / 15.0).collect(); |
533 | | let rgba = renderer.render_to_rgba(&buffer, 4, 4); |
534 | | |
535 | | let result = renderer.compare_rgba(&rgba, &rgba, 0); |
536 | | assert_eq!(result.different_pixels, 0); |
537 | | assert!(result.matches(0.0)); |
538 | | } |
539 | | |
540 | | #[test] |
541 | | fn test_buffer_renderer_compare_different() { |
542 | | let renderer = BufferRenderer::new(); |
543 | | let buffer_a: Vec<f32> = (0..16).map(|i| i as f32 / 15.0).collect(); |
544 | | let buffer_b: Vec<f32> = (0..16).map(|i| 1.0 - i as f32 / 15.0).collect(); |
545 | | |
546 | | let rgba_a = renderer.render_to_rgba(&buffer_a, 4, 4); |
547 | | let rgba_b = renderer.render_to_rgba(&buffer_b, 4, 4); |
548 | | |
549 | | let result = renderer.compare_rgba(&rgba_a, &rgba_b, 0); |
550 | | assert!(result.different_pixels > 0); |
551 | | } |
552 | | |
553 | | #[test] |
554 | | fn test_buffer_renderer_compare_with_tolerance() { |
555 | | let renderer = BufferRenderer::new(); |
556 | | let rgba_a = vec![100, 100, 100, 255]; |
557 | | let rgba_b = vec![105, 102, 98, 255]; |
558 | | |
559 | | // With tolerance 10, should match |
560 | | let result = renderer.compare_rgba(&rgba_a, &rgba_b, 10); |
561 | | assert_eq!(result.different_pixels, 0); |
562 | | |
563 | | // With tolerance 1, should differ |
564 | | let result_strict = renderer.compare_rgba(&rgba_a, &rgba_b, 1); |
565 | | assert!(result_strict.different_pixels > 0); |
566 | | } |
567 | | |
568 | | #[test] |
569 | | fn test_golden_baseline_paths() { |
570 | | let config = VisualRegressionConfig::new("/test/golden").with_output_dir("/test/output"); |
571 | | let baseline = GoldenBaseline::new(config); |
572 | | |
573 | | assert_eq!( |
574 | | baseline.golden_path("relu_4x4"), |
575 | | PathBuf::from("/test/golden/relu_4x4.golden") |
576 | | ); |
577 | | assert_eq!( |
578 | | baseline.output_path("relu_4x4"), |
579 | | PathBuf::from("/test/output/relu_4x4.output") |
580 | | ); |
581 | | } |
582 | | |
583 | | #[test] |
584 | | fn test_golden_baseline_config_access() { |
585 | | let config = VisualRegressionConfig::new("/golden").with_max_diff_pct(2.5); |
586 | | let baseline = GoldenBaseline::new(config); |
587 | | |
588 | | assert_eq!(baseline.config().max_diff_pct, 2.5); |
589 | | } |
590 | | } |