1use crate::decode;
60use crate::huffman::ZIGZAG;
61use crate::idct::dequant_idct_into;
62use pith_digest::fnv1a64;
63use pith_math::idct2_2d;
64
65pub const TOL_ABS: f64 = 1e-13;
68
69pub const TOL_REL: f64 = 1e-12;
72
73#[derive(Clone, Debug)]
76pub struct Vector {
77 pub name: &'static str,
79 pub exact: bool,
82 pub shape: Option<[usize; 2]>,
84 pub tol_abs: f64,
87 pub tol_rel: f64,
89 pub input: Vec<f64>,
91 pub output: Vec<f64>,
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub struct Digest {
98 pub name: &'static str,
100 pub file: &'static str,
102}
103
104#[must_use]
108pub fn digests() -> &'static [Digest] {
109 DIGESTS
110}
111
112const DIGESTS: &[Digest] = &[
113 Digest {
114 name: "base_420_odd",
115 file: "base_420_odd.jpg",
116 },
117 Digest {
118 name: "base_420_rst",
119 file: "base_420_rst.jpg",
120 },
121 Digest {
122 name: "base_422",
123 file: "base_422.jpg",
124 },
125 Digest {
126 name: "base_422_odd",
127 file: "base_422_odd.jpg",
128 },
129 Digest {
130 name: "base_440",
131 file: "base_440.jpg",
132 },
133 Digest {
134 name: "base_444",
135 file: "base_444.jpg",
136 },
137 Digest {
138 name: "base_gray",
139 file: "base_gray.jpg",
140 },
141 Digest {
142 name: "prog_420",
143 file: "prog_420.jpg",
144 },
145 Digest {
146 name: "prog_444",
147 file: "prog_444.jpg",
148 },
149 Digest {
150 name: "prog_444_rst",
151 file: "prog_444_rst.jpg",
152 },
153 Digest {
154 name: "prog_gray",
155 file: "prog_gray.jpg",
156 },
157];
158
159fn coef_block() -> Vec<f64> {
163 (0..64)
164 .map(|i: i32| (((i * 37 + 11) % 1024) - 512) as f64)
165 .collect()
166}
167
168fn quant_block() -> Vec<f64> {
171 (0..64)
172 .map(|i: u16| ((i * 17 + 3) % 32 + 1) as f64)
173 .collect()
174}
175
176fn scan_payload() -> Vec<f64> {
178 (0..64)
179 .map(|k: u16| f64::from(((k * 91 + 13) % 256) as u8))
180 .collect()
181}
182
183fn dequantized() -> Vec<f64> {
185 let coefs = coef_block();
186 let qt = quant_block();
187 coefs.iter().zip(qt.iter()).map(|(&c, &q)| c * q).collect()
188}
189
190#[must_use]
193pub fn vectors() -> Vec<Vector> {
194 let mut v: Vec<Vector> = Vec::new();
195
196 let payload = scan_payload();
198 let mut natural = vec![0.0; 64];
199 for (k, &val) in payload.iter().enumerate() {
200 natural[ZIGZAG[k] as usize] = val;
201 }
202 v.push(Vector {
203 name: "dequant.zigzag.8x8",
204 exact: true,
205 shape: Some([8, 8]),
206 tol_abs: TOL_ABS,
207 tol_rel: TOL_REL,
208 input: payload,
209 output: natural,
210 });
211
212 let coefs = coef_block();
214 let qt = quant_block();
215 let products = dequantized();
216 let mut input = coefs;
217 input.extend_from_slice(&qt);
218 v.push(Vector {
219 name: "dequant.8x8",
220 exact: true,
221 shape: None,
222 tol_abs: TOL_ABS,
223 tol_rel: TOL_REL,
224 input,
225 output: products.clone(),
226 });
227
228 let coefs = coef_block();
230 let qt = quant_block();
231 let coefs: Vec<i32> = coefs.iter().map(|&c| c as i32).collect();
232 let qt: Vec<u16> = qt.iter().map(|&q| q as u16).collect();
233 let mut plane = [0u8; 64];
234 dequant_idct_into(&coefs, &qt, &mut plane, 8, 0, 0);
235 v.push(Vector {
236 name: "idct.islow.8x8",
237 exact: true,
238 shape: None,
239 tol_abs: TOL_ABS,
240 tol_rel: TOL_REL,
241 input: {
242 let mut input = coefs.iter().map(|&c| f64::from(c)).collect::<Vec<f64>>();
243 input.extend(qt.iter().map(|&q| f64::from(q)));
244 input
245 },
246 output: plane.iter().map(|&s| f64::from(s)).collect(),
247 });
248
249 let mut f = dequantized();
251 idct2_2d(&mut f, 8, 8);
252 v.push(Vector {
253 name: "idct.oracle.8x8",
254 exact: false,
255 shape: Some([8, 8]),
256 tol_abs: TOL_ABS,
257 tol_rel: TOL_REL,
258 input: dequantized(),
259 output: f,
260 });
261
262 v
263}
264
265#[must_use]
278pub fn digest_of(d: &Digest) -> u64 {
279 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
280 .join("tests")
281 .join("fixtures")
282 .join(d.file);
283 let bytes = std::fs::read(&path)
284 .unwrap_or_else(|e| panic!("cannot read fixture {}: {e}", path.display()));
285 match decode(&bytes) {
286 Ok(crate::Jpeg::Gray(img)) => fnv1a64(img.as_slice()),
287 Ok(crate::Jpeg::Rgb(img)) => fnv1a64(img.as_slice()),
288 Err(why) => panic!("fixture {} no longer decodes: {why}", d.file),
289 }
290}
291
292#[must_use]
294pub fn digest_values() -> Vec<(&'static str, u64)> {
295 DIGESTS.iter().map(|d| (d.name, digest_of(d))).collect()
296}
297
298#[must_use]
300pub fn hx(v: f64) -> String {
301 format!("{:016x}", v.to_bits())
302}
303
304fn push_f64s(out: &mut String, vs: &[f64], indent: &str) {
305 out.push_str("[\n");
306 for v in vs {
307 out.push_str(indent);
308 out.push('"');
309 out.push_str(&hx(*v));
310 out.push_str("\",\n");
311 }
312 if !vs.is_empty() {
314 let l = out.len() - 2;
315 out.truncate(l);
316 out.push('\n');
317 }
318 out.push_str(indent.trim_end());
319 out.push(']');
320}
321
322#[must_use]
335pub fn reference_json() -> String {
336 let mut digests: Vec<(&str, u64)> = digest_values();
337 digests.sort_unstable_by_key(|&(name, _)| name);
338 let mut vectors = vectors();
339 vectors.sort_unstable_by(|a, b| a.name.cmp(b.name));
340
341 let mut out = String::new();
342 out.push_str("{\n");
343 out.push_str(" \"schema\": 1,\n");
344 out.push_str(" \"suite\": \"pith-jpeg\",\n");
345 out.push_str(
346 " \"note\": \"every f64 is its raw IEEE-754 bit pattern as 16-digit \
347lowercase hex; exact vectors must match bit-for-bit, the orthonormal-oracle \
348vector within max(tol_abs, tol_rel * |expected|); digests are fnv1a64 over \
349the decoded sample bytes (row-major, channel-interleaved); dequant.8x8 and \
350idct.islow.8x8 inputs are 64 coefficients followed by 64 quant values, \
351natural order\",\n",
352 );
353 out.push_str(" \"digests\": {\n");
354 for (i, (name, digest)) in digests.iter().enumerate() {
355 out.push_str(&format!(" \"{name}\": \"{digest:016x}\""));
356 out.push_str(if i + 1 == digests.len() { "\n" } else { ",\n" });
357 }
358 out.push_str(" },\n");
359 out.push_str(" \"vectors\": {\n");
360 for (i, v) in vectors.iter().enumerate() {
361 out.push_str(" \"");
362 out.push_str(v.name);
363 out.push_str("\": {\n");
364 out.push_str(" \"exact\": ");
365 out.push_str(if v.exact { "true" } else { "false" });
366 out.push_str(",\n");
367 if let Some([w, h]) = v.shape {
368 out.push_str(&format!(" \"shape\": [{w}, {h}],\n"));
369 }
370 out.push_str(" \"tol_abs\": \"");
371 out.push_str(&hx(v.tol_abs));
372 out.push_str("\",\n");
373 out.push_str(" \"tol_rel\": \"");
374 out.push_str(&hx(v.tol_rel));
375 out.push_str("\",\n");
376 out.push_str(" \"input\": ");
377 push_f64s(&mut out, &v.input, " ");
378 out.push_str(",\n");
379 out.push_str(" \"output\": ");
380 push_f64s(&mut out, &v.output, " ");
381 out.push('\n');
382 out.push_str(" }");
383 out.push_str(if i + 1 == vectors.len() { "\n" } else { ",\n" });
384 }
385 out.push_str(" }\n}\n");
386 out
387}
388
389pub fn verify() -> Result<(), String> {
403 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("reference.json");
404 let committed = std::fs::read_to_string(&path)
405 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
406 verify_str(&committed)
407}
408
409struct Committed {
411 exact: bool,
412 tol_abs: f64,
413 tol_rel: f64,
414 input: Vec<f64>,
415 output: Vec<f64>,
416}
417
418pub fn verify_str(committed: &str) -> Result<(), String> {
430 let mut p = Json::new(committed);
431 p.expect(b'{')?;
432 let mut committed_digests: Vec<(String, u64)> = Vec::new();
433 let mut committed_vectors: Vec<(String, Committed)> = Vec::new();
434 loop {
435 match p.peek() {
436 Some(b'}') => break,
437 Some(b'"') => {
438 let key = p.string()?;
439 p.expect(b':')?;
440 match key.as_str() {
441 "digests" => committed_digests = p.entries(Json::hex_u64)?,
442 "vectors" => committed_vectors = p.entries(Json::vector)?,
443 _ => p.skip_value()?,
444 }
445 if p.peek() == Some(b',') {
446 p.i += 1;
447 }
448 }
449 got => {
450 return Err(format!(
451 "expected key or }}, found {:?} at byte {}",
452 got.map(char::from),
453 p.i
454 ));
455 }
456 }
457 }
458
459 let mut fails: Vec<String> = Vec::new();
460
461 let fresh = digest_values();
463 for (name, want) in &fresh {
464 match committed_digests.iter().find(|(n, _)| n == name) {
465 None => fails.push(format!("[digest {name}] missing from committed file")),
466 Some((_, got)) => {
467 if *got != *want {
468 fails.push(format!(
469 "[digest {name}] drifted: committed {got:016x}, recomputed {want:016x}"
470 ));
471 }
472 }
473 }
474 }
475 for (name, _) in &committed_digests {
476 if !fresh.iter().any(|&(n, _)| n == name.as_str()) {
477 fails.push(format!("[digest {name}] not recomputable: unknown fixture"));
478 }
479 }
480
481 let fresh_vectors = vectors();
483 for want in &fresh_vectors {
484 let pos = committed_vectors
485 .iter()
486 .position(|(name, _)| name == want.name);
487 let Some(idx) = pos else {
488 fails.push(format!("[{}] missing from committed file", want.name));
489 continue;
490 };
491 let (_, got) = committed_vectors.swap_remove(idx);
492 if got.exact != want.exact {
493 fails.push(format!("[{}] exact flag drifted", want.name));
494 }
495 if got.tol_abs.to_bits() != want.tol_abs.to_bits()
496 || got.tol_rel.to_bits() != want.tol_rel.to_bits()
497 {
498 fails.push(format!("[{}] tolerance budgets drifted", want.name));
499 }
500 if got.input.len() != want.input.len() || got.output.len() != want.output.len() {
501 fails.push(format!("[{}] length drifted", want.name));
502 continue;
503 }
504 let input_drift = got
505 .input
506 .iter()
507 .zip(&want.input)
508 .any(|(g, w)| g.to_bits() != w.to_bits());
509 if input_drift {
510 fails.push(format!("[{}] input drifted", want.name));
511 }
512 for (i, (g, w)) in got.output.iter().zip(&want.output).enumerate() {
513 if want.exact {
514 if g.to_bits() != w.to_bits() {
515 fails.push(format!(
516 "[{}] output[{i}] drifted: committed {}, recomputed {}",
517 want.name,
518 hx(*g),
519 hx(*w)
520 ));
521 break;
522 }
523 } else {
524 let budget = want.tol_abs.max(want.tol_rel * w.abs());
525 let err = (g - w).abs();
526 if err > budget {
527 fails.push(format!(
528 "[{}] output[{i}] drifted beyond tolerance: committed {}, recomputed {}, err {err:e}, budget {budget:e}",
529 want.name,
530 hx(*g),
531 hx(*w)
532 ));
533 break;
534 }
535 }
536 }
537 }
538 for (name, _) in &committed_vectors {
539 if !fresh_vectors.iter().any(|v| v.name == name.as_str()) {
540 fails.push(format!("[{name}] not recomputable: unknown vector"));
541 }
542 }
543
544 if fails.is_empty() {
545 Ok(())
546 } else {
547 Err(fails.join("\n"))
548 }
549}
550
551struct Json<'a> {
555 b: &'a [u8],
556 i: usize,
557}
558
559impl<'a> Json<'a> {
560 fn new(s: &'a str) -> Self {
561 Json {
562 b: s.as_bytes(),
563 i: 0,
564 }
565 }
566
567 fn ws(&mut self) {
568 while self.i < self.b.len() && (self.b[self.i] as char).is_ascii_whitespace() {
569 self.i += 1;
570 }
571 }
572
573 fn peek(&mut self) -> Option<u8> {
574 self.ws();
575 self.b.get(self.i).copied()
576 }
577
578 fn expect(&mut self, c: u8) -> Result<(), String> {
579 match self.peek() {
580 Some(got) if got == c => {
581 self.i += 1;
582 Ok(())
583 }
584 got => Err(format!(
585 "expected {:?}, found {:?} at byte {}",
586 c as char,
587 got.map(char::from),
588 self.i
589 )),
590 }
591 }
592
593 fn string(&mut self) -> Result<String, String> {
594 self.expect(b'"')?;
595 let start = self.i;
596 while self.i < self.b.len() && self.b[self.i] != b'"' {
597 self.i += 1;
598 }
599 if self.i >= self.b.len() {
600 return Err("unterminated string".into());
601 }
602 let s = core::str::from_utf8(&self.b[start..self.i]).map_err(|e| e.to_string())?;
603 self.i += 1;
604 Ok(s.to_owned())
605 }
606
607 fn hex_u64(&mut self) -> Result<u64, String> {
608 let s = self.string()?;
609 u64::from_str_radix(&s, 16).map_err(|e| format!("bad hex u64 {s:?}: {e}"))
610 }
611
612 fn hex_f64(&mut self) -> Result<f64, String> {
613 let s = self.string()?;
614 let bits = u64::from_str_radix(&s, 16).map_err(|e| format!("bad hex f64 {s:?}: {e}"))?;
615 Ok(f64::from_bits(bits))
616 }
617
618 fn boolean(&mut self) -> Result<bool, String> {
619 self.ws();
620 if self.b[self.i..].starts_with(b"true") {
621 self.i += 4;
622 Ok(true)
623 } else if self.b[self.i..].starts_with(b"false") {
624 self.i += 5;
625 Ok(false)
626 } else {
627 Err("expected boolean".into())
628 }
629 }
630
631 fn number(&mut self) -> Result<i64, String> {
632 self.ws();
633 let start = self.i;
634 while self.i < self.b.len() && (self.b[self.i] as char).is_ascii_digit() {
635 self.i += 1;
636 }
637 core::str::from_utf8(&self.b[start..self.i])
638 .map_err(|e| e.to_string())?
639 .parse::<i64>()
640 .map_err(|e| format!("bad integer: {e}"))
641 }
642
643 fn f64_array(&mut self) -> Result<Vec<f64>, String> {
644 self.expect(b'[')?;
645 let mut out = Vec::new();
646 if self.peek() == Some(b']') {
647 self.i += 1;
648 return Ok(out);
649 }
650 loop {
651 out.push(self.hex_f64()?);
652 match self.peek() {
653 Some(b',') => self.i += 1,
654 Some(b']') => {
655 self.i += 1;
656 return Ok(out);
657 }
658 got => {
659 return Err(format!(
660 "expected , or ] in array, found {:?}",
661 got.map(char::from)
662 ));
663 }
664 }
665 }
666 }
667
668 fn vector(&mut self) -> Result<Committed, String> {
669 self.expect(b'{')?;
670 let mut c = Committed {
671 exact: false,
672 tol_abs: 0.0,
673 tol_rel: 0.0,
674 input: Vec::new(),
675 output: Vec::new(),
676 };
677 loop {
678 match self.peek() {
679 Some(b'}') => {
680 self.i += 1;
681 return Ok(c);
682 }
683 Some(b'"') => {
684 let key = self.string()?;
685 self.expect(b':')?;
686 match key.as_str() {
687 "exact" => c.exact = self.boolean()?,
688 "tol_abs" => c.tol_abs = self.hex_f64()?,
689 "tol_rel" => c.tol_rel = self.hex_f64()?,
690 "input" => c.input = self.f64_array()?,
691 "output" => c.output = self.f64_array()?,
692 "shape" => self.skip_value()?,
693 other => return Err(format!("unexpected vector key {other:?}")),
694 }
695 if self.peek() == Some(b',') {
696 self.i += 1;
697 }
698 }
699 got => {
700 return Err(format!(
701 "expected key or }}, found {:?}",
702 got.map(char::from)
703 ));
704 }
705 }
706 }
707 }
708
709 fn skip_value(&mut self) -> Result<(), String> {
710 match self.peek() {
711 Some(b'"') => {
712 self.string()?;
713 }
714 Some(b'[') => {
719 self.i += 1;
720 let mut depth = 1;
721 while depth > 0 {
722 match self.peek() {
723 Some(b'[') => {
724 depth += 1;
725 self.i += 1;
726 }
727 Some(b']') => {
728 depth -= 1;
729 self.i += 1;
730 }
731 Some(_) => self.i += 1,
732 None => return Err("unterminated array".into()),
733 }
734 }
735 }
736 Some(b'{') => {
737 self.i += 1;
738 let mut depth = 1;
739 while depth > 0 {
740 match self.peek() {
741 Some(b'{') => {
742 depth += 1;
743 self.i += 1;
744 }
745 Some(b'}') => {
746 depth -= 1;
747 self.i += 1;
748 }
749 Some(_) => self.i += 1,
750 None => return Err("unterminated object".into()),
751 }
752 }
753 }
754 Some(b't' | b'f') => {
755 self.boolean()?;
756 }
757 Some(c) if c.is_ascii_digit() || c == b'-' => {
758 self.number()?;
759 }
760 got => return Err(format!("unexpected value start {:?}", got.map(char::from))),
761 }
762 Ok(())
763 }
764
765 fn entries<T>(
768 &mut self,
769 item: fn(&mut Self) -> Result<T, String>,
770 ) -> Result<Vec<(String, T)>, String> {
771 self.expect(b'{')?;
772 let mut out = Vec::new();
773 loop {
774 match self.peek() {
775 Some(b'}') => {
776 self.i += 1;
777 return Ok(out);
778 }
779 Some(b'"') => {
780 let name = self.string()?;
781 self.expect(b':')?;
782 let value = item(self)?;
783 out.push((name, value));
784 if self.peek() == Some(b',') {
785 self.i += 1;
786 }
787 }
788 got => {
789 return Err(format!(
790 "expected key or }}, found {:?}",
791 got.map(char::from)
792 ));
793 }
794 }
795 }
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use super::{
802 Committed, Digest, Json, TOL_ABS, TOL_REL, digest_of, digest_values, hx, reference_json,
803 vectors, verify_str,
804 };
805
806 #[test]
808 fn render_is_deterministic() {
809 assert_eq!(reference_json(), reference_json());
810 }
811
812 #[test]
814 fn verify_accepts_current() {
815 verify_str(&reference_json()).expect("current render verifies");
816 }
817
818 fn replace_unique(committed: &str, from: &str, to: &str) -> String {
821 assert_eq!(committed.matches(from).count(), 1, "{from} is not unique");
822 committed.replacen(from, to, 1)
823 }
824
825 fn unique_digest_hex(committed: &str) -> (&'static str, String) {
827 digest_values()
828 .iter()
829 .map(|&(name, digest)| (name, format!("{digest:016x}")))
830 .find(|(_, hex)| committed.matches(hex.as_str()).count() == 1)
831 .expect("a digest unique in the render")
832 }
833
834 fn unique_oracle_hex(committed: &str) -> (f64, String) {
837 vectors()
838 .into_iter()
839 .find(|v| v.name == "idct.oracle.8x8")
840 .expect("oracle vector")
841 .output
842 .into_iter()
843 .map(|v| (v, hx(v)))
844 .find(|(_, hex)| committed.matches(hex.as_str()).count() == 1)
845 .expect("an oracle output unique in the render")
846 }
847
848 #[test]
850 fn verify_rejects_stale_digest() {
851 let committed = reference_json();
852 let (name, hex) = unique_digest_hex(&committed);
853 let stale = replace_unique(&committed, &hex, "0000000000000000");
854 let err = verify_str(&stale).expect_err("stale digest must fail");
855 assert!(err.contains(&format!("[digest {name}]")), "{err}");
856 }
857
858 #[test]
860 fn verify_rejects_drifted_exact_output() {
861 let exact: Vec<_> = vectors().into_iter().filter(|v| v.exact).collect();
862 let committed = reference_json();
863 let (name, committed_hex, drifted_hex) = exact
867 .iter()
868 .find_map(|v| {
869 v.output.iter().find_map(|&s| {
870 let hex = hx(s);
871 let drifted = hx(f64::from_bits(s.to_bits() ^ 1));
872 (committed.matches(hex.as_str()).count() == 1).then_some((v.name, hex, drifted))
873 })
874 })
875 .expect("an exact output sample unique in the render");
876 let stale = committed.replacen(&committed_hex, &drifted_hex, 1);
877 let err = verify_str(&stale).expect_err("drifted exact output must fail");
878 assert!(err.contains(name), "{err}");
879 }
880
881 #[test]
884 fn verify_accepts_in_budget_oracle_drift() {
885 let committed = reference_json();
886 let (value, hex) = unique_oracle_hex(&committed);
887 let nudged = f64::from_bits(value.to_bits() + 1);
890 let stale = replace_unique(&committed, &hex, &hx(nudged));
891 verify_str(&stale).expect("one-ulp drift is inside budget");
892 }
893
894 #[test]
896 fn verify_rejects_out_of_budget_oracle_drift() {
897 let committed = reference_json();
898 let (value, hex) = unique_oracle_hex(&committed);
899 let blown = value + 1.0;
900 let stale = replace_unique(&committed, &hex, &hx(blown));
901 let err = verify_str(&stale).expect_err("unit drift must fail");
902 assert!(err.contains("idct.oracle.8x8"), "{err}");
903 }
904
905 #[test]
907 fn verify_rejects_unknown_vector() {
908 let extra = format!(
909 " \"bogus.vector\": {{\n \"exact\": true,\n \"tol_abs\": \"{}\",\n \"tol_rel\": \"{}\",\n \"input\": [\"0000000000000000\"],\n \"output\": [\"0000000000000000\"]\n }},\n",
910 hx(TOL_ABS),
911 hx(TOL_REL)
912 );
913 let stale = reference_json().replacen(
914 " \"vectors\": {\n",
915 &format!(" \"vectors\": {{\n{extra}"),
916 1,
917 );
918 let err = verify_str(&stale).expect_err("unknown vector must fail");
919 assert!(err.contains("bogus.vector"), "{err}");
920 }
921
922 #[test]
924 fn digests_are_stable_and_nonzero() {
925 for d in digest_values() {
926 assert_ne!(d.1, 0, "{} digests to zero", d.0);
927 }
928 let d = Digest {
929 name: "x",
930 file: "base_444.jpg",
931 };
932 assert_eq!(
933 digest_of(&d),
934 digest_values()
935 .iter()
936 .find(|(n, _)| *n == "base_444")
937 .unwrap()
938 .1
939 );
940 }
941
942 #[test]
944 fn tolerance_budgets_are_the_suite_floors() {
945 assert_eq!(hx(TOL_ABS), "3d3c25c268497682");
946 assert_eq!(hx(TOL_REL), "3d719799812dea11");
947 }
948
949 #[test]
951 fn json_walker_rejects_garbage() {
952 let mut p = Json::new("{");
953 assert!(p.entries(Json::hex_u64).is_err());
954 let mut p = Json::new("{\"a\": \"zz\"}");
955 assert!(p.entries(Json::hex_u64).is_err());
956 let mut p = Json::new("\"zz\" ");
957 assert!(p.hex_u64().is_err());
958 let mut p = Json::new("[1, 2]");
959 assert!(p.f64_array().is_err());
960 let mut p = Json::new("\"true\"");
961 assert!(p.boolean().is_err());
962 let mut p = Json::new("{} ");
963 assert!(p.skip_value().is_ok());
964 let mut p = Json::new("]");
965 assert!(p.skip_value().is_err());
966 }
967
968 #[test]
970 fn verify_rejects_broken_structure() {
971 assert!(verify_str("").is_err()); assert!(verify_str("[1]").is_err()); assert!(verify_str("{\"digests\": 5}").is_err()); assert!(verify_str("{\"bogus\": {\"a\": [1]}, \"tail\": [1, 2]}").is_err());
977 assert!(verify_str("{\"vectors\": {\"v\": {\"bogus\": 1}}}").is_err());
979 assert!(verify_str("{\"vectors\": {\"v\": {\"exact\": 1}}}").is_err());
981 assert!(verify_str("{\"digests\": {\"a\": \"zz\"}}").is_err());
983 assert!(verify_str("{}").is_err());
985 }
986
987 #[test]
990 #[should_panic(expected = "cannot read fixture")]
991 fn missing_fixture_panics() {
992 let _ = digest_of(&Digest {
993 name: "absent",
994 file: "does_not_exist.jpg",
995 });
996 }
997
998 #[test]
1001 #[should_panic(expected = "no longer decodes")]
1002 fn undecodable_fixture_panics() {
1003 let _ = digest_of(&Digest {
1004 name: "junk",
1005 file: "PROVENANCE.md",
1006 });
1007 }
1008
1009 #[test]
1011 fn committed_defaults() {
1012 let mut p = Json::new("{}");
1013 let c: Committed = p.vector().expect("empty object");
1014 assert!(!c.exact);
1015 assert!(c.input.is_empty() && c.output.is_empty());
1016 }
1017}