Files
jiangchang-platform-kit/tests/test_media_assets.py
2026-06-06 16:16:18 +08:00

409 lines
15 KiB
Python

# -*- coding: utf-8 -*-
"""media_assets 共享媒体资源解析测试。"""
from __future__ import annotations
import json
import platform
import zipfile
from pathlib import Path
from unittest.mock import patch
import pytest
from jiangchang_skill_core import media_assets as ma
def _make_complete_assets(root: Path, *, with_ffmpeg: bool = True, with_ffprobe: bool = True) -> None:
(root / "music" / "calm").mkdir(parents=True)
(root / "music" / "calm" / "track.mp3").write_bytes(b"x" * 256)
(root / "fonts").mkdir(parents=True)
(root / "watermark").mkdir(parents=True)
bin_key = ma._platform_bin_key()
bin_dir = root / "bin" / bin_key
bin_dir.mkdir(parents=True)
if with_ffmpeg:
(bin_dir / ma._tool_filename("ffmpeg")).write_bytes(b"ffmpeg")
if with_ffprobe:
(bin_dir / ma._tool_filename("ffprobe")).write_bytes(b"ffprobe")
def _sample_manifest() -> dict:
bin_key = ma._platform_bin_key()
return {
"name": "media-assets",
"version": "0.2.0",
"directories": {"music": "music", "fonts": "fonts", "watermark": "watermark"},
"tools": {
"ffmpeg": {
bin_key: {
"package": "ffmpeg-release-essentials",
"archive_type": "zip",
"urls": [{"name": "test", "url": "https://example.test/ffmpeg.zip"}],
"archive_paths": {
"ffmpeg": "*/bin/ffmpeg.exe",
"ffprobe": "*/bin/ffprobe.exe",
},
}
}
},
}
def _install_tools_from_manifest(root: Path, warnings: list[str]) -> bool:
bin_key = ma._platform_bin_key()
bin_dir = root / "bin" / bin_key
bin_dir.mkdir(parents=True, exist_ok=True)
(bin_dir / ma._tool_filename("ffmpeg")).write_bytes(b"ffmpeg")
(bin_dir / ma._tool_filename("ffprobe")).write_bytes(b"ffprobe")
return True
def _build_archive_zip(dest: Path, inner_name: str) -> None:
manifest = _sample_manifest()
with zipfile.ZipFile(dest, "w") as zf:
zf.writestr(f"{inner_name}/README.md", "# media-assets\n")
zf.writestr(f"{inner_name}/manifest.json", json.dumps(manifest, ensure_ascii=False))
zf.writestr(f"{inner_name}/music/calm/track.mp3", b"x" * 256)
zf.writestr(f"{inner_name}/fonts/.keep", "")
zf.writestr(f"{inner_name}/watermark/.keep", "")
def test_default_bundle_url_is_release() -> None:
url = ma._media_assets_bundle_url()
assert url == ma.MEDIA_ASSETS_BUNDLE_URL
assert url.endswith("/releases/download/vlatest/media-assets.zip")
assert "/archive/main.zip" not in url
def test_bundle_url_env_override() -> None:
override = "https://example.com/media-assets.zip"
assert ma._media_assets_bundle_url(env={"MEDIA_ASSETS_BUNDLE_URL": override}) == override
def test_zip_download_uses_bundle_url_override(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
data_root = tmp_path / "data"
shared = data_root / "shared"
target = shared / "media-assets"
archive = tmp_path / "media-assets.zip"
override = "https://example.com/media-assets.zip"
_build_archive_zip(archive, "media-assets")
captured_urls: list[str] = []
def _fake_download(url: str, dest: Path) -> None:
captured_urls.append(url)
dest.write_bytes(archive.read_bytes())
monkeypatch.setattr(ma, "_download_zip", _fake_download)
monkeypatch.setattr(ma, "_find_git_executable", lambda: None)
monkeypatch.setattr(ma, "_try_download_ffmpeg_tools", _install_tools_from_manifest)
env = {
"JIANGCHANG_DATA_ROOT": str(data_root),
"MEDIA_ASSETS_BUNDLE_URL": override,
}
status = ma.ensure_media_assets(env=env)
assert captured_urls == [override]
assert target.is_dir()
assert status.ready is True
def test_media_assets_root_env_priority(tmp_path: Path) -> None:
custom = tmp_path / "custom-media"
custom.mkdir()
env = {
"MEDIA_ASSETS_ROOT": str(custom),
"JIANGCHANG_DATA_ROOT": str(tmp_path / "data"),
}
assert ma.resolve_media_assets_root(env=env) == custom
def test_default_media_assets_path(tmp_path: Path) -> None:
env = {"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}
expected = tmp_path / "data" / "shared" / "media-assets"
assert ma.resolve_media_assets_root(env=env) == expected
def test_complete_assets_skips_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path / "data" / "shared" / "media-assets"
_make_complete_assets(root)
def _fail_download(*args, **kwargs):
raise AssertionError("download should not be called")
monkeypatch.setattr(ma, "_try_download_via_zip", _fail_download)
monkeypatch.setattr(ma, "_try_download_via_git", _fail_download)
status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert status.ready is True
assert status.warnings == []
assert status.source == "local"
def test_missing_ffmpeg_warning(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path / "data" / "shared" / "media-assets"
_make_complete_assets(root, with_ffmpeg=False)
monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False)
monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False)
status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert status.ready is False
assert "ffmpeg_missing" in status.warnings
def test_missing_ffprobe_warning(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path / "data" / "shared" / "media-assets"
_make_complete_assets(root, with_ffprobe=False)
monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False)
monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False)
status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert status.ready is False
assert "ffprobe_missing" in status.warnings
def test_pick_background_music_stable_first(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path / "data" / "shared" / "media-assets"
_make_complete_assets(root)
music = root / "music"
(music / "b-upbeat.mp3").write_bytes(b"x" * 256)
(music / "calm" / "a-calm.mp3").write_bytes(b"x" * 256)
(music / "z-last.wav").write_bytes(b"x" * 256)
monkeypatch.setattr(
ma,
"ensure_media_assets",
lambda *a, **k: ma._inspect_media_assets(root, source="local"),
)
picked = ma.pick_background_music(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert picked == music / "b-upbeat.mp3"
@pytest.mark.parametrize("inner_name", ["media-assets", "media-assets-main"])
def test_zip_wrapper_layout_detected(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
inner_name: str,
) -> None:
data_root = tmp_path / "data"
shared = data_root / "shared"
target = shared / "media-assets"
archive = tmp_path / "media-assets.zip"
_build_archive_zip(archive, inner_name)
def _fake_download(url: str, dest: Path) -> None:
dest.write_bytes(archive.read_bytes())
monkeypatch.setattr(ma, "_download_zip", _fake_download)
monkeypatch.setattr(ma, "_find_git_executable", lambda: None)
monkeypatch.setattr(ma, "_try_download_ffmpeg_tools", _install_tools_from_manifest)
status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(data_root)})
assert target.is_dir()
assert status.ready is True
assert status.source == "zip"
assert "media_assets_zip_invalid_layout" not in status.warnings
def test_zip_download_failure_returns_warning(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
import urllib.error
def _raise_url_error(url: str, dest: Path) -> None:
raise urllib.error.URLError("network down")
monkeypatch.setattr(ma, "_download_zip", _raise_url_error)
monkeypatch.setattr(ma, "_find_git_executable", lambda: None)
status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert status.ready is False
assert "media_assets_zip_download_failed" in status.warnings
assert "git_not_available" in status.warnings
def test_git_not_available_when_zip_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
import urllib.error
monkeypatch.setattr(
ma,
"_download_zip",
lambda url, dest: (_ for _ in ()).throw(urllib.error.URLError("fail")),
)
monkeypatch.setattr(ma, "_find_git_executable", lambda: None)
status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert "git_not_available" in status.warnings
def test_media_assets_root_override_no_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
custom = tmp_path / "override"
custom.mkdir()
def _fail_download(*args, **kwargs):
raise AssertionError("download should not be called when MEDIA_ASSETS_ROOT is set")
monkeypatch.setattr(ma, "_try_download_via_zip", _fail_download)
status = ma.ensure_media_assets(env={"MEDIA_ASSETS_ROOT": str(custom)})
assert status.root == custom
assert status.exists is True
@pytest.mark.skipif(platform.system().lower() != "windows", reason="Windows default path check")
def test_windows_default_data_root() -> None:
with patch.dict("os.environ", {}, clear=True):
root = ma.resolve_media_assets_root()
assert root == Path(r"D:\jiangchang-data") / "shared" / "media-assets"
def test_probe_media_assets_does_not_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
def _fail_download(*args, **kwargs):
raise AssertionError("download should not be called")
monkeypatch.setattr(ma, "_try_download_via_zip", _fail_download)
monkeypatch.setattr(ma, "_try_download_via_git", _fail_download)
status = ma.probe_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert status.exists is False
assert status.ffmpeg_path is None
def test_lfs_pointer_warning_in_probe_and_ensure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path / "data" / "shared" / "media-assets"
_make_complete_assets(root)
music = root / "music"
(music / "calm" / "track.mp3").unlink()
(music / "lfs-only.mp3").write_text(
"version https://git-lfs.github.com/spec/v1\noid sha256:abc\nsize 123\n"
)
monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False)
monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False)
probed = ma.probe_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert "background_music_lfs_pointer_only" in probed.warnings
assert probed.music_ready is False
ensured = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert "background_music_lfs_pointer_only" in ensured.warnings
assert ensured.ready is False
def test_pick_background_music_skips_lfs_pointer(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path / "data" / "shared" / "media-assets"
_make_complete_assets(root, with_ffmpeg=True, with_ffprobe=True)
music = root / "music"
(music / "calm" / "track.mp3").unlink()
(music / "real.mp3").write_bytes(b"x" * 256)
lfs = music / "lfs-only.mp3"
lfs.write_text("version https://git-lfs.github.com/spec/v1\noid sha256:abc\nsize 123\n")
monkeypatch.setattr(
ma,
"ensure_media_assets",
lambda *a, **k: ma._inspect_media_assets(root, source="local"),
)
picked = ma.pick_background_music(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert picked == music / "real.mp3"
def test_pick_background_music_no_valid_files(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path / "data" / "shared" / "media-assets"
_make_complete_assets(root)
music = root / "music"
(music / "calm" / "track.mp3").unlink()
(music / "pointer.mp3").write_text(
"version https://git-lfs.github.com/spec/v1\noid sha256:abc\nsize 123\n"
)
monkeypatch.setattr(
ma,
"ensure_media_assets",
lambda *a, **k: ma._inspect_media_assets(root, source="local"),
)
picked = ma.pick_background_music(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert picked is None
def test_video_finalize_without_music_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
import asyncio
import os
import tempfile
from pathlib import Path
from jiangchang_skill_core import config
from jiangchang_skill_core.rpa.video_session import RpaVideoSession
ffmpeg_exe = tmp_path / "ffmpeg.exe"
ffmpeg_exe.write_bytes(b"ffmpeg")
def _fake_resolve_ffmpeg(*args, **kwargs):
return ffmpeg_exe
def _fake_pick_music(*args, **kwargs):
return None
monkeypatch.setattr(
"jiangchang_skill_core.rpa.video_session.resolve_ffmpeg",
_fake_resolve_ffmpeg,
)
monkeypatch.setattr(
"jiangchang_skill_core.rpa.video_session.pick_background_music",
_fake_pick_music,
)
monkeypatch.setattr(
"jiangchang_skill_core.rpa.video_session.background_music_issue",
lambda *a, **k: None,
)
os.environ["OPENCLAW_RECORD_VIDEO"] = "1"
try:
config.reset_cache()
with tempfile.TemporaryDirectory() as skill_data:
session = RpaVideoSession(
skill_slug="receive-order",
skill_data_dir=skill_data,
batch_id="batch_no_music",
)
capture = Path(session._capture_path)
capture.parent.mkdir(parents=True, exist_ok=True)
capture.write_bytes(b"\x00" * 64)
monkeypatch.setattr(
"jiangchang_skill_core.rpa.video_session._compose_capture_mp4",
lambda *a, **k: (True, ""),
)
asyncio.run(session.finalize())
assert "background_music_missing" in session.warnings
assert session.output_video_path is not None
finally:
os.environ.pop("OPENCLAW_RECORD_VIDEO", None)
config.reset_cache()
def test_manifest_ffmpeg_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path / "data" / "shared" / "media-assets"
(root / "music" / "calm").mkdir(parents=True)
(root / "music" / "calm" / "track.mp3").write_bytes(b"x" * 256)
(root / "fonts").mkdir(parents=True)
(root / "watermark").mkdir(parents=True)
(root / "manifest.json").write_text(
json.dumps(_sample_manifest(), ensure_ascii=False),
encoding="utf-8",
)
monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False)
monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False)
monkeypatch.setattr(ma, "_try_download_ffmpeg_tools", _install_tools_from_manifest)
status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")})
assert status.ready is True
assert status.ffmpeg_path is not None
assert status.ffprobe_path is not None