mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-11 16:31:45 +00:00
craft-media-testkit: shared media test helpers and ffmpeg oracle discovery (#701)
* media-testkit: shared test helpers and ffmpeg oracle discovery * media-testkit: report fixture rename failure via skip; media: unlink relative craft-video path * media-testkit: failed fixture generator honours CRAFT_MEDIA_REQUIRE_ORACLES generate() returned None directly when the generator reported failure, so a required-oracle run could pass with a missing fixture. Route it through skip, like run_generator, and cover both policies with tests. --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
This commit is contained in:
@@ -157,3 +157,163 @@ pub fn generate_and_report<T>(name: &str, outputs: &[PathBuf], generate: impl Fn
|
||||
println!("FIXTURE {status} {name}");
|
||||
r
|
||||
}
|
||||
|
||||
/// Check if ffmpeg has a specific encoder compiled in.
|
||||
pub fn ffmpeg_has_encoder(ff: &Path, encoder: &str) -> bool {
|
||||
let Ok(output) = Command::new(ff).args(["-hide_banner", "-encoders"]).output() else {
|
||||
return false;
|
||||
};
|
||||
if !output.status.success() {
|
||||
return false;
|
||||
}
|
||||
parse_has_encoder(&String::from_utf8_lossy(&output.stdout), encoder)
|
||||
}
|
||||
|
||||
fn parse_has_encoder(stdout: &str, encoder: &str) -> bool {
|
||||
stdout.lines().any(|line| {
|
||||
let mut parts = line.split_whitespace();
|
||||
if let (Some(_flags), Some(name)) = (parts.next(), parts.next()) { name == encoder } else { false }
|
||||
})
|
||||
}
|
||||
|
||||
/// Report a skipped test that is not an oracle check (an unsupported platform or an optional
|
||||
/// encoder capability). Unlike [`skip`], this never fails under `CRAFT_MEDIA_REQUIRE_ORACLES=1`.
|
||||
pub fn skip_test(what: &str, reason: &str) {
|
||||
eprintln!("SKIPPED ({what}): {reason}");
|
||||
}
|
||||
|
||||
/// Find ffmpeg, or print a skip message and return None.
|
||||
pub fn ffmpeg_or_skip(reason: &str) -> Option<PathBuf> {
|
||||
tool("ffmpeg").or_else(|| skip(&format!("{reason}: ffmpeg not found (set {} or put ffmpeg on PATH)", env_var("ffmpeg"))))
|
||||
}
|
||||
|
||||
/// Log progress of long tests.
|
||||
pub fn set_progress(label: &str) {
|
||||
eprintln!("PROGRESS: {label}");
|
||||
}
|
||||
|
||||
/// Run closure with an execution deadline, terminating with a panic if the limit is exceeded.
|
||||
#[allow(clippy::panic)]
|
||||
pub fn with_deadline<F: FnOnce() + Send + 'static>(secs: u64, f: F) {
|
||||
let timeout_secs = std::env::var("CRAFT_TEST_TIMEOUT_SECS").ok().and_then(|s| s.parse().ok()).unwrap_or(secs);
|
||||
with_timeout(timeout_secs, f);
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
fn with_timeout<F: FnOnce() + Send + 'static>(timeout_secs: u64, f: F) {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let handle = std::thread::spawn(move || {
|
||||
f();
|
||||
let _ = tx.send(());
|
||||
});
|
||||
match rx.recv_timeout(std::time::Duration::from_secs(timeout_secs)) {
|
||||
Ok(()) => {
|
||||
if let Err(payload) = handle.join() {
|
||||
std::panic::resume_unwind(payload);
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
panic!("test exceeded execution deadline of {timeout_secs} seconds");
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
|
||||
if let Err(payload) = handle.join() {
|
||||
std::panic::resume_unwind(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return fixture directory for a crate/category, creating it if needed.
|
||||
pub fn fixtures_dir(subdir: &str) -> PathBuf {
|
||||
fixture_subdir(subdir)
|
||||
}
|
||||
|
||||
pub mod fixtures {
|
||||
use super::temp_path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Atomically publish a fixture by renaming `tmp` to `path`.
|
||||
pub fn publish(tmp: &Path, path: &Path) -> std::io::Result<()> {
|
||||
std::fs::rename(tmp, path)
|
||||
}
|
||||
|
||||
/// Generate fixture `out` using generator closure if it does not already exist. A generator
|
||||
/// that returns `false`, or whose output cannot be moved into place, skips (see
|
||||
/// [`skip`](super::skip)): a failure with `CRAFT_MEDIA_REQUIRE_ORACLES=1`, so a required
|
||||
/// oracle run cannot pass with a fixture missing.
|
||||
pub fn generate<F: FnOnce(&Path) -> bool>(out: &Path, f: F) -> Option<PathBuf> {
|
||||
generate_with(out, super::oracles_required(), f)
|
||||
}
|
||||
|
||||
/// [`generate`] with `required` explicit, so tests can set it without touching the process
|
||||
/// environment.
|
||||
pub fn generate_with<F: FnOnce(&Path) -> bool>(out: &Path, required: bool, f: F) -> Option<PathBuf> {
|
||||
if out.exists() {
|
||||
return Some(out.to_path_buf());
|
||||
}
|
||||
let tmp = temp_path(out);
|
||||
let reason = if f(&tmp) {
|
||||
match std::fs::rename(&tmp, out) {
|
||||
Ok(()) => return Some(out.to_path_buf()),
|
||||
Err(e) => format!("could not move {} into place: {e}", tmp.display()),
|
||||
}
|
||||
} else {
|
||||
format!("fixture generator failed for {}", out.display())
|
||||
};
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
super::skip_with(required, &reason)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn failed_generator_skips_when_optional() {
|
||||
let out = std::env::temp_dir().join(format!("craft-media-testkit-gen-opt-{}", std::process::id()));
|
||||
let _ = std::fs::remove_file(&out);
|
||||
assert!(fixtures::generate_with(&out, false, |_| false).is_none());
|
||||
assert!(!out.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "forbids skipping oracle tests")]
|
||||
fn failed_generator_fails_when_required() {
|
||||
let out = std::env::temp_dir().join(format!("craft-media-testkit-gen-req-{}", std::process::id()));
|
||||
let _ = std::fs::remove_file(&out);
|
||||
let _ = fixtures::generate_with(&out, true, |_| false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_has_encoder_exact_match() {
|
||||
let sample = "\
|
||||
Encoders:
|
||||
V..... = Video
|
||||
------
|
||||
V....D libvpx-vp9 libvpx VP9 (codec vp9)
|
||||
V....D libvpx libvpx VP8 (codec vp8)
|
||||
A....D flac FLAC (Free Lossless Audio Codec)
|
||||
";
|
||||
assert!(parse_has_encoder(sample, "libvpx"));
|
||||
assert!(parse_has_encoder(sample, "libvpx-vp9"));
|
||||
assert!(parse_has_encoder(sample, "flac"));
|
||||
assert!(!parse_has_encoder(sample, "vp8"));
|
||||
assert!(!parse_has_encoder(sample, "libvp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_deadline_completes() {
|
||||
with_deadline(5, || {
|
||||
// fast operation
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "test exceeded execution deadline")]
|
||||
fn with_deadline_times_out() {
|
||||
with_timeout(1, || {
|
||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ demuxer, written from the public specifications. No `unsafe`, no panics on malfo
|
||||
for `wasm32-unknown-unknown`. Dependencies: `thiserror` (for `isobmff`), `serde` (optional).
|
||||
|
||||
Codecs are in separate crates that depend on this one: [`craft-audio`](../audio/README.md) for
|
||||
audio; video codecs will go in a separate crate. What both need lives here: the containers, the
|
||||
audio; `craft-video` for video. What both need lives here: the containers, the
|
||||
bit I/O, one `ByteSource` trait, and the [shared identifiers](#containers-codecs-and-codec-parameters)
|
||||
that codec crates use to describe a file and configure a decoder.
|
||||
|
||||
@@ -39,8 +39,8 @@ craft-media = { git = "https://github.com/storytold/craft-libs", rev = "<commit>
|
||||
|
||||
- In scope: bit-level reading and writing, and container demuxing and muxing (boxes, sample
|
||||
tables, sample entries and codec configuration records) for audio and video tracks.
|
||||
- Out of scope: codecs. Audio codecs are in `craft-audio`; video codecs (H.264, HEVC, ProRes and
|
||||
others) stay in the apps until a video crate exists. Containers only FilmCraft reads (MXF,
|
||||
- Out of scope: codecs. Audio codecs are in `craft-audio`; video codecs (H.264, HEVC, ProRes,
|
||||
DNx, VP8, VP9, AV1) are in `craft-video`. Containers only FilmCraft reads (MXF,
|
||||
MPEG-TS/PS, AVI) are not here yet.
|
||||
- Anything both the audio and the video codec crates need belongs here, so the two never depend
|
||||
on each other.
|
||||
|
||||
Reference in New Issue
Block a user