T2: cargo xtask fixtures pre-generates the oracle fixture matrix

Runs each crate's ignored generate_fixtures test (h264, hevc, isobmff,
matroska, prores) - the same generators the oracle tests use - and
prints made/cached/skipped per fixture plus a summary. Optional crate
filter: cargo xtask fixtures hevc prores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-01 00:20:59 -04:00
1 parent 8780d5ffa2
commit 3704002f89
11 files changed
+160 -1

No files matched your search

Generated
+1
View File
@@ -5626,6 +5626,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
name = "xtask"
version = "0.1.0"
dependencies = [
"filmcraft-testkit",
"serde_json",
]
+11
View File
@@ -95,3 +95,14 @@ fn coverage_report() {
);
}
}
/// Pre-generate every fixture and its reference decode (`cargo xtask fixtures`).
#[test]
#[ignore]
fn generate_fixtures() {
let dir = common::fixtures_dir();
for f in common::FIXTURES {
let outs = [dir.join(format!("{}.h264", f.name)), dir.join(format!("{}.yuv", f.name))];
filmcraft_testkit::fixtures::generate_and_report(&format!("h264/{}", f.name), &outs, || common::ensure(f));
}
}
+11
View File
@@ -91,3 +91,14 @@ fn compare_detects_mismatch() {
let err = common::compare(&pics, &reference, f.width as usize, f.height as usize, f.bit_depth).unwrap_err();
assert!(err.contains("plane V"), "{err}");
}
/// Pre-generate every fixture and its reference decode (`cargo xtask fixtures`).
#[test]
#[ignore]
fn generate_fixtures() {
let dir = common::fixtures_dir();
for f in common::FIXTURES {
let outs = [dir.join(format!("{}.hevc", f.name)), dir.join(format!("{}.yuv", f.name))];
filmcraft_testkit::fixtures::generate_and_report(&format!("hevc/{}", f.name), &outs, || common::ensure(f));
}
}
+10
View File
@@ -271,3 +271,13 @@ fn specific_properties() {
let v = &f.tracks[f.track_of_kind(TrackKind::Video).unwrap()];
assert!(v.references.iter().any(|(k, ids)| k.to_string() == "tmcd" && ids == &vec![t.id]));
}
/// Pre-generate every fixture (`cargo xtask fixtures`).
#[test]
#[ignore]
fn generate_fixtures() {
for name in all_fixture_names() {
let out = [fixture_dir().join(name)];
filmcraft_testkit::fixtures::generate_and_report(&format!("isobmff/{name}"), &out, || fixture(name));
}
}
+16
View File
@@ -25,6 +25,22 @@ fn write_subs(dir: &Path) {
std::fs::write(dir.join("in.vtt"), vtt).unwrap();
}
/// Every fixture `spec`/`fixture` knows (for `cargo xtask fixtures`).
pub const ALL_FIXTURES: &[&str] = &[
"h264_aac.mkv",
"vp9_opus.webm",
"flac.mkv",
"vorbis.mkv",
"subs.mkv",
"webvtt.webm",
"cues_front.mkv",
"hevc_hdr.mkv",
"prores_pcm.mkv",
"mjpeg_ac3.mkv",
"av1.mkv",
"live.mkv",
];
/// Fixture name → ffmpeg arguments (after `-y -v error`, before the output path).
/// `None` for fixtures written through a pipe (no Cues, unknown sizes).
pub fn spec(name: &str) -> Option<Vec<String>> {
+11
View File
@@ -360,3 +360,14 @@ fn not_matroska() {
assert!(matches!(open(&ebml[..]), Err(Error::NotMatroska(_))));
let _ = Value::Null;
}
/// Pre-generate every fixture (`cargo xtask fixtures`).
#[test]
#[ignore]
fn generate_fixtures() {
let Some((ffmpeg, _)) = tools() else { return };
for name in ALL_FIXTURES {
let out = [fixture_dir().join(name)];
filmcraft_testkit::fixtures::generate_and_report(&format!("matroska/{name}"), &out, || fixture(&ffmpeg, name));
}
}
+11
View File
@@ -140,3 +140,14 @@ fn color_metadata_is_reported() {
let f = decode_frame(&packets(&ff, &mov)[0]).unwrap();
assert_eq!((f.color.primaries, f.color.transfer, f.color.matrix), (9, 16, 9));
}
/// Pre-generate every decode fixture (`cargo xtask fixtures`).
#[test]
#[ignore]
fn generate_fixtures() {
let Some(ff) = ffmpeg() else { return };
for spec in specs() {
let out = [fixture_dir().join(format!("{}.mov", spec.name))];
filmcraft_testkit::fixtures::generate_and_report(&format!("prores/{}", spec.name), &out, || Some(make(&ff, &spec)));
}
}
+34
View File
@@ -65,6 +65,40 @@ pub fn generate(out: &Path, make: impl FnOnce(&Path) -> bool) -> Option<PathBuf>
Some(out.to_path_buf())
}
/// Status of one fixture for `cargo xtask fixtures`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Status {
Made,
Cached,
Skipped,
}
/// Print a `FIXTURE <made|cached|skipped> <name>` line (parsed by `cargo xtask fixtures`).
pub fn report(name: &str, status: Status) {
let s = match status {
Status::Made => "made",
Status::Cached => "cached",
Status::Skipped => "skipped",
};
println!("FIXTURE {s} {name}");
}
/// Run a fixture generator and [`report`] it: `cached` when every path in `outputs` existed
/// before, `made` when `generate` succeeded, `skipped` otherwise.
pub fn generate_and_report<T>(name: &str, outputs: &[PathBuf], generate: impl FnOnce() -> Option<T>) -> Option<T> {
let existed = !outputs.is_empty() && outputs.iter().all(|p| std::fs::metadata(p).is_ok_and(|m| m.len() > 0));
let r = generate();
report(
name,
match (&r, existed) {
(None, _) => Status::Skipped,
(Some(_), true) => Status::Cached,
(Some(_), false) => Status::Made,
},
);
r
}
#[cfg(test)]
mod tests {
use super::*;
+8
View File
@@ -5,6 +5,8 @@ cargo test --workspace # everything; oracle tests skip
cargo test -p filmcraft-h264 # one crate
cargo test --release -p filmcraft-hevc # codec tests are much faster in release
cargo xtask ci # all gates (fmt, clippy, tests, layers, assets, wasm)
cargo xtask fixtures # pre-generate the ffmpeg fixture matrix
FILMCRAFT_REQUIRE_ORACLES=1 cargo test --workspace # CI: missing ffmpeg fails instead of skipping
```
## 1. Kinds of tests
@@ -43,6 +45,12 @@ are never linked or shipped ([AGENTS.md](../AGENTS.md) §2).
- Every crate's `tests/common/mod.rs` delegates discovery and fixture paths to testkit, except
`crates/codecs/src/tests.rs`, which still has its own lookup (the crate was being edited
concurrently; migrate it when convenient).
- `cargo xtask fixtures [crate…]` pre-generates the whole fixture matrix up front (useful before a
parallel test run or on a fresh machine). It runs each crate's ignored `generate_fixtures` test —
the same generators the oracle tests call — for `h264`, `hevc`, `isobmff`, `matroska` and
`prores`, and prints one `made` / `cached` / `skipped` line per fixture plus a summary. The
`aac`, `opus` and `h264enc` oracles generate small per-test signals on demand and are not part of
the matrix.
- Fixture sources are synthetic: `testsrc2`, `mandelbrot`, SMPTE bars, noise and fades, sine tones.
H.264 and HEVC fixtures need ffmpeg built with libx264 and libx265. VideoToolbox fixtures are
generated only on macOS.
+1
View File
@@ -7,3 +7,4 @@ publish = false
[dependencies]
serde_json = { workspace = true }
filmcraft-testkit = { workspace = true }
+46 -1
View File
@@ -5,6 +5,9 @@
//! - `wasm`: `cargo check --target wasm32-unknown-unknown` for every crate in L0–L4.
//! - `assets`: every asset file (image, icon, font, LUT, audio, video…) has a complete
//! `<file>.attribution` sidecar and an entry in `ATTRIBUTION.md` (AGENTS.md §1).
//! - `fixtures [crate…]`: pre-generate the ffmpeg fixture matrix of the oracle tests (runs each
//! crate's ignored `generate_fixtures` test, i.e. the same generators the tests use) and print
//! what was made, reused or skipped.
//! - `ci`: fmt check, clippy -D warnings, tests, layers, assets, wasm.
use std::process::{Command, ExitCode};
@@ -237,6 +240,47 @@ fn wasm() -> Result<(), String> {
Ok(())
}
/// (crate, test target) whose ignored `generate_fixtures` test builds that crate's fixture matrix.
const FIXTURE_GENERATORS: &[(&str, &str)] =
&[("h264", "conformance"), ("hevc", "conformance"), ("isobmff", "oracle_demux"), ("matroska", "oracle"), ("prores", "oracle_decode")];
fn fixtures(only: &[String]) -> Result<(), String> {
let unknown: Vec<&String> = only.iter().filter(|o| !FIXTURE_GENERATORS.iter().any(|(c, _)| *c == short(o))).collect();
if !unknown.is_empty() {
let known: Vec<&str> = FIXTURE_GENERATORS.iter().map(|(c, _)| *c).collect();
return Err(format!("no fixture generator for {unknown:?} (known: {})", known.join(", ")));
}
let Some(ff) = filmcraft_testkit::ffmpeg() else {
return Err("ffmpeg not found: set FILMCRAFT_FFMPEG=/path/to/ffmpeg or put ffmpeg on PATH".into());
};
println!("ffmpeg: {}", ff.display());
println!("fixtures: {}", filmcraft_testkit::fixtures::fixtures_root().display());
let (mut made, mut cached, mut skipped) = (0, 0, 0);
let mut failed = Vec::new();
for (krate, target) in FIXTURE_GENERATORS.iter().filter(|(c, _)| only.is_empty() || only.iter().any(|o| short(o) == *c)) {
let pkg = format!("filmcraft-{krate}");
println!("\n== {pkg} ({target})");
let mut cmd = Command::new(env!("CARGO"));
cmd.args(["test", "--release", "-p", &pkg, "--test", target, "--", "--ignored", "--exact", "generate_fixtures", "--nocapture"]);
cmd.stderr(std::process::Stdio::inherit());
let out = cmd.output().map_err(|e| e.to_string())?;
for line in String::from_utf8_lossy(&out.stdout).lines() {
let Some(rest) = line.strip_prefix("FIXTURE ") else { continue };
match rest.split_whitespace().next() {
Some("made") => made += 1,
Some("cached") => cached += 1,
_ => skipped += 1,
}
println!(" {rest}");
}
if !out.status.success() {
failed.push(pkg);
}
}
println!("\nfixtures: {made} made, {cached} already present, {skipped} skipped");
if failed.is_empty() { Ok(()) } else { Err(format!("fixture generation failed in {}", failed.join(", "))) }
}
fn ci() -> Result<(), String> {
let cargo = env!("CARGO");
run(Command::new(cargo).args(["fmt", "--check"]))?;
@@ -253,8 +297,9 @@ fn main() -> ExitCode {
"layers" => layers(),
"wasm" => wasm(),
"assets" => assets(),
"fixtures" => fixtures(&std::env::args().skip(2).collect::<Vec<_>>()),
"ci" => ci(),
_ => Err("usage: cargo xtask <layers|assets|wasm|ci>".into()),
_ => Err("usage: cargo xtask <layers|assets|wasm|fixtures [crate…]|ci>".into()),
};
match r {
Ok(()) => ExitCode::SUCCESS,