- validate managed binaries before exec; 0-byte GGUF placeholders fail actionably instead of "Exec format error" (#1172) - KittenTTS: tokenizer-measured chunking to the ONNX 512-token cap; clear 400 for unspeakable input (#1173) - clean SIGTERM during weight load: shutdown-aware loader, benign cancelled-load classification, lifespan hardening, scoped log silencers (transformers load + alembic fileConfig) (#1174) - broken ASR deep-imports (lightning_fabric) mark the engine unavailable with a repair hint and fall through (#1185) - uv cache + managed Python follow the chosen install drive on Windows (cherry-picked cross-drive class fix + spaces/D: tests) (#1186) - adaptive silence-removal ladder for quiet clone references; localized actionable error for truly silent clips, all 21 locales (#1188) - CHANGELOG: consolidated Unreleased into the quiet one-liner style Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
174 lines
5.9 KiB
Python
174 lines
5.9 KiB
Python
"""Tests for backend/services/ffmpeg_utils.py — Phase 1 Wave 3 (issue #76).
|
|
|
|
Covers :func:`resolve_ffprobe`'s env-first / PATH-fallback cascade and the
|
|
legacy-name alias (``FFPROBE_PATH`` still works, ``OMNIVOICE_FFPROBE_PATH``
|
|
takes precedence).
|
|
"""
|
|
import os
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_env(monkeypatch):
|
|
"""Strip both ffprobe env vars before each test so we control the cascade."""
|
|
monkeypatch.delenv("OMNIVOICE_FFPROBE_PATH", raising=False)
|
|
monkeypatch.delenv("FFPROBE_PATH", raising=False)
|
|
|
|
|
|
def test_resolve_ffprobe_prefers_omnivoice_env_var(monkeypatch, tmp_path):
|
|
"""OMNIVOICE_FFPROBE_PATH set to a real file → that path wins."""
|
|
from services import ffmpeg_utils
|
|
|
|
fake = tmp_path / "ffprobe"
|
|
fake.write_text("#!/bin/sh\necho 1\n")
|
|
fake.chmod(0o755)
|
|
monkeypatch.setenv("OMNIVOICE_FFPROBE_PATH", str(fake))
|
|
|
|
assert ffmpeg_utils.resolve_ffprobe() == str(fake)
|
|
|
|
|
|
def test_resolve_ffprobe_falls_back_to_legacy_FFPROBE_PATH(monkeypatch, tmp_path):
|
|
"""OMNIVOICE_FFPROBE_PATH absent but legacy FFPROBE_PATH set → legacy used."""
|
|
from services import ffmpeg_utils
|
|
|
|
fake = tmp_path / "ffprobe-legacy"
|
|
fake.write_text("#!/bin/sh\necho 1\n")
|
|
fake.chmod(0o755)
|
|
monkeypatch.setenv("FFPROBE_PATH", str(fake))
|
|
|
|
assert ffmpeg_utils.resolve_ffprobe() == str(fake)
|
|
|
|
|
|
def test_resolve_ffprobe_omnivoice_path_takes_precedence_over_legacy(
|
|
monkeypatch, tmp_path,
|
|
):
|
|
"""Both env vars set → OMNIVOICE_FFPROBE_PATH wins."""
|
|
from services import ffmpeg_utils
|
|
|
|
canonical = tmp_path / "ffprobe-canonical"
|
|
legacy = tmp_path / "ffprobe-legacy"
|
|
for f in (canonical, legacy):
|
|
f.write_text("#!/bin/sh\necho 1\n")
|
|
f.chmod(0o755)
|
|
|
|
monkeypatch.setenv("OMNIVOICE_FFPROBE_PATH", str(canonical))
|
|
monkeypatch.setenv("FFPROBE_PATH", str(legacy))
|
|
|
|
assert ffmpeg_utils.resolve_ffprobe() == str(canonical)
|
|
|
|
|
|
def test_resolve_ffprobe_falls_back_to_PATH(monkeypatch, tmp_path):
|
|
"""No env var → shutil.which result is returned."""
|
|
from services import ffmpeg_utils
|
|
|
|
# Stub shutil.which inside the module so we do not depend on the host PATH.
|
|
fake = "/fake/path/from/system/ffprobe"
|
|
|
|
def _fake_which(name):
|
|
if name == "ffprobe":
|
|
return fake
|
|
return None
|
|
|
|
monkeypatch.setattr(ffmpeg_utils, "shutil", _ShutilStub(_fake_which))
|
|
# The fake path isn't a real binary — bypass the runnability probe here
|
|
# (rejection behavior has its own test below).
|
|
monkeypatch.setattr(ffmpeg_utils, "_binary_runs", lambda _p: True)
|
|
assert ffmpeg_utils.resolve_ffprobe() == fake
|
|
|
|
|
|
def test_resolve_ffprobe_rejects_non_runnable_candidate(monkeypatch, tmp_path):
|
|
"""#360/#361/#362: a candidate that exists but cannot execute (corrupt /
|
|
wrong-arch binary → WinError 193 on Windows) is skipped and the cascade
|
|
falls through to the next runnable source."""
|
|
from services import ffmpeg_utils
|
|
|
|
broken = tmp_path / "ffprobe-broken"
|
|
broken.write_bytes(b"\x00\x01not-a-binary")
|
|
broken.chmod(0o755)
|
|
good = tmp_path / "ffprobe-good"
|
|
good.write_text("#!/bin/sh\necho 1\n")
|
|
good.chmod(0o755)
|
|
|
|
monkeypatch.setenv("OMNIVOICE_FFPROBE_PATH", str(broken))
|
|
monkeypatch.setattr(
|
|
ffmpeg_utils, "shutil",
|
|
_ShutilStub(lambda name: str(good) if name == "ffprobe" else None),
|
|
)
|
|
ffmpeg_utils._BINARY_OK.clear()
|
|
|
|
assert ffmpeg_utils.resolve_ffprobe() == str(good)
|
|
|
|
|
|
def test_resolve_ffprobe_returns_None_when_nothing_resolves(monkeypatch):
|
|
"""No env, no PATH → returns None (no crash)."""
|
|
from services import ffmpeg_utils
|
|
|
|
monkeypatch.setattr(ffmpeg_utils, "shutil", _ShutilStub(lambda _name: None))
|
|
assert ffmpeg_utils.resolve_ffprobe() is None
|
|
|
|
|
|
def test_resolve_ffprobe_env_var_with_command_name_resolves_via_which(
|
|
monkeypatch, tmp_path,
|
|
):
|
|
"""Legacy shape: env var set to a bare command name (not a path) — resolve
|
|
via shutil.which so older Tauri shells that set FFPROBE_PATH=ffprobe still
|
|
work."""
|
|
from services import ffmpeg_utils
|
|
|
|
fake = tmp_path / "ffprobe"
|
|
fake.write_text("#!/bin/sh\necho 1\n")
|
|
fake.chmod(0o755)
|
|
|
|
def _fake_which(name):
|
|
if name == "ffprobe":
|
|
return str(fake)
|
|
return None
|
|
|
|
monkeypatch.setattr(ffmpeg_utils, "shutil", _ShutilStub(_fake_which))
|
|
monkeypatch.setenv("OMNIVOICE_FFPROBE_PATH", "ffprobe")
|
|
|
|
assert ffmpeg_utils.resolve_ffprobe() == str(fake)
|
|
|
|
|
|
class _ShutilStub:
|
|
"""A tiny shim that mimics the parts of shutil ffmpeg_utils touches.
|
|
|
|
monkeypatching the whole module avoids pulling in real PATH lookups while
|
|
keeping the existing `import shutil` call sites untouched.
|
|
"""
|
|
|
|
def __init__(self, which_impl):
|
|
self._which = which_impl
|
|
|
|
def which(self, name):
|
|
return self._which(name)
|
|
|
|
|
|
# ── windows_tool_candidates: no hardcoded system-drive assumptions ─────────
|
|
|
|
|
|
def test_windows_tool_candidates_empty_off_windows(monkeypatch):
|
|
from services import ffmpeg_utils
|
|
|
|
monkeypatch.setattr(ffmpeg_utils.os, "name", "posix")
|
|
assert ffmpeg_utils.windows_tool_candidates("ffmpeg") == []
|
|
|
|
|
|
def test_windows_tool_candidates_derive_from_environment(monkeypatch):
|
|
"""Program Files / the system drive can live on a non-C: drive — the
|
|
probe list must follow the environment instead of assuming C: (the
|
|
cross-drive install class)."""
|
|
from services import ffmpeg_utils
|
|
|
|
monkeypatch.setattr(ffmpeg_utils.os, "name", "nt")
|
|
monkeypatch.setenv("SystemDrive", "E:")
|
|
monkeypatch.setenv("ProgramFiles", "E:\\Program Files")
|
|
monkeypatch.setenv("ProgramW6432", "E:\\Program Files")
|
|
|
|
got = ffmpeg_utils.windows_tool_candidates("ffprobe")
|
|
assert "E:\\ffmpeg\\bin\\ffprobe.exe" in got # relocated system drive
|
|
assert any(p.startswith("E:\\Program Files") for p in got) # env-derived Program Files
|
|
assert "C:\\ffmpeg\\bin\\ffprobe.exe" in got # conventional fallbacks kept
|
|
assert "D:\\ffmpeg\\bin\\ffprobe.exe" in got
|