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:
1 parent
8780d5ffa2
commit
3704002f89
11 files changed
+160
-1
No files matched your search
Generated
+1
@@ -5626,6 +5626,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"filmcraft-testkit",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -7,3 +7,4 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
serde_json = { workspace = true }
|
||||
filmcraft-testkit = { workspace = true }
|
||||
+46
-1
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user