""" v6 behavior tests — per-file av1an→ffmpeg fallback. Verifies that when av1an fails for a specific file (concat failure, scene-detection panic, or other per-file issue), the encoder automatically retries with the ffmpeg fallback path. """ import importlib.util import os import shutil import subprocess import sys from pathlib import Path from unittest.mock import MagicMock import pytest 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}") _install_pyside6_stubs() spec = importlib.util.spec_from_file_location("open_transcode_v3", str(V3_PATH)) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def _install_pyside6_stubs(): if any(name in sys.modules for name in ("PySide6", "PySide6.QtWidgets", "PySide6.QtCore", "PySide6.QtGui")): return import types pyside6 = types.ModuleType("PySide6") qt_widgets = types.ModuleType("PySide6.QtWidgets") qt_core = types.ModuleType("PySide6.QtCore") qt_gui = types.ModuleType("PySide6.QtGui") class _QThread: def __init__(self, *a, **kw): pass def start(self): pass def isRunning(self): return False def wait(self, ms=None): pass class _Signal: def __init__(self, *a, **kw): pass def connect(self, *a, **kw): pass def emit(self, *a, **kw): pass def _Slot(*a, **kw): def deco(fn): return fn return deco 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() class _QWidget: def __init__(self, *a, **kw): 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()) for n in ("QFont", "QPalette", "QColor", "QPainter", "QPen", "QBrush", "QRadialGradient", "QFontMetrics"): setattr(qt_gui, n, 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(): return _load_v3_module() class TestCanFfmpegFallback: """v6-01: _can_ffmpeg_fallback checks if ffmpeg has the encoder.""" def test_returns_true_when_ffmpeg_has_encoder(self, v3_module): """When ffmpeg_libs has the encoder, _can_ffmpeg_fallback returns True.""" worker = v3_module.EncoderWorker.__new__(v3_module.EncoderWorker) worker.video_codec = MagicMock() worker.video_codec.ffmpeg_encoder = "libsvtav1" worker.env = MagicMock() worker.env.ffmpeg_libs = {"libsvtav1": True, "libx265": True} assert worker._can_ffmpeg_fallback() is True def test_returns_false_when_ffmpeg_lacks_encoder(self, v3_module): """When ffmpeg_libs does NOT have the encoder, returns False.""" worker = v3_module.EncoderWorker.__new__(v3_module.EncoderWorker) worker.video_codec = MagicMock() worker.video_codec.ffmpeg_encoder = "libsvtav1" worker.env = MagicMock() worker.env.ffmpeg_libs = {"libsvtav1": False, "libx265": True} assert worker._can_ffmpeg_fallback() is False class TestErrorPatternsIncludeSplitScores: """v6-02: 'split scores is not empty' panic is in the error_patterns table.""" def test_split_scores_pattern_in_source(self, v3_module): """The error_patterns table should include the 'split scores' pattern.""" src = Path(V3_PATH).read_text() assert "split scores is not empty" in src, \ "error_patterns table should include 'split scores is not empty'" def test_summary_detection_in_source(self, v3_module): """v6-03: SUMMARY + Average Speed detection for concat failures.""" src = Path(V3_PATH).read_text() assert "SUMMARY" in src and "Average Speed" in src, \ "Should detect encoder SUMMARY block for concat failure diagnosis" class TestPerFileFallbackRetry: """v6-01: When av1an fails, retry with ffmpeg fallback.""" def test_av1an_failure_triggers_ffmpeg_retry(self, v3_module, tmp_path): """When av1an fails (non-systematic) and ffmpeg has the encoder, the code should retry with _ffmpeg_fallback_encode.""" # Create a real test video ffmpeg_bin = shutil.which("ffmpeg") if not ffmpeg_bin: pytest.skip("ffmpeg not available") test_video = tmp_path / "input.mp4" subprocess.run( [ffmpeg_bin, "-f", "lavfi", "-i", "testsrc=duration=1:size=64x64:rate=24", "-c:v", "libx264", "-y", str(test_video)], capture_output=True, timeout=15, ) if not test_video.exists(): pytest.skip("Could not generate test video") # Build a worker in av1an mode (not ffmpeg fallback) 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_res = next(r for r in v3_module.RESOLUTION_PRESETS if r.category == "original") env = v3_module.probe_environment() # Set fake av1an path + flags so the command builder doesn't crash env.av1an_path = "/usr/bin/av1an" env.av1an_flags = { "worker": "--workers", "video_params": "--video-params", "audio_params": "--audio-params", "svt_name": "svt-av1", "concat_method": "ffmpeg", } worker = v3_module.EncoderWorker( in_dir=tmp_path, out_dir=tmp_path / "output", video_codec=av1_codec, audio_profile=opus_audio, container=mkv_container, crf=32, preset_label="Fast (4)", delete_source=False, env=env, extensions={".mp4"}, resolution=original_res, use_ffmpeg_fallback=False, # av1an mode — will fail and retry ) # Mock _run_with_stop_check to simulate av1an failure # (return a SUMMARY block in stderr + non-zero exit code = concat failure) def mock_run(cmd, **kw): # Simulate av1an encoding all frames then failing at concat fake_stderr = ( "Encoding: 24/24 Frames @ 3.52 fps\n" "SUMMARY -----------------------------------------\n" "Total Frames\t\tFrame Rate\t\tByte Count\n" " 24\t\t23.98 fps\t\t 12345\n\n" "Average Speed:\t\t3.598 fps\n" "SvtMalloc[info]: you have no memory leak\n\n" "source pipe stderr:\n\n" "ffmpeg pipe stderr:\n\n" ) return ("ok", 1, "", fake_stderr) worker._run_with_stop_check = mock_run # Mock _ffmpeg_fallback_encode to simulate success def mock_ffmpeg_fallback(file_path, encode_input, output_f): # Create the output file so the verify step passes output_f.parent.mkdir(parents=True, exist_ok=True) output_f.write_bytes(b"\x00" * 1024) # 1KB fake output return True worker._ffmpeg_fallback_encode = mock_ffmpeg_fallback logs = [] worker.log_msg.emit = lambda msg: logs.append(msg) # Set up _current_temps and _file_res_map (needed by _process_one_file) worker._current_temps = [] worker._file_res_map = {} worker._stop = False worker._consecutive_fail_count = 0 worker._last_fail_pattern = None # Call _encode_one directly output_f = tmp_path / "output" / "input_archived.mkv" result = worker._encode_one(test_video, test_video, output_f, 1) # Should have retried with ffmpeg and succeeded assert result is True, f"Expected retry to succeed. Logs: {logs}" # Should have logged the RETRY message assert any("RETRY" in l and "ffmpeg fallback" in l for l in logs), \ f"Expected RETRY message in logs: {logs}" # Should have logged RETRY OK assert any("RETRY OK" in l for l in logs), \ f"Expected RETRY OK in logs: {logs}" # fail_count should NOT be incremented (retry succeeded) assert worker.fail_count == 0, \ f"fail_count should be 0 after successful retry, got {worker.fail_count}" def test_av1an_failure_no_retry_when_ffmpeg_lacks_encoder(self, v3_module, tmp_path): """When av1an fails and ffmpeg does NOT have the encoder, no retry should happen — just increment fail_count and return False.""" ffmpeg_bin = shutil.which("ffmpeg") if not ffmpeg_bin: pytest.skip("ffmpeg not available") test_video = tmp_path / "input.mp4" test_video.write_bytes(b"\x00" * 1024) # fake video 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_res = next(r for r in v3_module.RESOLUTION_PRESETS if r.category == "original") # Build env with libsvtav1=False — can't fallback env = v3_module.probe_environment() env.av1an_path = "/usr/bin/av1an" env.av1an_flags = { "worker": "--workers", "video_params": "--video-params", "audio_params": "--audio-params", "svt_name": "svt-av1", "concat_method": "ffmpeg", } env.ffmpeg_libs["libsvtav1"] = False worker = v3_module.EncoderWorker( in_dir=tmp_path, out_dir=tmp_path / "output", video_codec=av1_codec, audio_profile=opus_audio, container=mkv_container, crf=32, preset_label="Fast (4)", delete_source=False, env=env, extensions={".mp4"}, resolution=original_res, use_ffmpeg_fallback=False, ) # Mock av1an failure def mock_run(cmd, **kw): return ("ok", 1, "", "some av1an error") worker._run_with_stop_check = mock_run # Mock _ffmpeg_fallback_encode — should NOT be called worker._ffmpeg_fallback_encode = MagicMock(return_value=True) logs = [] worker.log_msg.emit = lambda msg: logs.append(msg) worker._current_temps = [] worker._stop = False output_f = tmp_path / "output" / "input_archived.mkv" result = worker._encode_one(test_video, test_video, output_f, 1) # Should fail (no retry possible) assert result is False assert worker.fail_count == 1 # _ffmpeg_fallback_encode should NOT have been called worker._ffmpeg_fallback_encode.assert_not_called() # Should NOT have logged RETRY assert not any("RETRY" in l for l in logs) def test_systematic_failure_does_not_retry(self, v3_module, tmp_path): """When av1an fails with a systematic issue (VSScript API), self._stop is set and no retry should happen.""" ffmpeg_bin = shutil.which("ffmpeg") if not ffmpeg_bin: pytest.skip("ffmpeg not available") test_video = tmp_path / "input.mp4" test_video.write_bytes(b"\x00" * 1024) 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_res = next(r for r in v3_module.RESOLUTION_PRESETS if r.category == "original") env = v3_module.probe_environment() env.av1an_path = "/usr/bin/av1an" env.av1an_flags = { "worker": "--workers", "video_params": "--video-params", "audio_params": "--audio-params", "svt_name": "svt-av1", "concat_method": "ffmpeg", } worker = v3_module.EncoderWorker( in_dir=tmp_path, out_dir=tmp_path / "output", video_codec=av1_codec, audio_profile=opus_audio, container=mkv_container, crf=32, preset_label="Fast (4)", delete_source=False, env=env, extensions={".mp4"}, resolution=original_res, use_ffmpeg_fallback=False, ) # Mock av1an VSScript failure — this sets self._stop = True def mock_run(cmd, **kw): return ("ok", 1, "", "Failed to get VSScript API") worker._run_with_stop_check = mock_run worker._ffmpeg_fallback_encode = MagicMock(return_value=True) logs = [] worker.log_msg.emit = lambda msg: logs.append(msg) worker._current_temps = [] worker._stop = False # will be set by the pattern matcher output_f = tmp_path / "output" / "input_archived.mkv" result = worker._encode_one(test_video, test_video, output_f, 1) # Should fail — systematic issue, no retry assert result is False assert worker._stop is True, "VSScript failure should set _stop" # _ffmpeg_fallback_encode should NOT have been called (self._stop is True) worker._ffmpeg_fallback_encode.assert_not_called() # Should NOT have logged RETRY assert not any("RETRY" in l for l in logs)