mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-23 03:07:13 +00:00
avplan: whether a recording plays, and the one call that fixes it
This commit is contained in:
@@ -22,6 +22,7 @@ cuepack.workspace = true
|
||||
cueseg.workspace = true
|
||||
mdscrub.workspace = true
|
||||
mdsplit.workspace = true
|
||||
avplan.workspace = true
|
||||
pdftriage.workspace = true
|
||||
rrf.workspace = true
|
||||
corpus-core.workspace = true
|
||||
|
||||
@@ -156,3 +156,48 @@ exactly.
|
||||
|
||||
**Measured:** checked against the oracle for all of those combinations; only
|
||||
the zero hard maximum differs.
|
||||
|
||||
## avplan — the encoder is found rather than configured
|
||||
|
||||
**Original:** reads `COMPATIBLE_VIDEO_ENCODER` and trusts it, defaulting to
|
||||
`h264_nvenc`. A machine with no NVIDIA card and an unset variable builds an
|
||||
NVENC command line that fails the moment it runs.
|
||||
|
||||
**Rust:** asks `ffmpeg -encoders` what is installed and takes the best of
|
||||
them, hardware first.
|
||||
|
||||
**Why this is acceptable:** an environment variable naming the encoder is
|
||||
per-host configuration, and a filter that reads configuration is a filter
|
||||
whose answer depends on where it ran. Detection reaches the same answer on
|
||||
every machine the variable was set correctly on, and a better one on the
|
||||
machines it was not. The encoder table itself — which arguments each encoder
|
||||
gets — is identical, and `--encoder` names one explicitly for anyone who
|
||||
needs to.
|
||||
|
||||
**Measured:** all four branches of the encoder table are compared against the
|
||||
original in `cases/avplan`, each with the encoder named, so the argument
|
||||
lists agree exactly.
|
||||
|
||||
## avplan — the output path is the destination, not a temporary
|
||||
|
||||
**Original:** writes to `<output>.tmp.mp4` and renames on success, so a
|
||||
transcode killed halfway leaves no file that looks finished.
|
||||
|
||||
**Rust:** the command line names the destination.
|
||||
|
||||
**Why this is acceptable:** avplan does not run the command, so it cannot do
|
||||
the rename half of the pattern, and a command line that writes to a temporary
|
||||
nobody moves is worse than one that writes where it says. Anything running
|
||||
these in a loop can write to a temporary of its own choosing with `--output`.
|
||||
|
||||
## avplan — a failed probe is an answer, not a fallback
|
||||
|
||||
**Original:** when `ffprobe` cannot read a file it falls back to the codec
|
||||
recorded in the database, which came from the downloader's pre-download
|
||||
metadata and is wrong often enough that the comment above it says so.
|
||||
|
||||
**Rust:** exits 2, could not tell.
|
||||
|
||||
**Why this is acceptable:** there is no database, and inventing a codec is
|
||||
how a compatible file gets re-encoded for forty minutes. Saying so is the
|
||||
honest answer and the cheap one.
|
||||
|
||||
+4
-3
@@ -22,6 +22,7 @@ CATENA_PARITY_PYTHONPATH=/path/to/book-pipeline/src:/path/to/podcast-pipeline/sr
|
||||
CATENA_PARITY_NODE=node # only if it is not already on PATH
|
||||
```
|
||||
|
||||
One oracle is JavaScript rather than Python, because the code it compares
|
||||
against is. Its three functions are copied into `oracle/cuenorm.mjs` verbatim,
|
||||
since the module they live in opens a database connection when it loads.
|
||||
Two oracles are JavaScript rather than Python, because the code they compare
|
||||
against is. Their functions are copied into `oracle/cuenorm.mjs` and
|
||||
`oracle/compatible.mjs` verbatim, since the modules they live in open a
|
||||
database connection when they load.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{"name":"already-playable-mp4","path":"/media/talk.mp4","video":"h264","audio":"aac","encoder":"h264_nvenc"}
|
||||
{"name":"already-playable-mov","path":"/media/talk.mov","video":"avc1.640028","audio":"mp4a.40.2","encoder":"h264_nvenc"}
|
||||
{"name":"already-playable-m4v","path":"/media/talk.m4v","video":"hevc","audio":"aac","encoder":"h264_nvenc"}
|
||||
{"name":"uppercase-extension","path":"/media/TALK.MP4","video":"h264","audio":"aac","encoder":"h264_nvenc"}
|
||||
{"name":"vp9-opus-webm-nvenc","path":"/media/lecture.webm","video":"vp9","audio":"opus","encoder":"h264_nvenc"}
|
||||
{"name":"vp9-opus-webm-videotoolbox","path":"/media/lecture.webm","video":"vp9","audio":"opus","encoder":"h264_videotoolbox"}
|
||||
{"name":"vp9-opus-webm-vaapi","path":"/media/lecture.webm","video":"vp9","audio":"opus","encoder":"h264_vaapi"}
|
||||
{"name":"vp9-opus-webm-libx264","path":"/media/lecture.webm","video":"vp9","audio":"opus","encoder":"libx264"}
|
||||
{"name":"vaapi-other-device","path":"/media/lecture.webm","video":"av1","audio":"opus","encoder":"h264_vaapi","vaapi_device":"/dev/dri/renderD129"}
|
||||
{"name":"h264-in-matroska","path":"/media/film.mkv","video":"h264","audio":"aac","encoder":"h264_nvenc"}
|
||||
{"name":"h264-opus-in-mp4","path":"/media/interview.mp4","video":"h264","audio":"opus","encoder":"libx264"}
|
||||
{"name":"av1-aac-in-mp4","path":"/media/short.mp4","video":"av1","audio":"aac","encoder":"h264_nvenc"}
|
||||
{"name":"no-audio-stream","path":"/media/silent.webm","video":"vp9","audio":"","encoder":"libx264"}
|
||||
{"name":"no-video-stream","path":"/media/podcast.m4a","video":"","audio":"aac","encoder":"libx264"}
|
||||
{"name":"neither-stream","path":"/media/broken.bin","video":"","audio":"","encoder":"libx264"}
|
||||
{"name":"no-extension","path":"/media/download","video":"h264","audio":"aac","encoder":"libx264"}
|
||||
{"name":"two-dots-in-the-name","path":"/media/s01.e04.webm","video":"vp8","audio":"vorbis","encoder":"libx264"}
|
||||
{"name":"relative-path","path":"talk.webm","video":"vp9","audio":"opus","encoder":"libx264"}
|
||||
{"name":"space-in-the-name","path":"/media/a talk.webm","video":"vp9","audio":"opus","encoder":"libx264"}
|
||||
{"name":"named-output","path":"/media/lecture.webm","video":"vp9","audio":"opus","encoder":"libx264","output":"/elsewhere/out.mp4"}
|
||||
{"name":"codec-merely-starting-with-aac","path":"/media/x.mp4","video":"h264","audio":"aac_latm","encoder":"libx264"}
|
||||
{"name":"codec-merely-containing-h264","path":"/media/x.mp4","video":"prores_h264ish","audio":"aac","encoder":"libx264"}
|
||||
{"name":"mpeg4-in-avi","path":"/media/old.avi","video":"mpeg4","audio":"mp3","encoder":"h264_nvenc"}
|
||||
{"name":"hevc-in-mkv-aac","path":"/media/film.mkv","video":"hev1.1.6.L93","audio":"aac","encoder":"h264_nvenc"}
|
||||
@@ -0,0 +1,24 @@
|
||||
{"path":"/media/talk.mp4","playable":true,"encoder":null,"copies_audio":true,"argv":[]}
|
||||
{"path":"/media/talk.mov","playable":true,"encoder":null,"copies_audio":true,"argv":[]}
|
||||
{"path":"/media/talk.m4v","playable":true,"encoder":null,"copies_audio":true,"argv":[]}
|
||||
{"path":"/media/TALK.MP4","playable":true,"encoder":null,"copies_audio":true,"argv":[]}
|
||||
{"path":"/media/lecture.webm","playable":false,"encoder":"h264_nvenc","copies_audio":false,"argv":["-nostdin","-y","-i","/media/lecture.webm","-c:v","h264_nvenc","-preset","p5","-rc","vbr","-cq","23","-b:v","0","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/media/lecture.h264.mp4"]}
|
||||
{"path":"/media/lecture.webm","playable":false,"encoder":"h264_videotoolbox","copies_audio":false,"argv":["-nostdin","-y","-i","/media/lecture.webm","-c:v","h264_videotoolbox","-q:v","60","-profile:v","high","-pix_fmt","yuv420p","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/media/lecture.h264.mp4"]}
|
||||
{"path":"/media/lecture.webm","playable":false,"encoder":"h264_vaapi","copies_audio":false,"argv":["-nostdin","-y","-init_hw_device","vaapi=va:/dev/dri/renderD128","-filter_hw_device","va","-i","/media/lecture.webm","-vf","format=nv12,hwupload","-c:v","h264_vaapi","-qp","23","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/media/lecture.h264.mp4"]}
|
||||
{"path":"/media/lecture.webm","playable":false,"encoder":"libx264","copies_audio":false,"argv":["-nostdin","-y","-i","/media/lecture.webm","-c:v","libx264","-preset","veryfast","-crf","23","-pix_fmt","yuv420p","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/media/lecture.h264.mp4"]}
|
||||
{"path":"/media/lecture.webm","playable":false,"encoder":"h264_vaapi","copies_audio":false,"argv":["-nostdin","-y","-init_hw_device","vaapi=va:/dev/dri/renderD129","-filter_hw_device","va","-i","/media/lecture.webm","-vf","format=nv12,hwupload","-c:v","h264_vaapi","-qp","23","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/media/lecture.h264.mp4"]}
|
||||
{"path":"/media/film.mkv","playable":false,"encoder":"copy","copies_audio":true,"argv":["-nostdin","-y","-i","/media/film.mkv","-c:v","copy","-c:a","copy","-movflags","+faststart","/media/film.h264.mp4"]}
|
||||
{"path":"/media/interview.mp4","playable":false,"encoder":"copy","copies_audio":false,"argv":["-nostdin","-y","-i","/media/interview.mp4","-c:v","copy","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/media/interview.h264.mp4"]}
|
||||
{"path":"/media/short.mp4","playable":false,"encoder":"h264_nvenc","copies_audio":true,"argv":["-nostdin","-y","-i","/media/short.mp4","-c:v","h264_nvenc","-preset","p5","-rc","vbr","-cq","23","-b:v","0","-c:a","copy","-movflags","+faststart","/media/short.h264.mp4"]}
|
||||
{"path":"/media/silent.webm","playable":false,"encoder":"libx264","copies_audio":false,"argv":["-nostdin","-y","-i","/media/silent.webm","-c:v","libx264","-preset","veryfast","-crf","23","-pix_fmt","yuv420p","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/media/silent.h264.mp4"]}
|
||||
{"path":"/media/podcast.m4a","playable":false,"encoder":"libx264","copies_audio":true,"argv":["-nostdin","-y","-i","/media/podcast.m4a","-c:v","libx264","-preset","veryfast","-crf","23","-pix_fmt","yuv420p","-c:a","copy","-movflags","+faststart","/media/podcast.h264.mp4"]}
|
||||
{"path":"/media/broken.bin","playable":false,"encoder":"libx264","copies_audio":false,"argv":["-nostdin","-y","-i","/media/broken.bin","-c:v","libx264","-preset","veryfast","-crf","23","-pix_fmt","yuv420p","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/media/broken.h264.mp4"]}
|
||||
{"path":"/media/download","playable":false,"encoder":"copy","copies_audio":true,"argv":["-nostdin","-y","-i","/media/download","-c:v","copy","-c:a","copy","-movflags","+faststart","/media/download.h264.mp4"]}
|
||||
{"path":"/media/s01.e04.webm","playable":false,"encoder":"libx264","copies_audio":false,"argv":["-nostdin","-y","-i","/media/s01.e04.webm","-c:v","libx264","-preset","veryfast","-crf","23","-pix_fmt","yuv420p","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/media/s01.e04.h264.mp4"]}
|
||||
{"path":"talk.webm","playable":false,"encoder":"libx264","copies_audio":false,"argv":["-nostdin","-y","-i","talk.webm","-c:v","libx264","-preset","veryfast","-crf","23","-pix_fmt","yuv420p","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","talk.h264.mp4"]}
|
||||
{"path":"/media/a talk.webm","playable":false,"encoder":"libx264","copies_audio":false,"argv":["-nostdin","-y","-i","/media/a talk.webm","-c:v","libx264","-preset","veryfast","-crf","23","-pix_fmt","yuv420p","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/media/a talk.h264.mp4"]}
|
||||
{"path":"/media/lecture.webm","playable":false,"encoder":"libx264","copies_audio":false,"argv":["-nostdin","-y","-i","/media/lecture.webm","-c:v","libx264","-preset","veryfast","-crf","23","-pix_fmt","yuv420p","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/elsewhere/out.mp4"]}
|
||||
{"path":"/media/x.mp4","playable":true,"encoder":null,"copies_audio":true,"argv":[]}
|
||||
{"path":"/media/x.mp4","playable":false,"encoder":"libx264","copies_audio":true,"argv":["-nostdin","-y","-i","/media/x.mp4","-c:v","libx264","-preset","veryfast","-crf","23","-pix_fmt","yuv420p","-c:a","copy","-movflags","+faststart","/media/x.h264.mp4"]}
|
||||
{"path":"/media/old.avi","playable":false,"encoder":"h264_nvenc","copies_audio":false,"argv":["-nostdin","-y","-i","/media/old.avi","-c:v","h264_nvenc","-preset","p5","-rc","vbr","-cq","23","-b:v","0","-c:a","aac","-b:a","160k","-ac","2","-movflags","+faststart","/media/old.h264.mp4"]}
|
||||
{"path":"/media/film.mkv","playable":false,"encoder":"copy","copies_audio":true,"argv":["-nostdin","-y","-i","/media/film.mkv","-c:v","copy","-c:a","copy","-movflags","+faststart","/media/film.h264.mp4"]}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Decide playability and build the ffmpeg call the way the media server did,
|
||||
* for comparison.
|
||||
*
|
||||
* The functions below are the originals, copied out of the transcode service
|
||||
* verbatim and with their type annotations dropped — the module they live in
|
||||
* opens a database connection when it loads, which an oracle must not do. The
|
||||
* surrounding job bookkeeping (Prisma writes, progress parsing, the temporary
|
||||
* file it renames from) is not copied, because none of it decides anything.
|
||||
*
|
||||
* node oracle/compatible.mjs < cases.ndjson
|
||||
*
|
||||
* One case per line: {path, video, audio, encoder, output}. Writes one JSON
|
||||
* verdict per line.
|
||||
*/
|
||||
|
||||
import { extname } from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
|
||||
const APPLE_NATIVE_VIDEO = /^(avc1|h264|hvc1|hev1|hevc)/i;
|
||||
const APPLE_NATIVE_AUDIO = /^(aac|mp4a)/i;
|
||||
const MP4_CONTAINERS = new Set([".mp4", ".m4v", ".mov"]);
|
||||
|
||||
function isAppleCompatible(videoCodec, audioCodec, videoPath) {
|
||||
if (!videoCodec || !audioCodec || !videoPath) return false;
|
||||
return (
|
||||
APPLE_NATIVE_VIDEO.test(videoCodec) &&
|
||||
APPLE_NATIVE_AUDIO.test(audioCodec) &&
|
||||
MP4_CONTAINERS.has(extname(videoPath).toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The encoder table. The original reads COMPATIBLE_VIDEO_ENCODER here; the
|
||||
* name is passed in instead, so one run can cover every branch.
|
||||
*/
|
||||
function videoEncodePlan(name, device) {
|
||||
switch ((name || "h264_nvenc").toLowerCase()) {
|
||||
case "h264_videotoolbox":
|
||||
return {
|
||||
preInput: [],
|
||||
video: ["-c:v", "h264_videotoolbox", "-q:v", "60", "-profile:v", "high", "-pix_fmt", "yuv420p"],
|
||||
};
|
||||
case "h264_vaapi":
|
||||
return {
|
||||
preInput: ["-init_hw_device", `vaapi=va:${device || "/dev/dri/renderD128"}`, "-filter_hw_device", "va"],
|
||||
video: ["-vf", "format=nv12,hwupload", "-c:v", "h264_vaapi", "-qp", "23"],
|
||||
};
|
||||
case "libx264":
|
||||
return {
|
||||
preInput: [],
|
||||
video: ["-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-pix_fmt", "yuv420p"],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
preInput: [],
|
||||
video: ["-c:v", "h264_nvenc", "-preset", "p5", "-rc", "vbr", "-cq", "23", "-b:v", "0"],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function verdict(c) {
|
||||
const vcodec = c.video || "";
|
||||
const acodec = c.audio || "";
|
||||
|
||||
if (isAppleCompatible(vcodec, acodec, c.path)) {
|
||||
return { path: c.path, playable: true, encoder: null, copies_audio: true, argv: [] };
|
||||
}
|
||||
|
||||
const copyVideo = APPLE_NATIVE_VIDEO.test(vcodec);
|
||||
const copyAudio = APPLE_NATIVE_AUDIO.test(acodec);
|
||||
const base = c.path.slice(0, c.path.length - extname(c.path).length);
|
||||
const out = c.output || `${base}.h264.mp4`;
|
||||
|
||||
const plan = copyVideo
|
||||
? { preInput: [], video: ["-c:v", "copy"] }
|
||||
: videoEncodePlan(c.encoder, c.vaapi_device);
|
||||
const encoder = copyVideo ? "copy" : (c.encoder || "h264_nvenc");
|
||||
|
||||
const argv = [
|
||||
"-nostdin",
|
||||
"-y",
|
||||
...plan.preInput,
|
||||
"-i",
|
||||
c.path,
|
||||
...plan.video,
|
||||
...(copyAudio ? ["-c:a", "copy"] : ["-c:a", "aac", "-b:a", "160k", "-ac", "2"]),
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
out,
|
||||
];
|
||||
return { path: c.path, playable: false, encoder, copies_audio: copyAudio, argv };
|
||||
}
|
||||
|
||||
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
||||
for await (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
process.stdout.write(JSON.stringify(verdict(JSON.parse(line))) + "\n");
|
||||
}
|
||||
+14
-1
@@ -19,7 +19,7 @@ type Written = Result<Vec<String>, String>;
|
||||
type Job = (&'static str, fn() -> Written);
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let jobs: [Job; 15] = [
|
||||
let jobs: [Job; 16] = [
|
||||
("tokens", tokens),
|
||||
("unescape", unescape),
|
||||
("chunkmd", chunkmd),
|
||||
@@ -35,6 +35,7 @@ fn main() -> ExitCode {
|
||||
("author", author),
|
||||
("vocab", vocab),
|
||||
("mdsplit", mdsplit),
|
||||
("avplan", avplan),
|
||||
];
|
||||
|
||||
for (name, job) in jobs {
|
||||
@@ -217,6 +218,18 @@ fn cueseg() -> Written {
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
fn avplan() -> Written {
|
||||
let dir = parity::golden("avplan");
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
|
||||
let cases = parity::dir().join("cases/avplan/cases.ndjson");
|
||||
let text = std::fs::read_to_string(&cases).map_err(|e| e.to_string())?;
|
||||
let verdicts = parity::run_node_oracle("compatible.mjs", &text)?;
|
||||
let path = dir.join("cases.ndjson");
|
||||
std::fs::write(&path, verdicts).map_err(|e| e.to_string())?;
|
||||
Ok(vec![path.display().to_string()])
|
||||
}
|
||||
|
||||
fn cuenorm() -> Written {
|
||||
let dir = parity::golden("cuenorm");
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
//! The verdict and the command line must match the media server's exactly.
|
||||
//!
|
||||
//! A wrong "it plays" ships a file that opens to a black screen on the device
|
||||
//! it was converted for; a wrong "it does not" spends forty minutes of GPU
|
||||
//! time re-encoding something that was already fine. The command line has to
|
||||
//! agree argument for argument, because a dropped `-pix_fmt` or a `-movflags`
|
||||
//! in the wrong place is a file that encodes successfully and still will not
|
||||
//! play.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// The encoder the server defaults to when nothing names one.
|
||||
const DEFAULT_ENCODER: &str = "h264_nvenc";
|
||||
|
||||
fn named(name: &str) -> avplan::Encoder {
|
||||
*avplan::encoder::PREFERRED
|
||||
.iter()
|
||||
.find(|encoder| encoder.name() == name)
|
||||
.unwrap_or_else(|| panic!("{name} is not an encoder"))
|
||||
}
|
||||
|
||||
fn records(text: &str) -> Vec<Value> {
|
||||
text.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| serde_json::from_str(line).expect("valid JSON"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn cases() -> Vec<Value> {
|
||||
let text = std::fs::read_to_string(parity::dir().join("cases/avplan/cases.ndjson"))
|
||||
.expect("a readable case file");
|
||||
records(&text)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_verdict_matches_the_original() {
|
||||
let golden = records(&parity::read_golden_text("avplan/cases.ndjson"));
|
||||
let cases = cases();
|
||||
assert_eq!(cases.len(), golden.len(), "case count moved — regenerate");
|
||||
|
||||
let mut wrong: Vec<String> = Vec::new();
|
||||
for (case, theirs) in cases.iter().zip(&golden) {
|
||||
let name = case["name"].as_str().expect("a name");
|
||||
let path = case["path"].as_str().expect("a path");
|
||||
assert_eq!(theirs["path"].as_str(), Some(path), "cases out of order");
|
||||
|
||||
let streams = avplan::Streams {
|
||||
video: case["video"].as_str().map(str::to_string),
|
||||
audio: case["audio"].as_str().map(str::to_string),
|
||||
container: avplan::container_of(path),
|
||||
};
|
||||
let device = case["vaapi_device"]
|
||||
.as_str()
|
||||
.unwrap_or(avplan::DEFAULT_VAAPI_DEVICE);
|
||||
let output = case["output"]
|
||||
.as_str()
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| avplan::output_beside(path));
|
||||
let chosen = named(case["encoder"].as_str().unwrap_or(DEFAULT_ENCODER));
|
||||
let ours = avplan::plan(path, &streams, Some(chosen), &output, device);
|
||||
|
||||
let mut note = |field: &str, ours: String, theirs: &Value| {
|
||||
wrong.push(format!("{name}.{field}: original {theirs}, rust {ours}"));
|
||||
};
|
||||
|
||||
if Some(ours.playable) != theirs["playable"].as_bool() {
|
||||
note("playable", ours.playable.to_string(), &theirs["playable"]);
|
||||
}
|
||||
if Some(ours.copies_audio) != theirs["copies_audio"].as_bool() {
|
||||
note(
|
||||
"copies_audio",
|
||||
ours.copies_audio.to_string(),
|
||||
&theirs["copies_audio"],
|
||||
);
|
||||
}
|
||||
|
||||
let their_encoder = theirs["encoder"].as_str();
|
||||
let our_encoder = ours.encoder.map(avplan::Encoder::name);
|
||||
if our_encoder != their_encoder {
|
||||
note("encoder", format!("{our_encoder:?}"), &theirs["encoder"]);
|
||||
}
|
||||
|
||||
let their_argv: Vec<&str> = theirs["argv"]
|
||||
.as_array()
|
||||
.expect("an argument list")
|
||||
.iter()
|
||||
.map(|arg| arg.as_str().expect("a string argument"))
|
||||
.collect();
|
||||
if ours.argv != their_argv {
|
||||
note("argv", format!("{:?}", ours.argv), &theirs["argv"]);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
wrong.is_empty(),
|
||||
"{} verdicts differ:\n{}",
|
||||
wrong.len(),
|
||||
wrong.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
/// Every branch of the encoder table has to be exercised, or the parity test
|
||||
/// proves nothing about the three encoders nobody's laptop happens to have.
|
||||
#[test]
|
||||
fn every_encoder_appears_among_the_cases() {
|
||||
let named: Vec<String> = cases()
|
||||
.iter()
|
||||
.filter_map(|case| case["encoder"].as_str().map(str::to_string))
|
||||
.collect();
|
||||
for encoder in avplan::encoder::PREFERRED {
|
||||
assert!(
|
||||
named.iter().any(|name| name == encoder.name()),
|
||||
"no case uses {}",
|
||||
encoder.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Catches a golden file drifting away from an original that has moved on.
|
||||
#[test]
|
||||
#[cfg(feature = "python")]
|
||||
fn the_golden_file_still_matches_the_live_original() {
|
||||
let cases = std::fs::read_to_string(parity::dir().join("cases/avplan/cases.ndjson"))
|
||||
.expect("a readable case file");
|
||||
let live = parity::run_node_oracle("compatible.mjs", &cases).expect("the oracle runs");
|
||||
assert_eq!(live, parity::read_golden_text("avplan/cases.ndjson"));
|
||||
}
|
||||
Reference in New Issue
Block a user