mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-17 06:26:31 +03:00
fix: harden FFmpeg capability preflight and references
Repairs public FFmpeg evidence references, adds named capability checks, and expands deterministic failure-path coverage. Closes #428.
This commit is contained in:
@@ -1,36 +1,311 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Smoke-test the FFmpeg preflight script without media or network access."""
|
||||
"""Deterministic pytest suite for ffmpeg-preflight.
|
||||
|
||||
Fake ffmpeg/ffprobe binaries cover the declared failure matrix without real
|
||||
media, network access, GPU hardware, or dependence on a particular CI image.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
def test_preflight_with_fake_tools():
|
||||
script = Path(__file__).resolve().parent / "ffmpeg-preflight"
|
||||
import pytest
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fake_bin = Path(directory)
|
||||
for name in ("ffmpeg", "ffprobe"):
|
||||
tool = fake_bin / name
|
||||
tool.write_text("#!/bin/sh\nprintf \"%s\\n\" \"$0 $*\"\n")
|
||||
tool.chmod(0o755)
|
||||
SCRIPT = Path(__file__).resolve().parent / "ffmpeg-preflight"
|
||||
|
||||
FAKE_FFMPEG_VERSION = "ffmpeg version 8.1.2 fake"
|
||||
FAKE_FFPROBE_VERSION = "ffprobe version 8.1.2 fake"
|
||||
|
||||
FIXTURE_FILTERS = """Filters:
|
||||
T.. = Timeline support
|
||||
A = Audio input/output
|
||||
V = Video input/output
|
||||
| = Source or sink filter
|
||||
------
|
||||
.. scale V->V Scale the input video size.
|
||||
TS aap AA->A Apply Affine Projection algorithm.
|
||||
.. anullsrc |->A Null audio source, return empty audio frames.
|
||||
.. abuffersink A->| Buffer audio frames,
|
||||
"""
|
||||
|
||||
FIXTURE_ENCODERS = """Encoders:
|
||||
V..... = Video
|
||||
A..... = Audio
|
||||
S..... = Subtitle
|
||||
------
|
||||
V....D libx264 libx264 H.264 (codec h264)
|
||||
A....D aac AAC (Advanced Audio Coding)
|
||||
V....D libvpx-vp9 libvpx VP9 (codec vp9)
|
||||
"""
|
||||
|
||||
FIXTURE_HWACCELS = """Hardware acceleration methods:
|
||||
videotoolbox
|
||||
"""
|
||||
|
||||
HEADER_ONLY_FILTERS = "Filters:\n"
|
||||
MALFORMED_FILTERS = """some preamble noise
|
||||
scale V->V without leading fields
|
||||
T.. = Timeline support
|
||||
.. V->V name token is missing
|
||||
not->an-entry
|
||||
Filters:
|
||||
"""
|
||||
|
||||
|
||||
def write_fixtures(directory: Path, filters: str, encoders: str, hwaccels: str) -> None:
|
||||
(directory / "filters.txt").write_text(filters)
|
||||
(directory / "encoders.txt").write_text(encoders)
|
||||
(directory / "hwaccels.txt").write_text(hwaccels)
|
||||
|
||||
|
||||
def install_tools(
|
||||
bin_dir: Path,
|
||||
fixtures_dir: Path,
|
||||
*,
|
||||
with_ffprobe: bool = True,
|
||||
version_rc: int = 0,
|
||||
filters_rc: int = 0,
|
||||
encoders_rc: int = 0,
|
||||
hwaccels_rc: int = 0,
|
||||
) -> None:
|
||||
emit = (
|
||||
"emit() {\n"
|
||||
' while IFS= read -r line || [ -n "$line" ]; do\n'
|
||||
" printf '%s\\n' \"$line\"\n"
|
||||
" done < \"$1\"\n"
|
||||
"}\n"
|
||||
)
|
||||
ffmpeg = (
|
||||
"#!/bin/sh\n"
|
||||
+ emit
|
||||
+ 'case "$*" in\n'
|
||||
f' *-version*) echo "{FAKE_FFMPEG_VERSION}"; exit {version_rc} ;;\n'
|
||||
f' *-filters*) emit "$FIXTURES/filters.txt"; exit {filters_rc} ;;\n'
|
||||
f' *-encoders*) emit "$FIXTURES/encoders.txt"; exit {encoders_rc} ;;\n'
|
||||
f' *-hwaccels*) emit "$FIXTURES/hwaccels.txt"; exit {hwaccels_rc} ;;\n'
|
||||
"esac\n"
|
||||
'echo "unexpected arguments: $*" >&2\n'
|
||||
"exit 99\n"
|
||||
)
|
||||
ffmpeg_path = bin_dir / "ffmpeg"
|
||||
ffmpeg_path.write_text(ffmpeg)
|
||||
ffmpeg_path.chmod(0o755)
|
||||
if with_ffprobe:
|
||||
ffprobe = bin_dir / "ffprobe"
|
||||
ffprobe.write_text(f'#!/bin/sh\necho "{FAKE_FFPROBE_VERSION}"\n')
|
||||
ffprobe.chmod(0o755)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def environment(tmp_path: Path):
|
||||
"""Provide (setup, run) with healthy fake tools pre-installed.
|
||||
|
||||
``setup(**overrides)`` rebuilds the fake tools (use for missing ffprobe,
|
||||
failing probes, or alternate fixtures). ``run(*args, bin_dir=None)``
|
||||
invokes the preflight script with PATH restricted to the fake bin dir.
|
||||
"""
|
||||
|
||||
bin_dir = tmp_path / "bin"
|
||||
fixtures_dir = tmp_path / "fixtures"
|
||||
|
||||
def setup(
|
||||
*,
|
||||
with_ffprobe: bool = True,
|
||||
filters: str = FIXTURE_FILTERS,
|
||||
encoders: str = FIXTURE_ENCODERS,
|
||||
hwaccels: str = FIXTURE_HWACCELS,
|
||||
**tool_kwargs,
|
||||
) -> Path:
|
||||
shutil.rmtree(bin_dir, ignore_errors=True)
|
||||
bin_dir.mkdir()
|
||||
fixtures_dir.mkdir(exist_ok=True)
|
||||
write_fixtures(fixtures_dir, filters, encoders, hwaccels)
|
||||
install_tools(bin_dir, fixtures_dir, with_ffprobe=with_ffprobe, **tool_kwargs)
|
||||
return bin_dir
|
||||
|
||||
def run(*arguments: str, bin_dir: Path | None = None) -> subprocess.CompletedProcess:
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = str(fake_bin)
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--json"],
|
||||
env["PATH"] = str(bin_dir if bin_dir is not None else tmp_path / "bin")
|
||||
env["FIXTURES"] = str(fixtures_dir)
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), *arguments],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
setup()
|
||||
return setup, run
|
||||
|
||||
|
||||
def test_success_json_reports_availability_counts(environment):
|
||||
setup, run = environment
|
||||
result = run("--json")
|
||||
assert result.returncode == 0, result.stderr
|
||||
report = json.loads(result.stdout)
|
||||
assert report["ffmpeg"]["available"] is True
|
||||
assert report["ffmpeg"]["first_line"] == FAKE_FFMPEG_VERSION
|
||||
assert report["ffprobe"]["available"] is True
|
||||
assert report["filters"]["available"] is True
|
||||
assert report["filters"]["entry_count"] == 4
|
||||
assert report["encoders"]["entry_count"] == 3
|
||||
assert report["hwaccels"]["entry_count"] == 1
|
||||
assert "warning" not in report["filters"]
|
||||
assert report["queries"] == {}
|
||||
|
||||
|
||||
def test_named_queries_present_exit_zero(environment):
|
||||
setup, run = environment
|
||||
result = run("--json", "--filter", "scale", "--filter", "anullsrc",
|
||||
"--encoder", "libx264", "--encoder", "libvpx-vp9",
|
||||
"--hwaccel", "videotoolbox")
|
||||
assert result.returncode == 0, result.stderr
|
||||
report = json.loads(result.stdout)
|
||||
assert report["queries"] == {
|
||||
"filter": {"scale": True, "anullsrc": True},
|
||||
"encoder": {"libx264": True, "libvpx-vp9": True},
|
||||
"hwaccel": {"videotoolbox": True},
|
||||
}
|
||||
|
||||
|
||||
def test_named_queries_absent_exit_two(environment):
|
||||
setup, run = environment
|
||||
result = run("--json", "--filter", "drawtext", "--encoder", "nosuchenc",
|
||||
"--hwaccel", "cuda")
|
||||
assert result.returncode == 2
|
||||
report = json.loads(result.stdout)
|
||||
assert report["queries"]["filter"]["drawtext"] is False
|
||||
assert report["queries"]["encoder"]["nosuchenc"] is False
|
||||
assert report["queries"]["hwaccel"]["cuda"] is False
|
||||
|
||||
|
||||
def test_mixed_queries_exit_two(environment):
|
||||
setup, run = environment
|
||||
result = run("--filter", "scale", "--filter", "drawtext")
|
||||
assert result.returncode == 2
|
||||
|
||||
|
||||
def test_missing_tools_exit_one(environment, tmp_path):
|
||||
setup, run = environment
|
||||
empty_bin = tmp_path / "empty-bin"
|
||||
empty_bin.mkdir()
|
||||
result = run("--json", "--filter", "scale", bin_dir=empty_bin)
|
||||
assert result.returncode == 1
|
||||
report = json.loads(result.stdout)
|
||||
assert report["ffmpeg"]["available"] is False
|
||||
assert report["ffprobe"]["available"] is False
|
||||
assert report["filters"]["available"] is False
|
||||
assert report["queries"] == {"filter": {"scale": False}}
|
||||
|
||||
|
||||
def test_ffprobe_missing_exit_one(environment):
|
||||
setup, run = environment
|
||||
setup(with_ffprobe=False)
|
||||
result = run("--json")
|
||||
assert result.returncode == 1
|
||||
report = json.loads(result.stdout)
|
||||
assert report["ffmpeg"]["available"] is True
|
||||
assert report["ffprobe"]["available"] is False
|
||||
|
||||
|
||||
def test_inventory_command_failure_exit_one(environment):
|
||||
setup, run = environment
|
||||
setup(filters_rc=3)
|
||||
result = run("--json")
|
||||
assert result.returncode == 1
|
||||
report = json.loads(result.stdout)
|
||||
assert report["filters"]["available"] is False
|
||||
assert report["filters"]["returncode"] == 3
|
||||
assert report["encoders"]["available"] is True
|
||||
assert report["hardware_acceleration"]["available"] is True
|
||||
print("ffmpeg preflight smoke test passed")
|
||||
|
||||
|
||||
def test_inventory_failure_takes_precedence_over_absent_query(environment):
|
||||
setup, run = environment
|
||||
setup(filters_rc=1)
|
||||
result = run("--filter", "scale")
|
||||
assert result.returncode == 1
|
||||
|
||||
|
||||
def test_empty_inventory_warns_and_exits_zero_without_queries(environment):
|
||||
setup, run = environment
|
||||
setup(filters=HEADER_ONLY_FILTERS, encoders="", hwaccels="")
|
||||
result = run("--json")
|
||||
assert result.returncode == 0, result.stderr
|
||||
report = json.loads(result.stdout)
|
||||
assert report["filters"]["entry_count"] == 0
|
||||
assert "warning" in report["filters"]
|
||||
assert report["encoders"]["entry_count"] == 0
|
||||
assert report["hwaccels"]["entry_count"] == 0
|
||||
|
||||
|
||||
def test_query_against_empty_inventory_is_absent_exit_two(environment):
|
||||
setup, run = environment
|
||||
setup(filters=HEADER_ONLY_FILTERS)
|
||||
result = run("--filter", "scale")
|
||||
assert result.returncode == 2
|
||||
|
||||
|
||||
def test_malformed_output_yields_no_entries(environment):
|
||||
setup, run = environment
|
||||
setup(filters=MALFORMED_FILTERS, encoders="garbage line\nsecond line\n")
|
||||
result = run("--json", "--filter", "scale", "--encoder", "libx264")
|
||||
assert result.returncode == 2
|
||||
report = json.loads(result.stdout)
|
||||
assert report["filters"]["entry_count"] == 0
|
||||
assert report["encoders"]["entry_count"] == 0
|
||||
assert report["queries"]["filter"]["scale"] is False
|
||||
|
||||
|
||||
def test_repeated_flags_are_deduplicated(environment):
|
||||
setup, run = environment
|
||||
result = run("--json", "--filter", "scale", "--filter", "scale", "--filter", "scale")
|
||||
assert result.returncode == 0, result.stderr
|
||||
report = json.loads(result.stdout)
|
||||
assert report["queries"]["filter"] == {"scale": True}
|
||||
|
||||
|
||||
def test_stderr_noise_is_not_parsed_as_inventory(environment, tmp_path):
|
||||
setup, run = environment
|
||||
noisy = tmp_path / "bin" / "ffmpeg"
|
||||
body = noisy.read_text()
|
||||
noisy.write_text(body.replace(
|
||||
'#!/bin/sh\n',
|
||||
'#!/bin/sh\necho "static banner noise" >&2\necho "banner on stdout too" \n',
|
||||
))
|
||||
result = run("--json")
|
||||
assert result.returncode == 0, result.stderr
|
||||
report = json.loads(result.stdout)
|
||||
assert report["filters"]["entry_count"] == 4
|
||||
|
||||
|
||||
def test_text_mode_is_concise_and_names_results(environment):
|
||||
setup, run = environment
|
||||
result = run("--filter", "scale", "--encoder", "nosuchenc")
|
||||
assert result.returncode == 2
|
||||
lines = result.stdout.splitlines()
|
||||
assert FAKE_FFMPEG_VERSION in result.stdout
|
||||
assert any(l.startswith("filters: 4 entries parsed") for l in lines)
|
||||
assert any(l.startswith("filter 'scale': present") for l in lines)
|
||||
assert any(l.startswith("encoder 'nosuchenc': absent") for l in lines)
|
||||
assert "Scale the input video size" not in result.stdout
|
||||
|
||||
|
||||
def test_json_mode_stdout_is_a_single_json_document(environment):
|
||||
setup, run = environment
|
||||
result = run("--json")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.lstrip().startswith("{")
|
||||
json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_human_mode_failure_line_for_missing_binary(environment, tmp_path):
|
||||
setup, run = environment
|
||||
empty_bin = tmp_path / "nobin"
|
||||
empty_bin.mkdir()
|
||||
result = run(bin_dir=empty_bin)
|
||||
assert result.returncode == 1
|
||||
assert "ffmpeg: unavailable" in result.stdout
|
||||
|
||||
Reference in New Issue
Block a user