1use super::*;
4use zencodec::animation::FrameDuration;
5use zencodec::encode::DynEncoderConfig;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ExpectedError {
10 Unsupported(UnsupportedOperation),
11 Category(ErrorCategory),
12}
13impl ExpectedError {
14 pub fn matches(self, error: &(dyn std::error::Error + 'static)) -> bool {
17 match self {
18 Self::Unsupported(op) => error.unsupported_operation() == Some(&op),
19 Self::Category(category) => error.error_category() == Some(category),
20 }
21 }
22}
23
24pub enum PrefixOutcome<'a> {
26 Accept(PixelSlice<'a>),
28 Reject(ExpectedError),
29}
30
31pub struct PrefixCase<'a> {
33 pub end: usize,
34 pub outcome: PrefixOutcome<'a>,
35}
36
37pub fn check_decode_prefix_cases<D: DecoderConfig + 'static>(
42 decoder: D,
43 bytes: &[u8],
44 cases: &[PrefixCase<'_>],
45) -> Conformance {
46 const CHECK: &str = "structural decode prefixes";
47 if cases.is_empty() {
48 return Err(fail(CHECK, "supply explicit prefix cases"));
49 }
50 for case in cases {
51 let input = bytes
52 .get(..case.end)
53 .ok_or_else(|| fail(CHECK, "prefix exceeds fixture"))?;
54 for sink_path in [false, true] {
55 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
56 let job = decoder.dyn_job();
57 if sink_path {
58 let mut sink = CollectSink::default();
59 job.push_decode(Cow::Borrowed(input), &mut sink, &[])?;
60 Ok(sink.into_pixels().map_err(|e| fail(CHECK, e)))
61 } else {
62 let output = job.into_decoder(Cow::Borrowed(input), &[])?.decode()?;
63 Ok(Ok(grab(output.pixels())))
64 }
65 }));
66 let result: Result<Result<Pixels, Failure>, zencodec::decode::BoxedError> = result
67 .map_err(|_| fail(CHECK, format!("panic at {} (sink={sink_path})", case.end)))?;
68 match (&case.outcome, result) {
69 (PrefixOutcome::Accept(expected), Ok(pixels)) => {
70 if pixels? != grab_ref(expected) {
71 return Err(fail(CHECK, format!("wrong pixels at {}", case.end)));
72 }
73 }
74 (PrefixOutcome::Reject(expected), Err(error))
75 if expected.matches(error.as_ref()) => {}
76 (_, result) => {
77 return Err(fail(
78 CHECK,
79 format!(
80 "unexpected result at {} (sink={sink_path}): {result:?}",
81 case.end
82 ),
83 ));
84 }
85 }
86 }
87 }
88 Ok(())
89}
90
91pub struct TimingCase<'a> {
95 pub pixels: PixelSlice<'a>,
96 pub requested: FrameDuration,
97 pub expected: Result<(FrameDuration, PixelSlice<'a>), ExpectedError>,
98}
99
100pub fn check_animation_cases<E, D>(
104 enc: E,
105 dec: D,
106 exact: bool,
107 cases: &[TimingCase<'_>],
108) -> Conformance
109where
110 E: EncoderConfig + 'static,
111 D: DecoderConfig + 'static,
112 <E::Job as EncodeJob>::Enc: Encoder + Send,
113 <E::Job as EncodeJob>::AnimationFrameEnc: AnimationFrameEncoder,
114{
115 const CHECK: &str = "explicit animation cases";
116 let accepted: Vec<_> = cases
117 .iter()
118 .filter_map(|c| c.expected.as_ref().ok())
119 .collect();
120 if accepted.len() < 2 {
121 return Err(fail(CHECK, "supply at least two accepted frames"));
122 }
123 let mut encoder = enc
126 .dyn_job()
127 .into_animation_frame_encoder()
128 .map_err(|e| fail(CHECK, e.to_string()))?;
129 for (index, case) in cases.iter().enumerate() {
130 if exact
131 && case
132 .expected
133 .as_ref()
134 .is_ok_and(|(duration, _)| *duration != case.requested)
135 {
136 return Err(fail(CHECK, "exact fixture expects rounding"));
137 }
138 let pixels = case.pixels.sub_rows(0, case.pixels.rows());
139 let result = if exact {
140 encoder
141 .push_frame_exact(pixels, case.requested, None)
142 .map(|()| case.requested)
143 } else {
144 encoder.push_frame_timed(pixels, case.requested, None)
145 };
146 match (&case.expected, result) {
147 (Ok((duration, _)), Ok(actual)) if *duration == actual => (),
148 (Err(expected), Err(error)) if expected.matches(error.as_ref()) => (),
149 (_, result) => {
150 return Err(fail(
151 CHECK,
152 format!("frame {index}: unexpected admission {result:?}"),
153 ));
154 }
155 }
156 }
157 let output = encoder
158 .finish(None)
159 .map_err(|e| fail(CHECK, e.to_string()))?;
160 let durations: Vec<_> = accepted.iter().map(|(duration, _)| *duration).collect();
161 check_animation_durations(dec.clone(), output.data(), &durations)?;
162 let mut decoder = dec
163 .dyn_job()
164 .into_animation_frame_decoder(Cow::Borrowed(output.data()), &[])
165 .map_err(|e| fail(CHECK, e.to_string()))?;
166 for (index, (_, expected)) in accepted.iter().enumerate() {
167 let frame = decoder
168 .render_next_frame_owned(None)
169 .map_err(|e| fail(CHECK, e.to_string()))?
170 .ok_or_else(|| fail(CHECK, "missing frame"))?;
171 if grab(frame.pixels()) != grab_ref(expected) {
172 return Err(fail(
173 CHECK,
174 format!("frame {index}: pixels differ from independent oracle"),
175 ));
176 }
177 }
178 Ok(())
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn typed_rejection_does_not_accept_unrelated_errors() {
187 let expected = ExpectedError::Unsupported(UnsupportedOperation::AnimationTiming);
188 assert!(expected.matches(&UnsupportedOperation::AnimationTiming));
189 assert!(!expected.matches(&UnsupportedOperation::AnimationEncode));
190 assert!(!expected.matches(&std::io::Error::other("internal failure")));
191 }
192
193 #[test]
194 fn distinct_frame_oracles_detect_reordering_and_wrong_pixels() {
195 let a = TestImage::rgba8_gradient_seeded(4, 3, 0);
196 let b = TestImage::rgba8_gradient_seeded(4, 3, 37);
197 let ms = FrameDuration::from_millis;
198 let cases = [
199 TimingCase {
200 pixels: a.as_slice(),
201 requested: ms(40),
202 expected: Ok((ms(40), a.as_slice())),
203 },
204 TimingCase {
205 pixels: b.as_slice(),
206 requested: ms(60),
207 expected: Ok((ms(60), b.as_slice())),
208 },
209 ];
210 check_animation_cases(
211 ReferenceEncoderConfig::new(),
212 ReferenceDecoderConfig,
213 true,
214 &cases,
215 )
216 .unwrap();
217 let wrong = [
218 TimingCase {
219 pixels: a.as_slice(),
220 requested: ms(40),
221 expected: Ok((ms(40), b.as_slice())),
222 },
223 TimingCase {
224 pixels: b.as_slice(),
225 requested: ms(60),
226 expected: Ok((ms(60), a.as_slice())),
227 },
228 ];
229 assert!(
230 check_animation_cases(
231 ReferenceEncoderConfig::new(),
232 ReferenceDecoderConfig,
233 true,
234 &wrong
235 )
236 .is_err()
237 );
238 }
239
240 #[test]
241 fn structural_prefixes_allow_complete_payload_before_a_large_trailer() {
242 let image = TestImage::rgb8_gradient(3, 2);
243 let bytes = MinimalEncoderConfig::new()
244 .encode(image.as_slice())
245 .unwrap();
246 let complete = bytes.data().len();
247 let mut trailing = bytes.data().to_vec();
248 trailing.resize(complete * 8, 0);
249 let cases = [
250 PrefixCase {
251 end: 0,
252 outcome: PrefixOutcome::Reject(ExpectedError::Category(ErrorCategory::Image(
253 zencodec::ImageError::Malformed,
254 ))),
255 },
256 PrefixCase {
257 end: complete,
258 outcome: PrefixOutcome::Accept(image.as_slice()),
259 },
260 ];
261 check_decode_prefix_cases(MinimalDecoderConfig::new(), &trailing, &cases).unwrap();
262 let cases = [PrefixCase {
264 end: complete - 1,
265 outcome: PrefixOutcome::Reject(ExpectedError::Category(ErrorCategory::Image(
266 zencodec::ImageError::Malformed,
267 ))),
268 }];
269 check_decode_prefix_cases(MinimalDecoderConfig::new(), bytes.data(), &cases).unwrap();
270 let wrong = TestImage::rgb8_gradient(4, 2);
271 let cases = [PrefixCase {
272 end: complete,
273 outcome: PrefixOutcome::Accept(wrong.as_slice()),
274 }];
275 assert!(
276 check_decode_prefix_cases(MinimalDecoderConfig::new(), bytes.data(), &cases).is_err()
277 );
278 let cases = [PrefixCase {
279 end: complete - 1,
280 outcome: PrefixOutcome::Accept(image.as_slice()),
281 }];
282 assert!(
283 check_decode_prefix_cases(MinimalDecoderConfig::new(), bytes.data(), &cases).is_err()
284 );
285 }
286}