tests/test_media.py
Rama master · réplica de lectura del repositorio original.
import os
import subprocess
import tempfile
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
import peach.media as media_module
from peach.ffmpeg import BinaryChoice, FFmpegResolver, binary_diagnostics
from peach.media import (
FilesystemBackend, MediaEngine, MediaUnavailable, normalized_path,
resolve_case_insensitive,
)
from peach.repository import MediaAsset
class MediaEngineTests(unittest.TestCase):
def test_tool_diagnostics_distinguishes_software_av1_and_hardware_only(self):
for decoder, available in (('av1', False), ('libdav1d', True)):
results = [subprocess.CompletedProcess([], 0, b'ffmpeg version test\n', b''),
subprocess.CompletedProcess([], 0, f' V..... {decoder} AV1 decoder\n'.encode(), b'')]
with patch('peach.ffmpeg.subprocess.run', side_effect=results):
result = binary_diagnostics(BinaryChoice(Path('ffmpeg'), 'test'), decoders=True)
self.assertTrue(result['usable'])
self.assertEqual(result['av1_software'], available)
def test_tool_diagnostics_probe_each_binary_file_once_until_it_changes(self):
with tempfile.TemporaryDirectory() as tmp:
binary = Path(tmp).resolve() / 'ffmpeg.exe'
binary.write_bytes(b'one')
choice = BinaryChoice(binary, 'test')
version = subprocess.CompletedProcess([], 0, b'ffmpeg version test\n', b'')
with patch('peach.ffmpeg.subprocess.run', return_value=version) as run:
self.assertTrue(binary_diagnostics(choice)['usable'])
self.assertTrue(binary_diagnostics(choice)['usable'])
self.assertEqual(run.call_count, 1)
binary.write_bytes(b'upgraded')
self.assertTrue(binary_diagnostics(choice)['usable'])
self.assertEqual(run.call_count, 2)
with patch('peach.ffmpeg.subprocess.run', side_effect=subprocess.TimeoutExpired('ffmpeg', 5)) as run:
self.assertFalse(binary_diagnostics(BinaryChoice(binary, 'other'))['usable'])
self.assertFalse(binary_diagnostics(BinaryChoice(binary, 'other'))['usable'])
self.assertEqual(run.call_count, 2)
def test_tool_diagnostics_reports_execution_failure(self):
with patch('peach.ffmpeg.subprocess.run', side_effect=subprocess.TimeoutExpired('ffmpeg', 5)):
result = binary_diagnostics(BinaryChoice(Path('ffmpeg'), 'test'), decoders=True)
self.assertTrue(result['available'])
self.assertFalse(result['usable'])
self.assertEqual(result['error'], 'TimeoutExpired')
# 临时目录一律先 resolve 再喂给被测代码。媒体解析本身就以 resolve 后的真实路径
# 判断是否越过根目录,而 CI runner 的临时目录都是别名:macOS 的 /var 是
# /private/var 的软链,Windows runner 的 %TEMP% 是 C:\Users\RUNNER~1 这种 8.3
# 短名,展开后是 runneradmin。拿未 resolve 的路径做断言,本机全绿、CI 全红。
def test_filesystem_backend_enforces_root(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve()
inside = root / "media" / "one.mp4"
outside = root.parent / "outside.mp4"
inside.parent.mkdir()
inside.write_bytes(b"test")
backend = FilesystemBackend([root], root / "snapshots")
self.assertEqual(backend.file_for(MediaAsset(1, str(inside), None)), inside)
with self.assertRaises(MediaUnavailable):
backend.file_for(MediaAsset(2, str(outside), None))
def test_engine_resolves_through_the_filesystem_only(self):
"""媒体解析没有第二条来源。
2026-09-01 关掉 Stash adapter 之前,这里还有一层 backend 契约。它在生产里
一次都没有生效过:`media_binding` 表是空的,`external_id("stash")` 永远返回
None,而 `stream_candidates` 本身没有任何调用方。见 ADR-0021。
"""
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve()
media = root / "one.mp4"
media.write_bytes(b"test")
repository = unittest.mock.Mock()
repository.media_asset.return_value = MediaAsset(1, str(media), None)
engine = MediaEngine(repository, FilesystemBackend([root], root / "snapshots"))
self.assertEqual(engine.file_for(1), media)
def test_engine_raises_for_unknown_asset(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
repository = unittest.mock.Mock()
repository.media_asset.return_value = None
engine = MediaEngine(
repository,
FilesystemBackend([root], root / "snapshots"),
)
with self.assertRaises(Exception) as raised:
engine.file_for(404)
self.assertEqual(type(raised.exception).__name__, "MediaNotFound")
def test_ffmpeg_resolver_prefers_explicit_environment(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
binary = root / "ffmpeg.exe"
binary.write_bytes(b"test")
resolver = FFmpegResolver(root)
with patch.dict("os.environ", {"PEACH_FFMPEG": str(binary)}, clear=False):
choice = resolver.ffmpeg()
self.assertEqual(choice.source, "environment")
def test_ffmpeg_resolver_uses_managed_bundle(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
binary = root / "bin" / ("ffmpeg.exe" if os.name == "nt" else "ffmpeg")
binary.parent.mkdir()
binary.write_bytes(b"test")
with patch.dict("os.environ", {"PEACH_FFMPEG": ""}, clear=False), \
patch("peach.ffmpeg.shutil.which", return_value=None):
choice = FFmpegResolver(root).ffmpeg()
self.assertEqual(choice.source, "peach-managed")
def test_frozen_application_uses_both_bundled_tools_without_path(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve()
bundle = root / "app" / "tools" / "ffmpeg" / "bin"
bundle.mkdir(parents=True)
suffix = ".exe" if os.name == "nt" else ""
for name in ("ffmpeg", "ffprobe"):
(bundle / (name + suffix)).write_bytes(b"tool")
with patch.object(sys, "frozen", True, create=True), \
patch.object(sys, "executable", str(root / "app" / "Peach.exe")), \
patch.dict(os.environ, {"PEACH_FFMPEG": "", "PEACH_FFPROBE": ""}), \
patch("peach.ffmpeg.shutil.which", return_value=None):
resolver = FFmpegResolver(root / "data")
for choice in (resolver.ffmpeg(), resolver.ffprobe()):
self.assertEqual(choice.source, "peach-bundled")
self.assertEqual(choice.path.parent, bundle)
def test_source_environment_does_not_use_tools_beside_python(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve()
tool = root / "tools" / "ffmpeg" / "bin" / ("ffmpeg.exe" if os.name == "nt" else "ffmpeg")
tool.parent.mkdir(parents=True)
tool.write_bytes(b"tool")
with patch.object(sys, "frozen", False, create=True), \
patch.object(sys, "executable", str(root / "python.exe")), \
patch.dict(os.environ, {"PEACH_FFMPEG": ""}), \
patch("peach.ffmpeg.shutil.which", return_value=None):
self.assertIsNone(FFmpegResolver(root / "data").ffmpeg())
def test_offline_windows_root_does_not_block_startup(self):
with patch.object(Path, "resolve", side_effect=OSError("offline")):
path = normalized_path(Path("B:/"))
self.assertTrue(path.is_absolute())
@unittest.skipUnless(os.name == "nt", "只有 Windows 的盘符路径跳过 resolve")
def test_windows_ledger_paths_are_not_resolved_through_the_redirector(self):
"""PikPak 的 A: 是 WinFsp 网络驱动器:`resolve()` 一条文件路径要 7 秒,还换成 UNC 形态,
之后每次 stat 再花 7 到 14 秒;盘符路径本身就是绝对路径,只需要折掉 `..`。"""
with patch.object(Path, "resolve", side_effect=AssertionError("resolve() 走了网络重定向器")):
path = normalized_path(r"A:\番号\..\番号\clip.mp4")
self.assertEqual(path, Path(r"A:\番号\clip.mp4"))
def test_filesystem_backend_matches_case_insensitively_on_sensitive_mounts(self):
"""CloudDrive 大小写敏感:账本 `abw-118.mp4` 对磁盘 `ABW-118.mp4` 必须救回。"""
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
real = root / "ABW-118.MP4"
real.write_bytes(b"test")
backend = FilesystemBackend([root], root / "snapshots")
opened = backend.file_for(MediaAsset(1, str(root / "abw-118.mp4"), None))
self.assertTrue(opened.is_file())
self.assertEqual(opened.name.casefold(), "abw-118.mp4")
# 完全缺失的名字仍应拒绝
with self.assertRaises(MediaUnavailable):
backend.file_for(MediaAsset(2, str(root / "nope.mp4"), None))
def test_batch_path_resolution_translates_ledger_drive_before_case_matching(self):
"""sheets/probe 不经过 FilesystemBackend,也必须先翻译账本盘符。"""
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
real = root / "ABW-118.MP4"
real.write_bytes(b"test")
with patch(
"peach.media.translate_ledger_path",
return_value=root / "abw-118.mp4",
):
opened = Path(resolve_case_insensitive(r"A:\\ABW-118.mp4"))
self.assertTrue(opened.is_file())
self.assertTrue(opened.parent.samefile(root))
def test_case_insensitive_miss_expires_after_the_short_ttl(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
with patch("peach.media.monotonic", return_value=0):
self.assertIsNone(
media_module._case_insensitive_match(str(root), "abw-118.mp4")
)
real = root / "ABW-118.MP4"
real.write_bytes(b"test")
with patch("peach.media.monotonic", return_value=6):
self.assertEqual(
media_module._case_insensitive_match(str(root), "abw-118.mp4"),
real.name,
)
def test_remote_mp4_defaults_to_range_and_only_segments_on_request(self):
"""默认 HLS 会让 HEVC 静默黑屏:`-c copy` 把 HEVC 装进 MPEG-TS,而 Chromium 的
MSE 不支持 TS 里的 HEVC(实测 `video/mp2t; codecs="hvc1…"` 为 false),数据进得了
缓冲却一帧都解不出,也没有 error 事件。同一浏览器直接 Range 播同一文件能出帧。
见 ADR-0016。
"""
repository = unittest.mock.Mock()
repository.media_asset.return_value = MediaAsset(
1, r"B:\video\one.mp4", None, "115", "one.mp4", 31.5, 100,
)
engine = MediaEngine(
repository, FilesystemBackend([Path("B:/")], Path("snapshots")),
)
self.assertEqual(engine.stream_plan(1).protocol, "range")
self.assertEqual(engine.stream_plan(1, mode="auto").protocol, "range")
plan = engine.stream_plan(1, mode="hls")
self.assertEqual(plan.protocol, "hls")
self.assertEqual(plan.segment_seconds, 6)
def test_hls_stays_unavailable_where_it_never_applied(self):
"""按需模式不是万能开关:本地来源和时长未知的片源仍然只能走 Range。"""
repository = unittest.mock.Mock()
repository.media_asset.return_value = MediaAsset(
1, r"R:\video\one.mp4", None, "local", "one.mp4", 31.5, 100,
)
engine = MediaEngine(repository, FilesystemBackend([Path("R:/")], Path("snapshots")))
self.assertEqual(engine.stream_plan(1, mode="hls").protocol, "range")
repository.media_asset.return_value = MediaAsset(
1, r"B:\video\one.mp4", None, "115", "one.mp4", None, 100,
)
engine = MediaEngine(repository, FilesystemBackend([Path("B:/")], Path("snapshots")))
self.assertEqual(engine.stream_plan(1, mode="hls").protocol, "range")
def test_local_or_unknown_duration_keeps_range_plan(self):
repository = unittest.mock.Mock()
repository.media_asset.return_value = MediaAsset(
1, r"R:\video\one.mp4", None, "local", "one.mp4", None, 100,
)
engine = MediaEngine(
repository, FilesystemBackend([Path("R:/")], Path("snapshots")),
)
self.assertEqual(engine.stream_plan(1).protocol, "range")
if __name__ == "__main__":
unittest.main()