mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-30 13:07:19 -06:00
188 lines
8.6 KiB
Rust
188 lines
8.6 KiB
Rust
//! End-to-end tests of the CLI binary (headless backend).
|
|
|
|
use std::io::Write;
|
|
use std::process::{Command, Output, Stdio};
|
|
|
|
use serde_json::Value;
|
|
|
|
fn cli(args: &[&str]) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_filmcraft-cli")).args(args).output().expect("spawn filmcraft-cli")
|
|
}
|
|
|
|
fn json_out(o: &Output) -> Value {
|
|
assert!(o.status.success(), "failed: {}", String::from_utf8_lossy(&o.stderr));
|
|
serde_json::from_slice(&o.stdout).expect("JSON on stdout")
|
|
}
|
|
|
|
#[test]
|
|
fn help_and_usage_errors() {
|
|
let h = cli(&["help"]);
|
|
assert!(h.status.success());
|
|
assert!(String::from_utf8_lossy(&h.stdout).contains("exec <id>"));
|
|
assert_eq!(cli(&[]).status.code(), Some(2));
|
|
assert_eq!(cli(&["frobnicate"]).status.code(), Some(2));
|
|
assert_eq!(cli(&["exec", "file.newBin", "notkv"]).status.code(), Some(2));
|
|
}
|
|
|
|
#[test]
|
|
fn probe_reports_mpeg_transport_and_program_streams() {
|
|
let Some(ffmpeg) = ["/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg"].into_iter().find(|p| std::path::Path::new(p).exists()) else {
|
|
eprintln!("SKIPPED (probe_reports_mpeg_transport_and_program_streams): ffmpeg not found");
|
|
return;
|
|
};
|
|
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/cli-tests").join(format!("mpeg-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
for (name, fmt, extra) in [("a.ts", "mpegts", &["-mpegts_m2ts_mode", "0"][..]), ("b.vob", "vob", &[][..])] {
|
|
let out = dir.join(name);
|
|
let st = Command::new(ffmpeg)
|
|
.args(["-v", "error", "-y", "-f", "lavfi", "-i", "testsrc2=size=320x240:rate=25", "-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000"])
|
|
.args(["-t", "0.5", "-c:v", "mpeg2video", "-c:a", "mp2", "-ac", "2", "-f", fmt])
|
|
.args(extra)
|
|
.arg(&out)
|
|
.status()
|
|
.unwrap();
|
|
assert!(st.success());
|
|
let v = json_out(&cli(&["probe", out.to_str().unwrap()]));
|
|
assert!(v["video"]["codec"].as_str().unwrap().starts_with("MPEG-2 Video"), "{v}");
|
|
assert_eq!(v["audio"]["codec"], "MPEG Audio");
|
|
let streams = v["mpeg"]["streams"].as_array().unwrap();
|
|
assert_eq!(streams.len(), 2, "{v}");
|
|
assert_eq!(streams[0]["codec"], "MPEG-2 Video");
|
|
assert!(streams[0]["pictures"]["I"].as_u64().unwrap() >= 1);
|
|
assert_eq!(v["mpeg"]["format"], if fmt == "mpegts" { "MPEG-2 TS" } else { "MPEG-2 PS" });
|
|
}
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn exec_inspect_and_describe() {
|
|
let seq = json_out(&cli(&["--demo", "inspect", "sequence", "--compact"]));
|
|
assert!(seq["tracks"].is_array() || seq.is_object(), "{seq}");
|
|
let d = json_out(&cli(&["describe", "timeline.razor"]));
|
|
assert_eq!(d["id"], "timeline.razor");
|
|
assert!(d["params"].is_string());
|
|
let list = json_out(&cli(&["commands", "razor", "--json"]));
|
|
assert!(list.as_array().unwrap().iter().any(|c| c["id"] == "timeline.razor"));
|
|
// An unknown command fails with exit status 1.
|
|
let bad = cli(&["--demo", "exec", "no.such.command"]);
|
|
assert_eq!(bad.status.code(), Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn exec_save_as_then_reopen() {
|
|
let dir = std::env::temp_dir().join(format!("fc-cli-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let proj = dir.join("p.fcproj");
|
|
let p = proj.to_str().unwrap();
|
|
json_out(&cli(&["--demo", "exec", "file.newBin", "name=CLI Selects", "--save-as", p]));
|
|
let tree = json_out(&cli(&["--project", p, "inspect", "project"]));
|
|
assert!(tree.to_string().contains("CLI Selects"), "{tree}");
|
|
// `run` from stdin, editing and saving back in place.
|
|
let mut child = Command::new(env!("CARGO_BIN_EXE_filmcraft-cli"))
|
|
.args(["--project", p, "--save", "run", "-"])
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
child.stdin.take().unwrap().write_all(b"# comment\n{\"id\":\"file.newBin\",\"params\":{\"name\":\"Second\"}}\n").unwrap();
|
|
let out = child.wait_with_output().unwrap();
|
|
assert!(out.status.success());
|
|
let line: Value = serde_json::from_slice(out.stdout.split(|b| *b == b'\n').next().unwrap()).unwrap();
|
|
assert_eq!(line["ok"], true);
|
|
assert!(json_out(&cli(&["--project", p, "inspect", "project"])).to_string().contains("Second"));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn run_keep_going_reports_failures() {
|
|
let mut child = Command::new(env!("CARGO_BIN_EXE_filmcraft-cli"))
|
|
.args(["--demo", "--keep-going", "run", "-"])
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
child.stdin.take().unwrap().write_all(b"{\"id\":\"no.such\"}\n{\"id\":\"file.newBin\",\"params\":{\"name\":\"x\"}}\n").unwrap();
|
|
let out = child.wait_with_output().unwrap();
|
|
assert_eq!(out.status.code(), Some(1));
|
|
let lines: Vec<Value> = String::from_utf8_lossy(&out.stdout).lines().map(|l| serde_json::from_str(l).unwrap()).collect();
|
|
assert_eq!(lines.len(), 2);
|
|
assert_eq!(lines[0]["ok"], false);
|
|
assert_eq!(lines[1]["ok"], true);
|
|
}
|
|
|
|
#[test]
|
|
fn export_wav_waits_for_job() {
|
|
let dir = std::env::temp_dir().join(format!("fc-cli-x-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let out = dir.join("still.wav");
|
|
let o = cli(&["--demo", "export", out.to_str().unwrap()]);
|
|
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
|
|
assert!(std::fs::metadata(&out).map(|m| m.len() > 44).unwrap_or(false));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
/// `export --preset`: a built-in preset by name (dash or en dash), a custom range, a user preset
|
|
/// from `--data-dir`, `--list-presets`, `--queue`, and a clean failure for an unknown preset.
|
|
#[test]
|
|
fn export_with_presets() {
|
|
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/export-tests").join(format!("cli-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let data = dir.join("data");
|
|
let data_s = data.to_str().unwrap();
|
|
let out = dir.join("proxy.mov");
|
|
let o = cli(&["--demo", "--data-dir", data_s, "export", out.to_str().unwrap(), "--preset", "Apple ProRes 422 Proxy", "--start", "0", "--end", "0.25"]);
|
|
let v = json_out(&o);
|
|
assert_eq!(v["preset"], "Apple ProRes 422 Proxy");
|
|
assert!(std::fs::metadata(&out).map(|m| m.len() > 1000).unwrap_or(false));
|
|
if let Some(ffprobe) = ["/opt/homebrew/bin/ffprobe", "/usr/local/bin/ffprobe", "/usr/bin/ffprobe"].into_iter().find(|p| std::path::Path::new(p).exists()) {
|
|
let p = Command::new(ffprobe)
|
|
.args(["-v", "error", "-select_streams", "v:0", "-show_entries", "stream=codec_name,profile,width", "-of", "csv=p=0", out.to_str().unwrap()])
|
|
.output()
|
|
.unwrap();
|
|
assert_eq!(String::from_utf8_lossy(&p.stdout).trim(), "prores,Proxy,1920");
|
|
}
|
|
// the extension comes from the preset; a hyphen finds the en-dash name
|
|
let base = dir.join("adaptive");
|
|
json_out(&cli(&[
|
|
"--demo",
|
|
"--data-dir",
|
|
data_s,
|
|
"export",
|
|
base.to_str().unwrap(),
|
|
"--preset",
|
|
"Match Source - Adaptive Low Bitrate",
|
|
"--start",
|
|
"0",
|
|
"--end",
|
|
"0.2",
|
|
]));
|
|
assert!(dir.join("adaptive.mp4").exists());
|
|
// a user preset saved into the data directory is found by later runs
|
|
json_out(&cli(&[
|
|
"--data-dir",
|
|
data_s,
|
|
"exec",
|
|
"export.presets.save",
|
|
"name=Tiny WAV",
|
|
"from=Waveform Audio 48 kHz 16-bit",
|
|
"settings={\"audio\":{\"channels\":1}}",
|
|
]));
|
|
let list = json_out(&cli(&["--data-dir", data_s, "export", "--list-presets", "tiny"]));
|
|
assert_eq!(list.as_array().unwrap().len(), 1, "{list}");
|
|
let wav = dir.join("tiny.wav");
|
|
json_out(&cli(&["--demo", "--data-dir", data_s, "export", wav.to_str().unwrap(), "--preset", "Tiny WAV", "--start", "0", "--end", "0.5"]));
|
|
let b = std::fs::read(&wav).unwrap();
|
|
assert_eq!(u16::from_le_bytes([b[22], b[23]]), 1, "mono from the user preset");
|
|
// through the queue
|
|
let q = dir.join("queued.wav");
|
|
let v = json_out(&cli(&["--demo", "--data-dir", data_s, "export", q.to_str().unwrap(), "--preset", "Tiny WAV", "--queue", "--range", "entire"]));
|
|
assert_eq!(v["items"][0]["status"], "done", "{v}");
|
|
assert!(q.exists());
|
|
// unknown preset: exit status 1 and a message naming it
|
|
let bad = cli(&["--demo", "--data-dir", data_s, "export", dir.join("x.mp4").to_str().unwrap(), "--preset", "No Such Preset"]);
|
|
assert_eq!(bad.status.code(), Some(1));
|
|
assert!(String::from_utf8_lossy(&bad.stderr).contains("No Such Preset"));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|