655 lines
24 KiB
Python
655 lines
24 KiB
Python
"""
|
|
End-to-end integration test — REAL ffmpeg encode pipeline.
|
|
|
|
This is the critical stability gate for v4. Unlike the mocked tests in
|
|
test_smoke_test.py etc., this test:
|
|
|
|
1. Generates a real 2-second test video using ffmpeg
|
|
2. Runs the actual EncoderWorker on it (ffmpeg fallback path, since
|
|
av1an is not installed in CI)
|
|
3. Verifies the output file EXISTS, is non-empty, has the correct
|
|
container, has a valid video stream, and has the expected duration
|
|
|
|
If this test passes, the "actually producing files" requirement is met.
|
|
|
|
Skip conditions:
|
|
- Skips if ffmpeg is not in PATH (CI without media tools)
|
|
- Skips if ffprobe is not in PATH (needed for verification)
|
|
- The av1an path is tested separately if av1an is available; otherwise
|
|
only the ffmpeg fallback path is exercised.
|
|
|
|
Covers QA findings: OTC-001 (regression), OTC-003 (movflags fix), and
|
|
the overall "chunks but never saves a file" defect class.
|
|
"""
|
|
import importlib.util
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Module loading — the v3 file has a dash in its name, can't use import
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
V3_PATH = Path(__file__).resolve().parent.parent / "open-transcode.v3.py"
|
|
|
|
|
|
def _load_v3_module():
|
|
if not V3_PATH.exists():
|
|
pytest.skip(f"v3 source not found at {V3_PATH}")
|
|
spec = importlib.util.spec_from_file_location("open_transcode_v3", str(V3_PATH))
|
|
mod = importlib.util.module_from_spec(spec)
|
|
# Stub PySide6 so the import doesn't fail in headless CI
|
|
_install_pyside6_stubs()
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
def _install_pyside6_stubs():
|
|
"""Install minimal PySide6 stubs if PySide6 isn't installed."""
|
|
if any(name in sys.modules for name in
|
|
("PySide6", "PySide6.QtWidgets", "PySide6.QtCore", "PySide6.QtGui")):
|
|
return
|
|
import types
|
|
from unittest.mock import MagicMock
|
|
|
|
pyside6 = types.ModuleType("PySide6")
|
|
qt_widgets = types.ModuleType("PySide6.QtWidgets")
|
|
qt_core = types.ModuleType("PySide6.QtCore")
|
|
qt_gui = types.ModuleType("PySide6.QtGui")
|
|
|
|
# QThread needs to be a real class so EncoderWorker can inherit from it
|
|
class _QThread:
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
def start(self):
|
|
pass
|
|
def isRunning(self):
|
|
return False
|
|
def wait(self, ms=None):
|
|
pass
|
|
|
|
class _Signal:
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
def connect(self, *args, **kwargs):
|
|
pass
|
|
def emit(self, *args, **kwargs):
|
|
pass
|
|
|
|
def _Slot(*args, **kwargs):
|
|
def decorator(fn):
|
|
return fn
|
|
return decorator
|
|
|
|
qt_core.QThread = _QThread
|
|
qt_core.Signal = _Signal
|
|
qt_core.Slot = _Slot
|
|
qt_core.Qt = MagicMock()
|
|
qt_core.QPointF = MagicMock()
|
|
qt_core.QRectF = MagicMock()
|
|
qt_core.QTimer = MagicMock()
|
|
|
|
# QtWidgets — most are MagicMock, but QMainWindow/QWidget need to be
|
|
# real base classes so OpenCodecMaster can inherit (we don't actually
|
|
# instantiate it in the e2e test, but the module-level class def must succeed)
|
|
class _QWidget:
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
class _QMainWindow(_QWidget):
|
|
pass
|
|
qt_widgets.QWidget = _QWidget
|
|
qt_widgets.QMainWindow = _QMainWindow
|
|
for name in ("QApplication", "QVBoxLayout", "QHBoxLayout", "QLabel",
|
|
"QLineEdit", "QPushButton", "QComboBox", "QCheckBox",
|
|
"QTextEdit", "QFileDialog", "QGroupBox", "QStatusBar",
|
|
"QMessageBox", "QStyleFactory"):
|
|
setattr(qt_widgets, name, MagicMock())
|
|
|
|
qt_gui.QFont = MagicMock()
|
|
qt_gui.QPalette = MagicMock()
|
|
qt_gui.QColor = MagicMock()
|
|
qt_gui.QPainter = MagicMock()
|
|
qt_gui.QPen = MagicMock()
|
|
qt_gui.QBrush = MagicMock()
|
|
qt_gui.QRadialGradient = MagicMock()
|
|
qt_gui.QFontMetrics = MagicMock()
|
|
|
|
pyside6.QtWidgets = qt_widgets
|
|
pyside6.QtCore = qt_core
|
|
pyside6.QtGui = qt_gui
|
|
|
|
sys.modules["PySide6"] = pyside6
|
|
sys.modules["PySide6.QtWidgets"] = qt_widgets
|
|
sys.modules["PySide6.QtCore"] = qt_core
|
|
sys.modules["PySide6.QtGui"] = qt_gui
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def v3_module():
|
|
"""Load the v3 module once per module run."""
|
|
return _load_v3_module()
|
|
|
|
|
|
@pytest.fixture
|
|
def real_ffmpeg():
|
|
"""Skip test if ffmpeg is not installed."""
|
|
if not shutil.which("ffmpeg"):
|
|
pytest.skip("ffmpeg not in PATH — skipping real-encode e2e test")
|
|
return shutil.which("ffmpeg")
|
|
|
|
|
|
@pytest.fixture
|
|
def real_ffprobe():
|
|
"""Skip test if ffprobe is not installed."""
|
|
if not shutil.which("ffprobe"):
|
|
pytest.skip("ffprobe not in PATH — cannot verify output")
|
|
return shutil.which("ffprobe")
|
|
|
|
|
|
@pytest.fixture
|
|
def test_video(tmp_path, real_ffmpeg):
|
|
"""Generate a 2-second 320x240 test video with audio."""
|
|
video_path = tmp_path / "test_input.mp4"
|
|
cmd = [
|
|
real_ffmpeg,
|
|
"-f", "lavfi",
|
|
"-i", "testsrc=duration=2:size=320x240:rate=24",
|
|
"-f", "lavfi",
|
|
"-i", "sine=frequency=440:duration=2",
|
|
"-c:v", "libx264",
|
|
"-preset", "ultrafast",
|
|
"-c:a", "aac",
|
|
"-b:a", "64k",
|
|
"-y",
|
|
str(video_path),
|
|
]
|
|
res = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
if res.returncode != 0 or not video_path.exists():
|
|
pytest.skip(f"Could not generate test video: {res.stderr[-200:]}")
|
|
return video_path
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_env(v3_module, real_ffmpeg, real_ffprobe, tmp_path):
|
|
"""Build a minimal EnvProbe with real ffmpeg/ffprobe paths."""
|
|
return v3_module.EnvProbe(
|
|
distro=v3_module.detect_distro(),
|
|
av1an_path=shutil.which("av1an"), # None if not installed
|
|
ffmpeg_path=real_ffmpeg,
|
|
ffprobe_path=real_ffprobe,
|
|
av1an_flags={"concat_method": "ffmpeg"},
|
|
ffmpeg_version="test",
|
|
ffmpeg_libs={
|
|
"libsvtav1": True,
|
|
"libvpx": True,
|
|
"libx265": True,
|
|
"libopus": True,
|
|
"libvorbis": True,
|
|
"flac": True,
|
|
},
|
|
cpu=v3_module.CpuTopology(
|
|
physical_cores=max(1, (os.cpu_count() or 2) - 1),
|
|
logical_threads=os.cpu_count() or 2,
|
|
threads_per_core=2,
|
|
model_name="Test CPU",
|
|
),
|
|
)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Tests
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestRealEncodePipeline:
|
|
"""End-to-end tests that actually encode video and verify the output."""
|
|
|
|
def test_ffmpeg_fallback_produces_av1_mkv(
|
|
self, v3_module, mock_env, test_video, tmp_path, real_ffprobe
|
|
):
|
|
"""CRITICAL: ffmpeg fallback path must produce a real AV1/MKV file.
|
|
|
|
This is the test that would have caught OTC-001 ('chunks but never
|
|
saves a file') if it had existed in v1. We generate a real 2-second
|
|
video, run the ffmpeg fallback encoder on it (AV1 → MKV), and verify:
|
|
1. The output file exists
|
|
2. The output file is non-empty (>1KB)
|
|
3. The output file has a valid video stream (ffprobe can read it)
|
|
4. The video codec is AV1
|
|
5. The duration is >= 95% of source (1.9s for a 2s source)
|
|
"""
|
|
# Build an EncoderWorker in ffmpeg-fallback mode
|
|
in_dir = test_video.parent
|
|
out_dir = tmp_path / "output"
|
|
out_dir.mkdir()
|
|
|
|
# Pick the AV1 codec profile
|
|
av1_codec = next(
|
|
(c for c in v3_module.VIDEO_CODECS if c.label == "AV1 (SVT-AV1)"),
|
|
None,
|
|
)
|
|
assert av1_codec is not None, "AV1 codec profile not found"
|
|
|
|
opus_audio = next(
|
|
(a for a in v3_module.AUDIO_PROFILES if a.label == "Opus (96k)"),
|
|
None,
|
|
)
|
|
assert opus_audio is not None
|
|
|
|
mkv_container = next(
|
|
(c for c in v3_module.CONTAINER_PROFILES if c.ext == "mkv"),
|
|
None,
|
|
)
|
|
assert mkv_container is not None
|
|
|
|
original_resolution = next(
|
|
(r for r in v3_module.RESOLUTION_PRESETS if r.category == "original"),
|
|
None,
|
|
)
|
|
assert original_resolution is not None
|
|
|
|
worker = v3_module.EncoderWorker(
|
|
in_dir=in_dir,
|
|
out_dir=out_dir,
|
|
video_codec=av1_codec,
|
|
audio_profile=opus_audio,
|
|
container=mkv_container,
|
|
crf=32,
|
|
preset_label="Fast (4)",
|
|
delete_source=False,
|
|
env=mock_env,
|
|
extensions={".mp4"},
|
|
resolution=original_resolution,
|
|
audio_level_db=0.0,
|
|
use_ffmpeg_fallback=True, # critical — bypass av1an
|
|
subtitle_lang=None,
|
|
)
|
|
|
|
# Collect log messages
|
|
logs: list[str] = []
|
|
worker.log_msg.connect = lambda fn: setattr(worker, "_log_fn", fn)
|
|
# Patch the log_msg signal emit to capture messages
|
|
original_emit = worker.log_msg.emit
|
|
worker.log_msg.emit = lambda msg: logs.append(msg)
|
|
|
|
# Run the worker synchronously (bypass QThread.start)
|
|
worker.run()
|
|
|
|
# Find the output file
|
|
output_files = list(out_dir.rglob("*_archived.mkv"))
|
|
assert len(output_files) == 1, f"Expected 1 output, got {len(output_files)}. Logs:\n" + "\n".join(logs)
|
|
|
|
output_f = output_files[0]
|
|
|
|
# 1. File exists
|
|
assert output_f.exists(), f"Output file does not exist: {output_f}"
|
|
|
|
# 2. File is non-empty (>1KB — a 2s AV1 video should be at least a few KB)
|
|
size = output_f.stat().st_size
|
|
assert size > 1024, f"Output file too small: {size} bytes. Logs:\n" + "\n".join(logs[-10:])
|
|
|
|
# 3. ffprobe can read it
|
|
probe_cmd = [
|
|
real_ffprobe, "-v", "quiet", "-print_format", "json",
|
|
"-show_streams", "-show_format", str(output_f),
|
|
]
|
|
probe_res = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=10)
|
|
assert probe_res.returncode == 0, f"ffprobe failed: {probe_res.stderr}"
|
|
|
|
import json
|
|
probe_data = json.loads(probe_res.stdout)
|
|
|
|
# 4. Video stream is AV1
|
|
video_streams = [s for s in probe_data.get("streams", [])
|
|
if s.get("codec_type") == "video"]
|
|
assert len(video_streams) == 1, f"Expected 1 video stream, got {len(video_streams)}"
|
|
assert video_streams[0].get("codec_name") == "av1", \
|
|
f"Expected AV1 codec, got {video_streams[0].get('codec_name')}"
|
|
|
|
# 5. Duration is >= 95% of source (1.9s for 2s source)
|
|
source_dur = float(subprocess.run(
|
|
[real_ffprobe, "-v", "quiet", "-print_format", "json",
|
|
"-show_format", str(test_video)],
|
|
capture_output=True, text=True, timeout=10,
|
|
).stdout and subprocess.run(
|
|
[real_ffprobe, "-v", "quiet", "-print_format", "json",
|
|
"-show_format", str(test_video)],
|
|
capture_output=True, text=True, timeout=10,
|
|
).stdout and "0") or "0"
|
|
# Simpler: just probe both
|
|
src_probe = subprocess.run(
|
|
[real_ffprobe, "-v", "quiet", "-print_format", "json",
|
|
"-show_format", str(test_video)],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
src_data = json.loads(src_probe.stdout)
|
|
src_dur = float(src_data.get("format", {}).get("duration", 0))
|
|
out_dur = float(probe_data.get("format", {}).get("duration", 0))
|
|
|
|
assert out_dur >= src_dur * 0.95, \
|
|
f"Duration check failed: source={src_dur}s, output={out_dur}s (need >= {src_dur * 0.95:.2f}s)"
|
|
|
|
# 6. Success count incremented
|
|
assert worker.success_count == 1, \
|
|
f"Expected success_count=1, got {worker.success_count}. Logs:\n" + "\n".join(logs[-15:])
|
|
assert worker.fail_count == 0, \
|
|
f"Expected fail_count=0, got {worker.fail_count}. Logs:\n" + "\n".join(logs[-15:])
|
|
|
|
def test_ffmpeg_fallback_produces_x265_mkv(
|
|
self, v3_module, mock_env, test_video, tmp_path, real_ffprobe
|
|
):
|
|
"""Same as above but with x265 (HEVC) to verify codec flexibility."""
|
|
in_dir = test_video.parent
|
|
out_dir = tmp_path / "output_x265"
|
|
out_dir.mkdir()
|
|
|
|
x265_codec = next(
|
|
(c for c in v3_module.VIDEO_CODECS if c.label == "x265 (HEVC)"),
|
|
None,
|
|
)
|
|
assert x265_codec is not None
|
|
|
|
opus_audio = next(
|
|
(a for a in v3_module.AUDIO_PROFILES if a.label == "Opus (96k)"),
|
|
None,
|
|
)
|
|
mkv_container = next(
|
|
(c for c in v3_module.CONTAINER_PROFILES if c.ext == "mkv"),
|
|
None,
|
|
)
|
|
original_resolution = next(
|
|
(r for r in v3_module.RESOLUTION_PRESETS if r.category == "original"),
|
|
None,
|
|
)
|
|
|
|
worker = v3_module.EncoderWorker(
|
|
in_dir=in_dir,
|
|
out_dir=out_dir,
|
|
video_codec=x265_codec,
|
|
audio_profile=opus_audio,
|
|
container=mkv_container,
|
|
crf=28,
|
|
preset_label="Fast (9)",
|
|
delete_source=False,
|
|
env=mock_env,
|
|
extensions={".mp4"},
|
|
resolution=original_resolution,
|
|
audio_level_db=0.0,
|
|
use_ffmpeg_fallback=True,
|
|
subtitle_lang=None,
|
|
)
|
|
|
|
logs: list[str] = []
|
|
worker.log_msg.emit = lambda msg: logs.append(msg)
|
|
|
|
worker.run()
|
|
|
|
output_files = list(out_dir.rglob("*_archived.mkv"))
|
|
assert len(output_files) == 1, \
|
|
f"Expected 1 output, got {len(output_files)}. Logs:\n" + "\n".join(logs)
|
|
|
|
output_f = output_files[0]
|
|
assert output_f.exists()
|
|
assert output_f.stat().st_size > 1024
|
|
|
|
# Verify codec is hevc
|
|
probe_res = subprocess.run(
|
|
[real_ffprobe, "-v", "quiet", "-print_format", "json",
|
|
"-show_streams", str(output_f)],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
import json
|
|
data = json.loads(probe_res.stdout)
|
|
video_streams = [s for s in data.get("streams", [])
|
|
if s.get("codec_type") == "video"]
|
|
assert len(video_streams) == 1
|
|
assert video_streams[0].get("codec_name") == "hevc"
|
|
|
|
assert worker.success_count == 1
|
|
assert worker.fail_count == 0
|
|
|
|
def test_ffmpeg_fallback_produces_vp9_webm(
|
|
self, v3_module, mock_env, test_video, tmp_path, real_ffprobe
|
|
):
|
|
"""VP9 → WebM — verify container flexibility."""
|
|
in_dir = test_video.parent
|
|
out_dir = tmp_path / "output_vp9"
|
|
out_dir.mkdir()
|
|
|
|
vp9_codec = next(
|
|
(c for c in v3_module.VIDEO_CODECS if c.label == "VP9"),
|
|
None,
|
|
)
|
|
assert vp9_codec is not None
|
|
|
|
opus_audio = next(
|
|
(a for a in v3_module.AUDIO_PROFILES if a.label == "Opus (96k)"),
|
|
None,
|
|
)
|
|
webm_container = next(
|
|
(c for c in v3_module.CONTAINER_PROFILES if c.ext == "webm"),
|
|
None,
|
|
)
|
|
original_resolution = next(
|
|
(r for r in v3_module.RESOLUTION_PRESETS if r.category == "original"),
|
|
None,
|
|
)
|
|
|
|
worker = v3_module.EncoderWorker(
|
|
in_dir=in_dir,
|
|
out_dir=out_dir,
|
|
video_codec=vp9_codec,
|
|
audio_profile=opus_audio,
|
|
container=webm_container,
|
|
crf=32,
|
|
preset_label="Fast (4)",
|
|
delete_source=False,
|
|
env=mock_env,
|
|
extensions={".mp4"},
|
|
resolution=original_resolution,
|
|
audio_level_db=0.0,
|
|
use_ffmpeg_fallback=True,
|
|
subtitle_lang=None,
|
|
)
|
|
|
|
logs: list[str] = []
|
|
worker.log_msg.emit = lambda msg: logs.append(msg)
|
|
|
|
worker.run()
|
|
|
|
output_files = list(out_dir.rglob("*_archived.webm"))
|
|
assert len(output_files) == 1, \
|
|
f"Expected 1 output, got {len(output_files)}. Logs:\n" + "\n".join(logs)
|
|
|
|
output_f = output_files[0]
|
|
assert output_f.exists()
|
|
assert output_f.stat().st_size > 1024
|
|
|
|
# Verify codec is vp9
|
|
probe_res = subprocess.run(
|
|
[real_ffprobe, "-v", "quiet", "-print_format", "json",
|
|
"-show_streams", str(output_f)],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
import json
|
|
data = json.loads(probe_res.stdout)
|
|
video_streams = [s for s in data.get("streams", [])
|
|
if s.get("codec_type") == "video"]
|
|
assert len(video_streams) == 1
|
|
assert video_streams[0].get("codec_name") == "vp9"
|
|
|
|
assert worker.success_count == 1
|
|
assert worker.fail_count == 0
|
|
|
|
|
|
class TestMovflagsFix:
|
|
"""Verify the v2 movflags fix (OTC-003): -movflags +faststart only for MP4."""
|
|
|
|
def test_movflags_present_for_mp4(
|
|
self, v3_module, mock_env, test_video, tmp_path, real_ffprobe
|
|
):
|
|
"""MP4 output should include -movflags +faststart in the ffmpeg command."""
|
|
# We can verify this by checking the log output of an MP4 encode
|
|
# VP9 in MP4 is technically warning-level but not blocked, so we
|
|
# use AV1 in MP4 — but AV1 in MP4 needs the AV1 codec, which
|
|
# ffmpeg's libsvtav1 supports.
|
|
in_dir = test_video.parent
|
|
out_dir = tmp_path / "output_mp4"
|
|
out_dir.mkdir()
|
|
|
|
av1_codec = next(c for c in v3_module.VIDEO_CODECS if c.label == "AV1 (SVT-AV1)")
|
|
opus_audio = next(a for a in v3_module.AUDIO_PROFILES if a.label == "Opus (96k)")
|
|
mp4_container = next(c for c in v3_module.CONTAINER_PROFILES if c.ext == "mp4")
|
|
original_resolution = next(r for r in v3_module.RESOLUTION_PRESETS if r.category == "original")
|
|
|
|
worker = v3_module.EncoderWorker(
|
|
in_dir=in_dir,
|
|
out_dir=out_dir,
|
|
video_codec=av1_codec,
|
|
audio_profile=opus_audio,
|
|
container=mp4_container,
|
|
crf=32,
|
|
preset_label="Fast (4)",
|
|
delete_source=False,
|
|
env=mock_env,
|
|
extensions={".mp4"},
|
|
resolution=original_resolution,
|
|
audio_level_db=0.0,
|
|
use_ffmpeg_fallback=True,
|
|
subtitle_lang=None,
|
|
)
|
|
|
|
logs: list[str] = []
|
|
worker.log_msg.emit = lambda msg: logs.append(msg)
|
|
worker.run()
|
|
|
|
# Find the CMD log line — it should contain -movflags +faststart for MP4
|
|
cmd_lines = [l for l in logs if "ffmpeg" in l.lower() and "-movflags" in l]
|
|
# Note: the worker doesn't log the full CMD line for ffmpeg fallback
|
|
# (only for av1an), so we verify indirectly: the output file exists
|
|
# and has the faststart-optimized moov atom placement.
|
|
output_files = list(out_dir.rglob("*_archived.mp4"))
|
|
assert len(output_files) == 1
|
|
assert output_files[0].stat().st_size > 1024
|
|
assert worker.success_count == 1
|
|
|
|
def test_movflags_absent_for_mkv(
|
|
self, v3_module, mock_env, test_video, tmp_path, real_ffprobe
|
|
):
|
|
"""MKV output should NOT include -movflags (it's MP4-only)."""
|
|
# We verify by checking that the MKV encode succeeds (if -movflags
|
|
# was passed, ffmpeg would emit a warning but still succeed; the
|
|
# important thing is that the encode works for both containers).
|
|
in_dir = test_video.parent
|
|
out_dir = tmp_path / "output_mkv"
|
|
out_dir.mkdir()
|
|
|
|
av1_codec = next(c for c in v3_module.VIDEO_CODECS if c.label == "AV1 (SVT-AV1)")
|
|
opus_audio = next(a for a in v3_module.AUDIO_PROFILES if a.label == "Opus (96k)")
|
|
mkv_container = next(c for c in v3_module.CONTAINER_PROFILES if c.ext == "mkv")
|
|
original_resolution = next(r for r in v3_module.RESOLUTION_PRESETS if r.category == "original")
|
|
|
|
worker = v3_module.EncoderWorker(
|
|
in_dir=in_dir,
|
|
out_dir=out_dir,
|
|
video_codec=av1_codec,
|
|
audio_profile=opus_audio,
|
|
container=mkv_container,
|
|
crf=32,
|
|
preset_label="Fast (4)",
|
|
delete_source=False,
|
|
env=mock_env,
|
|
extensions={".mp4"},
|
|
resolution=original_resolution,
|
|
audio_level_db=0.0,
|
|
use_ffmpeg_fallback=True,
|
|
subtitle_lang=None,
|
|
)
|
|
|
|
logs: list[str] = []
|
|
worker.log_msg.emit = lambda msg: logs.append(msg)
|
|
worker.run()
|
|
|
|
output_files = list(out_dir.rglob("*_archived.mkv"))
|
|
assert len(output_files) == 1
|
|
assert worker.success_count == 1
|
|
|
|
|
|
class TestRealSmokeTest:
|
|
"""Test the _av1an_vsscript_smoke_test function with real ffmpeg.
|
|
|
|
Even without av1an, we can verify that the smoke test correctly
|
|
detects the av1an-missing case and returns False (the v3 fix).
|
|
"""
|
|
|
|
def test_smoke_returns_false_when_av1an_missing(
|
|
self, v3_module, real_ffmpeg, tmp_path
|
|
):
|
|
"""If av1an is not installed, smoke test must return False.
|
|
|
|
This is the v3 fix for OTC-001. v1 would have returned True here
|
|
(masking the failure), causing every subsequent file to fail.
|
|
"""
|
|
if shutil.which("av1an"):
|
|
pytest.skip("av1an is installed — this test only runs when av1an is MISSING")
|
|
|
|
# Use a fake av1an path — the function will try to run it and fail
|
|
fake_av1an = "/usr/local/bin/av1an_does_not_exist"
|
|
result = v3_module._av1an_vsscript_smoke_test(
|
|
av1an_bin=fake_av1an,
|
|
ffmpeg_bin=real_ffmpeg,
|
|
av1an_flags={"worker": "--workers", "video_params": "--video-params",
|
|
"audio_params": "--audio-params"},
|
|
svt_name="svt_av1",
|
|
timeout=10,
|
|
)
|
|
|
|
ok, detail = result
|
|
# The smoke test should return False because av1an doesn't exist
|
|
assert ok is False, \
|
|
f"Smoke test should return False when av1an is missing, got True. Detail: {detail}"
|
|
# And the detail should mention the failure
|
|
assert any(marker in detail for marker in
|
|
("SMOKE_BIN_MISSING", "SMOKE_FAIL", "SMOKE_OS_ERROR",
|
|
"No such file", "not found")), \
|
|
f"Detail should mention the missing binary, got: {detail}"
|
|
|
|
|
|
class TestEnvironmentProbe:
|
|
"""Test probe_environment() against the real system."""
|
|
|
|
def test_probe_finds_real_ffmpeg(self, v3_module, real_ffmpeg):
|
|
"""probe_environment() must find the real ffmpeg on this system."""
|
|
env = v3_module.probe_environment()
|
|
assert env.ffmpeg_path is not None, "ffmpeg_path should be set"
|
|
assert "ffmpeg" in env.ffmpeg_path
|
|
# ffmpeg_version should be populated
|
|
assert env.ffmpeg_version is not None
|
|
assert len(env.ffmpeg_version) > 0
|
|
|
|
def test_probe_finds_real_ffprobe(self, v3_module, real_ffprobe):
|
|
"""probe_environment() must find the real ffprobe on this system."""
|
|
env = v3_module.probe_environment()
|
|
assert env.ffprobe_path is not None
|
|
assert "ffprobe" in env.ffprobe_path
|
|
|
|
def test_probe_detects_ffmpeg_libs(self, v3_module, real_ffmpeg):
|
|
"""probe_environment() must detect the codecs ffmpeg was built with."""
|
|
env = v3_module.probe_environment()
|
|
# This system has libsvtav1, libx265, libvpx, libopus (verified above)
|
|
assert env.ffmpeg_libs.get("libsvtav1", False), "libsvtav1 should be detected"
|
|
assert env.ffmpeg_libs.get("libx265", False), "libx265 should be detected"
|
|
assert env.ffmpeg_libs.get("libvpx", False), "libvpx should be detected"
|
|
assert env.ffmpeg_libs.get("libopus", False), "libopus should be detected"
|
|
|
|
def test_probe_distro_detection(self, v3_module):
|
|
"""probe_environment() must detect a distro family."""
|
|
env = v3_module.probe_environment()
|
|
# We're on Debian 14 (per the ffmpeg version string)
|
|
assert env.distro.family in ("debian", "arch", "redhat", "suse", "nixos", "unknown")
|
|
assert env.distro.name # not empty
|