Skip to content

Commit 9ce04e1

Browse files
committed
test: add pixel value verification to animation round-trip tests
Previously, animation tests verified frame counts and dimensions but never checked actual pixel values — a color-corrupting encoder would pass all tests. Now every round-trip test asserts decoded pixel values are within tolerance of the input: - frame_source_decode_3_frames: gray values match build_test_gif formula - frame_sink_encode_2_frames: frame 0 is red, frame 1 is blue - transcode_gif_passthrough: gray values survive encode/decode - transcode_gif_with_crop: gray values survive crop + encode/decode
1 parent 27e4ef7 commit 9ce04e1

File tree

1 file changed

+92
-10
lines changed

1 file changed

+92
-10
lines changed

tests/animation.rs

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,21 @@ fn frame_source_decode_3_frames() {
9393
assert_eq!(source.width(), 8);
9494
assert_eq!(source.height(), 8);
9595

96-
drain(&mut source);
96+
let frame_count = 3usize;
97+
let frame0_pixels = drain(&mut source);
98+
assert_pixel_gray(&frame0_pixels, 0, frame_count);
9799

98100
assert!(source.advance_frame().unwrap());
99101
let info = source.frame_info().unwrap();
100102
assert_eq!(info.index, 1);
101-
drain(&mut source);
103+
let frame1_pixels = drain(&mut source);
104+
assert_pixel_gray(&frame1_pixels, 1, frame_count);
102105

103106
assert!(source.advance_frame().unwrap());
104107
let info = source.frame_info().unwrap();
105108
assert_eq!(info.index, 2);
106-
drain(&mut source);
109+
let frame2_pixels = drain(&mut source);
110+
assert_pixel_gray(&frame2_pixels, 2, frame_count);
107111

108112
assert!(!source.advance_frame().unwrap());
109113
assert!(source.frame_info().is_none());
@@ -164,8 +168,27 @@ fn frame_sink_encode_2_frames() {
164168
.animation_frame_decoder(Cow::Owned(encoded), &[PixelDescriptor::RGBA8_SRGB])
165169
.unwrap();
166170

167-
assert!(verify.render_next_frame_owned(None).unwrap().is_some());
168-
assert!(verify.render_next_frame_owned(None).unwrap().is_some());
171+
// Frame 0: should be reddish
172+
let frame0 = verify.render_next_frame_owned(None).unwrap().expect("missing frame 0");
173+
let px0 = frame0.pixels().as_strided_bytes();
174+
// Sample first pixel (RGBA)
175+
assert!(px0.len() >= 4, "frame 0 pixel data too short");
176+
let (r0, g0, b0) = (px0[0], px0[1], px0[2]);
177+
assert!(
178+
r0 > 200 && g0 < 50 && b0 < 50,
179+
"frame 0 should be reddish, got r={r0} g={g0} b={b0}"
180+
);
181+
182+
// Frame 1: should be bluish
183+
let frame1 = verify.render_next_frame_owned(None).unwrap().expect("missing frame 1");
184+
let px1 = frame1.pixels().as_strided_bytes();
185+
assert!(px1.len() >= 4, "frame 1 pixel data too short");
186+
let (r1, g1, b1) = (px1[0], px1[1], px1[2]);
187+
assert!(
188+
b1 > 200 && r1 < 50 && g1 < 50,
189+
"frame 1 should be bluish, got r={r1} g={g1} b={b1}"
190+
);
191+
169192
assert!(verify.render_next_frame_owned(None).unwrap().is_none());
170193
}
171194

@@ -196,11 +219,26 @@ fn transcode_gif_passthrough() {
196219
.animation_frame_decoder(Cow::Owned(encoded), &[PixelDescriptor::RGBA8_SRGB])
197220
.unwrap();
198221

199-
for i in 0..3 {
222+
let frame_count = 3usize;
223+
for i in 0..frame_count {
224+
let frame = verify
225+
.render_next_frame_owned(None)
226+
.unwrap()
227+
.unwrap_or_else(|| panic!("missing frame {i}"));
228+
let px = frame.pixels().as_strided_bytes();
229+
assert!(px.len() >= 4, "frame {i} pixel data too short");
230+
231+
// build_test_gif produces gray = (i+1)*255/frame_count for each frame
232+
let expected_gray = ((i + 1) * 255 / frame_count) as u8;
233+
let (r, g, b, a) = (px[0], px[1], px[2], px[3]);
234+
let tolerance = 32i16; // GIF quantization loses precision
200235
assert!(
201-
verify.render_next_frame_owned(None).unwrap().is_some(),
202-
"missing frame {i}"
236+
(r as i16 - expected_gray as i16).abs() <= tolerance
237+
&& (g as i16 - expected_gray as i16).abs() <= tolerance
238+
&& (b as i16 - expected_gray as i16).abs() <= tolerance,
239+
"frame {i}: expected ~gray({expected_gray}), got r={r} g={g} b={b}"
203240
);
241+
assert!(a > 200, "frame {i}: alpha should be opaque, got a={a}");
204242
}
205243
assert!(verify.render_next_frame_owned(None).unwrap().is_none());
206244
}
@@ -245,15 +283,59 @@ fn transcode_gif_with_crop() {
245283

246284
assert_eq!(verify.info().width, 4);
247285
assert_eq!(verify.info().height, 4);
248-
assert!(verify.render_next_frame_owned(None).unwrap().is_some());
249-
assert!(verify.render_next_frame_owned(None).unwrap().is_some());
286+
287+
// Solid-color frames survive cropping — verify pixel values still match
288+
let crop_frame_count = 2usize;
289+
for i in 0..crop_frame_count {
290+
let frame = verify
291+
.render_next_frame_owned(None)
292+
.unwrap()
293+
.unwrap_or_else(|| panic!("missing cropped frame {i}"));
294+
let px = frame.pixels().as_strided_bytes();
295+
assert!(px.len() >= 4, "cropped frame {i} pixel data too short");
296+
297+
let expected_gray = ((i + 1) * 255 / crop_frame_count) as u8;
298+
let (r, g, b, a) = (px[0], px[1], px[2], px[3]);
299+
let tolerance = 32i16;
300+
assert!(
301+
(r as i16 - expected_gray as i16).abs() <= tolerance
302+
&& (g as i16 - expected_gray as i16).abs() <= tolerance
303+
&& (b as i16 - expected_gray as i16).abs() <= tolerance,
304+
"cropped frame {i}: expected ~gray({expected_gray}), got r={r} g={g} b={b}"
305+
);
306+
assert!(a > 200, "cropped frame {i}: alpha should be opaque, got a={a}");
307+
}
250308
assert!(verify.render_next_frame_owned(None).unwrap().is_none());
251309
}
252310

253311
// =========================================================================
254312
// Helpers
255313
// =========================================================================
256314

315+
/// Assert that the first pixel of RGBA frame data matches the expected gray value
316+
/// from `build_test_gif`: gray = (frame_index+1)*255/frame_count.
317+
/// Allows ±32 tolerance for GIF palette quantization.
318+
fn assert_pixel_gray(pixel_data: &[u8], frame_index: usize, frame_count: usize) {
319+
assert!(
320+
pixel_data.len() >= 4,
321+
"frame {frame_index} pixel data too short ({} bytes)",
322+
pixel_data.len()
323+
);
324+
let expected_gray = ((frame_index + 1) * 255 / frame_count) as u8;
325+
let (r, g, b, a) = (pixel_data[0], pixel_data[1], pixel_data[2], pixel_data[3]);
326+
let tolerance = 32i16;
327+
assert!(
328+
(r as i16 - expected_gray as i16).abs() <= tolerance
329+
&& (g as i16 - expected_gray as i16).abs() <= tolerance
330+
&& (b as i16 - expected_gray as i16).abs() <= tolerance,
331+
"frame {frame_index}: expected ~gray({expected_gray}), got r={r} g={g} b={b}"
332+
);
333+
assert!(
334+
a > 200,
335+
"frame {frame_index}: alpha should be opaque, got a={a}"
336+
);
337+
}
338+
257339
fn make_solid_source(width: u32, height: u32, pixel: [u8; 4]) -> Box<dyn Source> {
258340
let row_bytes = width as usize * 4;
259341
let mut rows_produced = 0u32;

0 commit comments

Comments
 (0)