"""Theora/OGV target tests — v4.9.0. Covers the three pieces of the OGV scenario: 1. The quality-scale wrapper (``theora_quality_from_crf``) — libtheora grades quality with ``-q:v 0..31`` where HIGHER is better, the exact opposite of the shared CRF knob. The wrapper must invert linearly, clamp its input, and stay inside ``THEORA_Q_RANGE``. 2. The ``Theora (OGV)`` VideoCodecProfile + ``OGV (Ogg)`` container — ffmpeg-only flag, no GPU family, ffprobe codec name for skip-existing, and encoder args that use ``-q:v`` (never ``-crf``). 3. The UI compatibility rules — Theora is INCOMPATIBLE with MP4/WebM, valid-but-unusual in MKV (WARNING), the OGV container is INCOMPATIBLE with every other video codec, and IAMF audio stays INCOMPATIBLE in OGV via the existing non-MP4 rule. Empirical grounding (ffmpeg 7.x, libtheora): bitrate rises monotonically with -q:v and saturates from ~31; the mp4 muxer rejects Theora; the webm muxer only accepts VP8/VP9/AV1; the ogg muxer only accepts Theora video; Opus, Vorbis and FLAC all mux fine into Ogg. No PySide6 required: profile/wrapper tests import ``codec_profiles`` directly (pure data + pure functions); the compat-rule tests reuse the ``__new__``-mock pattern from test_container_compat.py, which works with the conftest PySide6 stubs. """ from __future__ import annotations from unittest.mock import MagicMock import pytest from conftest import load_program_module _m = load_program_module() CONTAINER_PROFILES = _m.CONTAINER_PROFILES FFMPEG_LIB_KEY_MAP = _m.FFMPEG_LIB_KEY_MAP THEORA_Q_RANGE = _m.THEORA_Q_RANGE VIDEO_CODECS = _m.VIDEO_CODECS theora_quality_from_crf = _m.theora_quality_from_crf # ───────────────────────────────────────────────────────────────────────────── # Lookups — the Theora profile and OGV container, found by VALUE so the # tests don't depend on list positions (the combos index into the same # lists, but the tables are free to grow). # ───────────────────────────────────────────────────────────────────────────── def _theora_profile(): matches = [vc for vc in VIDEO_CODECS if vc.ffmpeg_encoder == "libtheora"] assert matches, "VIDEO_CODECS must contain a Theora profile" return matches[0] def _container_exts(): return {cp.ext for cp in CONTAINER_PROFILES} # ───────────────────────────────────────────────────────────────────────────── # 1. The quality-scale wrapper # ───────────────────────────────────────────────────────────────────────────── class TestTheoraQualityWrapper: def test_best_knob_position_maps_to_best_q(self): # Knob 18 = the knob's best → q 31 = libtheora's best (inverted!) assert theora_quality_from_crf(18) == THEORA_Q_RANGE[1] == 31 def test_worst_knob_position_maps_to_worst_q(self): assert theora_quality_from_crf(52) == THEORA_Q_RANGE[0] == 0 def test_default_knob_position(self): # The 4.8.2 default knob value (26) lands at q 24 — high-quality # archival territory on the 0..31 scale. assert theora_quality_from_crf(26) == 24 def test_monotonic_non_increasing_over_all_integers(self): # Turning the knob toward WORSE quality must never RAISE q # (per-integer steps can repeat a value: 34 knob integers map # onto 32 q values). values = [theora_quality_from_crf(crf) for crf in range(18, 53)] assert all(a >= b for a, b in zip(values, values[1:])), values def test_strictly_decreasing_per_knob_tick(self): # The knob snaps to every-2 steps (snap_ticks over 18..52); each # full user-visible tick must strictly drop q (~1.8 per tick). ticks = [theora_quality_from_crf(crf) for crf in range(18, 53, 2)] assert all(a > b for a, b in zip(ticks, ticks[1:])), ticks def test_output_stays_inside_q_scale(self): for crf in range(-10, 80): # far outside the knob range q = theora_quality_from_crf(crf) assert THEORA_Q_RANGE[0] <= q <= THEORA_Q_RANGE[1], (crf, q) def test_custom_knob_range_is_respected(self): # The wrapper takes the range as parameters so a future profile # with a different crf_range can reuse it. assert theora_quality_from_crf(10, crf_lo=10, crf_hi=20) == 31 assert theora_quality_from_crf(20, crf_lo=10, crf_hi=20) == 0 # ───────────────────────────────────────────────────────────────────────────── # 2. Profile / container invariants # ───────────────────────────────────────────────────────────────────────────── class TestTheoraProfile: def test_profile_is_ffmpeg_only(self): vc = _theora_profile() assert vc.ffmpeg_only is True assert vc.av1an_encoder == "" def test_params_fn_raises_loudly(self): # The av1an path can never legitimately build Theora params; the # guard turns a would-be opaque av1an CLI failure into a clear # RuntimeError naming the fix. with pytest.raises(RuntimeError, match="ffmpeg_only"): _theora_profile().params_fn(26, 0) def test_ffmpeg_args_use_qscale_not_crf(self): args = _theora_profile().ffmpeg_vargs_fn(26, 1) assert args[0:2] == ["-c:v", "libtheora"] assert "-q:v" in args q_pos = args.index("-q:v") assert args[q_pos + 1] == "24" # 4.8.2 default → q 24 assert "-crf" not in args # libtheora has no CRF assert "-pix_fmt" in args assert args[args.index("-pix_fmt") + 1] == "yuv420p" # 4:2:0-only codec def test_preset_maps_to_speed_level_and_clamps(self): vc = _theora_profile() for label, level in vc.preset_map.items(): args = vc.ffmpeg_vargs_fn(26, level) assert args[args.index("-speed_level") + 1] == str(level) # Out-of-range preset values (e.g. the EncoderWorker fallback 6) # clamp to libtheora's maximum speed level 2 instead of erroring. clamped = vc.ffmpeg_vargs_fn(26, 6) assert clamped[clamped.index("-speed_level") + 1] == "2" def test_skip_existing_codec_name(self): assert _theora_profile().ffprobe_codec_name == "theora" def test_no_gpu_family(self): # No hardware Theora encoder exists on any API — resolve_gpu_ # encoder() must land on CPU for every engine selection. assert _theora_profile().gpu_family == "" assert _theora_profile().gpu_encoder == "" def test_ogv_container_exists_and_is_default(self): vc = _theora_profile() assert vc.container == "ogv" assert "ogv" in _container_exts() def test_lib_key_map_covers_theora(self): # _disable_unavailable_codecs and the ffmpeg-fallback gate both # look the encoder up through FFMPEG_LIB_KEY_MAP — a missing # entry would silently report the codec as unavailable forever. assert FFMPEG_LIB_KEY_MAP["libtheora"] == "libtheora" # ───────────────────────────────────────────────────────────────────────────── # 3. UI compatibility rules (same __new__-mock pattern as # test_container_compat.py — works with or without real PySide6) # ───────────────────────────────────────────────────────────────────────────── def _make_master_for_compat(codec_idx, audio_idx, container_idx): OpenCodecMaster = load_program_module().OpenCodecMaster master = OpenCodecMaster.__new__(OpenCodecMaster) codec_combo = MagicMock() codec_combo.currentIndex.return_value = codec_idx audio_combo = MagicMock() audio_combo.currentIndex.return_value = audio_idx container_combo = MagicMock() container_combo.currentIndex.return_value = container_idx master.codec_combo = codec_combo master.audio_combo = audio_combo master.container_combo = container_combo logged: list[str] = [] master._log = lambda msg: logged.append(msg) return master, logged def _codec_idx(encoder: str) -> int: for i, vc in enumerate(VIDEO_CODECS): if vc.ffmpeg_encoder == encoder: return i raise AssertionError(f"no profile with ffmpeg_encoder={encoder!r}") def _container_idx(ext: str) -> int: for i, cp in enumerate(CONTAINER_PROFILES): if cp.ext == ext: return i raise AssertionError(f"no container with ext={ext!r}") def _audio_idx(encoder: str) -> int: AUDIO_PROFILES = load_program_module().AUDIO_PROFILES for i, ap in enumerate(AUDIO_PROFILES): if ap.ffmpeg_encoder_name == encoder: return i raise AssertionError(f"no audio profile {encoder!r}") class TestTheoraCompatRules: def test_theora_in_webm_incompatible(self): master, _ = _make_master_for_compat( _codec_idx("libtheora"), _audio_idx("libopus"), _container_idx("webm"), ) warnings = master._check_combo_compatibility() assert any(w.startswith("INCOMPATIBLE:") and "Theora" in w for w in warnings), warnings def test_theora_in_mp4_incompatible(self): master, _ = _make_master_for_compat( _codec_idx("libtheora"), _audio_idx("libopus"), _container_idx("mp4"), ) warnings = master._check_combo_compatibility() assert any(w.startswith("INCOMPATIBLE:") and "Theora" in w for w in warnings), warnings def test_theora_in_mkv_is_warning_not_block(self): # MKV really can hold Theora (verified with a live encode), so it # must NOT hard-block — just advise OGV as the canonical pairing. master, _ = _make_master_for_compat( _codec_idx("libtheora"), _audio_idx("libvorbis"), _container_idx("mkv"), ) warnings = master._check_combo_compatibility() assert any(w.startswith("WARNING:") and "Theora" in w for w in warnings), warnings assert not any(w.startswith("INCOMPATIBLE:") for w in warnings) def test_theora_in_ogv_is_clean(self): # The canonical pairing (with Ogg-native audio) fires no rule. master, _ = _make_master_for_compat( _codec_idx("libtheora"), _audio_idx("libvorbis"), _container_idx("ogv"), ) assert master._check_combo_compatibility() == [] def test_theora_flac_opus_in_ogv_are_clean(self): # Vorbis, Opus and FLAC are all Ogg-standard audio codecs. for audio in ("libopus", "flac"): master, _ = _make_master_for_compat( _codec_idx("libtheora"), _audio_idx(audio), _container_idx("ogv"), ) assert master._check_combo_compatibility() == [], audio @pytest.mark.parametrize("encoder", ["libsvtav1", "libvpx-vp9", "libx265"]) def test_non_theora_in_ogv_incompatible(self, encoder): # The Ogg muxer rejects every other video codec — the user must # switch to Theora or a different container. master, _ = _make_master_for_compat( _codec_idx(encoder), _audio_idx("libopus"), _container_idx("ogv"), ) warnings = master._check_combo_compatibility() assert any(w.startswith("INCOMPATIBLE:") and "Ogg" in w for w in warnings), (encoder, warnings) def test_iamf_in_ogv_incompatible_via_existing_rule(self): # IAMF requires MP4; the pre-existing non-MP4 rule covers OGV # with no Theora-specific additions. master, _ = _make_master_for_compat( _codec_idx("libtheora"), _audio_idx("libiamf"), _container_idx("ogv"), ) warnings = master._check_combo_compatibility() assert any(w.startswith("INCOMPATIBLE:") and "IAMF" in w for w in warnings), warnings