You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
124 lines
4.4 KiB
124 lines
4.4 KiB
import json |
|
import subprocess |
|
from pathlib import Path |
|
|
|
import pytest |
|
|
|
from voice_transcriptor.services.media_probe import ( |
|
InvalidMediaPathError, |
|
MediaProbeError, |
|
MediaProbeService, |
|
ProbeExecutionError, |
|
ProbeOutputError, |
|
ProbeTimeoutError, |
|
detect_tools, |
|
parse_probe_output, |
|
validate_media_path, |
|
) |
|
|
|
|
|
def test_detects_ffmpeg_and_ffprobe_executables(monkeypatch: pytest.MonkeyPatch) -> None: |
|
discovered = {"ffmpeg": r"C:\\tools\\ffmpeg.exe", "ffprobe": r"C:\\tools\\ffprobe.exe"} |
|
monkeypatch.setattr("voice_transcriptor.services.media_probe.shutil.which", discovered.get) |
|
|
|
status = detect_tools() |
|
|
|
assert status.ffmpeg_path == Path(r"C:\tools\ffmpeg.exe") |
|
assert status.ffprobe_path == Path(r"C:\tools\ffprobe.exe") |
|
|
|
|
|
def test_rejects_directory_path(tmp_path: Path) -> None: |
|
with pytest.raises(InvalidMediaPathError, match="not a file"): |
|
validate_media_path(tmp_path) |
|
|
|
|
|
def test_parses_audio_only_media(tmp_path: Path) -> None: |
|
media = tmp_path / "memo.m4a" |
|
payload = json.dumps({ |
|
"format": {"duration": "4.5"}, |
|
"streams": [{"codec_type": "audio", "codec_name": "aac"}], |
|
}) |
|
|
|
info = parse_probe_output(media, 3, payload) |
|
|
|
assert info.duration_seconds == 4.5 |
|
assert info.audio_codec == "aac" |
|
assert info.duration_text == "4.5" |
|
assert info.has_audio is True |
|
assert info.has_video is False |
|
|
|
|
|
def test_parses_video_with_audio(tmp_path: Path) -> None: |
|
media = tmp_path / "meeting.mov" |
|
media.write_bytes(b"x" * 12) |
|
payload = json.dumps({ |
|
"format": {"duration": "3661.25"}, |
|
"streams": [{"codec_type": "video", "codec_name": "h264"}, {"codec_type": "audio", "codec_name": "aac"}], |
|
}) |
|
info = parse_probe_output(media, 12, payload) |
|
assert info.duration_seconds == 3661.25 |
|
assert info.audio_codec == "aac" |
|
assert info.has_video is True |
|
|
|
|
|
def test_missing_metadata_is_unknown(tmp_path: Path) -> None: |
|
media = tmp_path / "memo.m4a" |
|
info = parse_probe_output(media, 0, '{"format": {}, "streams": []}') |
|
assert info.duration_seconds is None |
|
assert info.audio_codec is None |
|
|
|
|
|
def test_rejects_malformed_probe_json(tmp_path: Path) -> None: |
|
with pytest.raises(MediaProbeError, match="invalid data"): |
|
parse_probe_output(tmp_path / "bad.mov", 0, "not json") |
|
|
|
|
|
@pytest.mark.parametrize("payload", ["[]", '{"format": []}', '{"streams": [null]}']) |
|
def test_rejects_structurally_invalid_probe_json(tmp_path: Path, payload: str) -> None: |
|
with pytest.raises(ProbeOutputError, match="invalid data"): |
|
parse_probe_output(tmp_path / "bad.mov", 0, payload) |
|
|
|
|
|
def test_rejects_missing_file(tmp_path: Path) -> None: |
|
with pytest.raises(MediaProbeError, match="does not exist"): |
|
validate_media_path(tmp_path / "missing.mov") |
|
|
|
|
|
def test_maps_nonzero_ffprobe_result(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: |
|
media = tmp_path / "bad.mov" |
|
media.write_bytes(b"bad") |
|
monkeypatch.setattr(subprocess, "run", lambda *a, **k: subprocess.CompletedProcess(a, 1, "", "invalid input")) |
|
with pytest.raises(MediaProbeError, match="could not inspect"): |
|
MediaProbeService(Path("ffprobe")).probe(media) |
|
|
|
|
|
def test_maps_ffprobe_timeout(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: |
|
media = tmp_path / "slow.mov" |
|
media.write_bytes(b"slow") |
|
def timeout(*args: object, **kwargs: object) -> None: |
|
raise subprocess.TimeoutExpired("ffprobe", 30) |
|
monkeypatch.setattr(subprocess, "run", timeout) |
|
with pytest.raises(MediaProbeError, match="timed out"): |
|
MediaProbeService(Path("ffprobe")).probe(media) |
|
|
|
|
|
def test_maps_probe_failures_to_typed_errors(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: |
|
missing = tmp_path / "missing.mov" |
|
with pytest.raises(InvalidMediaPathError): |
|
validate_media_path(missing) |
|
|
|
with pytest.raises(ProbeOutputError): |
|
parse_probe_output(tmp_path / "bad.mov", 0, "not json") |
|
|
|
media = tmp_path / "bad.mov" |
|
media.write_bytes(b"bad") |
|
monkeypatch.setattr(subprocess, "run", lambda *a, **k: subprocess.CompletedProcess(a, 1, "", "invalid input")) |
|
with pytest.raises(ProbeExecutionError): |
|
MediaProbeService(Path("ffprobe")).probe(media) |
|
|
|
def timeout(*args: object, **kwargs: object) -> None: |
|
raise subprocess.TimeoutExpired("ffprobe", 30) |
|
|
|
monkeypatch.setattr(subprocess, "run", timeout) |
|
with pytest.raises(ProbeTimeoutError): |
|
MediaProbeService(Path("ffprobe")).probe(media)
|
|
|