Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/tests/avi_corpus.rs

5.1 KiB, 17 runs

created by r1870400018:21356, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Does the AVI header reader agree with a player about real films?
2//!
3//! The unit tests beside the reader build their own files, so they prove the
4//! reader agrees with the author's idea of the format and nothing more. This
5//! asks a different program about films nobody here wrote: point `AVI_CORPUS` at
6//! a directory and every `.avi` under it is read twice, once by this crate and
7//! once by `ffprobe`, and the two answers are compared.
8//!
9//! Only the front of each file is given to the reader -- the same sniffing
10//! buffer a scanner would hold -- because answering from a head is the property
11//! that makes this usable during a walk.
12//!
13//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
14//! Anthropic Claude
15
16use oxedyne_fe2o3_core::prelude::*;
17use oxedyne_fe2o3_graphics::avi::Avi;
18
19use std::{
20 env,
21 fs::{self, File},
22 io::Read,
23 path::{Path, PathBuf},
24 process::Command,
25};
26
27const HEAD: usize = 64 * 1024; // the head a scanner holds, and all this reader is given
28
29// How far a running time may differ from the player's and still agree: a tenth
30// of a second, or one part in a hundred, whichever is larger. The container
31// states a frame count and a rate; a player may also consult the index and the
32// last frame's timestamp, so the two need not agree to the millisecond and a
33// disagreement that matters is far larger than this.
34const SLACK_MS: i64 = 100;
35
36fn films(dir: &Path, out: &mut Vec<PathBuf>) {
37 let entries = match fs::read_dir(dir) {
38 Ok(e) => e,
39 Err(_) => return,
40 };
41 let mut here = Vec::new();
42 for entry in entries.flatten() {
43 let path = entry.path();
44 if path.is_dir() {
45 films(&path, out);
46 } else if path.to_string_lossy().to_lowercase().ends_with(".avi") {
47 here.push(path);
48 }
49 }
50 here.sort();
51 out.extend(here);
52}
53
54fn head_of(path: &Path) -> Option<Vec<u8>> {
55 let mut f = match File::open(path) {
56 Ok(f) => f,
57 Err(_) => return None,
58 };
59 let mut buf = vec![0u8; HEAD];
60 let mut got = 0usize;
61 while got < HEAD {
62 match f.read(&mut buf[got..]) {
63 Ok(0) => break,
64 Ok(n) => got += n,
65 Err(_) => return None,
66 }
67 }
68 buf.truncate(got);
69 Some(buf)
70}
71
72/// What ffprobe says: width, height and duration in milliseconds.
73fn oracle(path: &Path) -> Option<(u32, u32, i64)> {
74 let out = Command::new("ffprobe")
75 .args([
76 "-v", "error",
77 "-select_streams", "v:0",
78 "-show_entries", "stream=width,height:format=duration",
79 "-of", "default=noprint_wrappers=1:nokey=1",
80 ])
81 .arg(path)
82 .output()
83 .ok()?;
84 if !out.status.success() {
85 return None;
86 }
87 let text = String::from_utf8_lossy(&out.stdout);
88 let mut lines = text.lines();
89 let w: u32 = lines.next()?.trim().parse().ok()?;
90 let h: u32 = lines.next()?.trim().parse().ok()?;
91 let secs: f64 = lines.next()?.trim().parse().ok()?;
92 Some((w, h, (secs * 1000.0).round() as i64))
93}
94
95#[test]
96fn the_header_agrees_with_ffprobe() -> Outcome<()> {
97 let dir = match env::var("AVI_CORPUS") {
98 Ok(d) if !d.is_empty() => PathBuf::from(d),
99 _ => {
100 println!("skipped: set AVI_CORPUS to a directory of films");
101 return Ok(());
102 },
103 };
104 let cap: usize = env::var("AVI_CORPUS_MAX").ok()
105 .and_then(|n| n.parse().ok())
106 .unwrap_or(600);
107
108 let mut all = Vec::new();
109 films(&dir, &mut all);
110 println!("{} AVI files under {}", all.len(), dir.display());
111
112 let (mut compared, mut agreed, mut refused, mut no_oracle) = (0usize, 0usize, 0usize, 0usize);
113 let mut worst_ms = 0i64;
114 let mut sizes_wrong = Vec::new();
115 let mut times_wrong = Vec::new();
116
117 for path in all.iter().take(cap) {
118 let head = match head_of(path) {
119 Some(h) => h,
120 None => continue,
121 };
122 let mine = match Avi::read(&head) {
123 Ok(a) => a,
124 Err(_) => { refused += 1; continue },
125 };
126 let (w, h, ms) = match oracle(path) {
127 Some(o) => o,
128 None => { no_oracle += 1; continue },
129 };
130 compared += 1;
131 let (mw, mh) = mine.size();
132 if (mw, mh) != (w, h) {
133 if sizes_wrong.len() < 5 {
134 sizes_wrong.push(fmt!("{}: {}x{} against {}x{}",
135 path.display(), mw, mh, w, h));
136 }
137 continue;
138 }
139 match mine.millis() {
140 Some(got) => {
141 let off = (got as i64 - ms).abs();
142 let slack = SLACK_MS.max(ms / 100);
143 if off > slack {
144 if times_wrong.len() < 5 {
145 times_wrong.push(fmt!("{}: {} ms against {} ms",
146 path.display(), got, ms));
147 }
148 continue;
149 }
150 worst_ms = worst_ms.max(off);
151 },
152 None => {
153 if times_wrong.len() < 5 {
154 times_wrong.push(fmt!("{}: no running time, player says {} ms",
155 path.display(), ms));
156 }
157 continue;
158 },
159 }
160 agreed += 1;
161 }
162
163 println!("{} compared, {} agreed on size and running time", compared, agreed);
164 println!("worst running-time difference: {} ms", worst_ms);
165 println!("{} refused by the reader, {} ffprobe would not read", refused, no_oracle);
166 for line in sizes_wrong.iter().chain(times_wrong.iter()) {
167 println!(" {}", line);
168 }
169
170 // Reporting is not the point here: a header reader that disagrees with a
171 // player about the size of the picture is wrong, and the catalogue would
172 // show the wrong shape for every one of these films.
173 if compared > 0 {
174 req!(agreed, compared,
175 "The header reader and ffprobe disagree about {} of {} films.",
176 compared - agreed, compared);
177 }
178 Ok(())
179}