All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 17s
Co-authored-by: Cursor <cursoragent@cursor.com>
343 lines
12 KiB
Python
343 lines
12 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 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"
|
|
|
|
|
|
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_zip_wrapper_layout_detected(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"
|
|
|
|
_build_archive_zip(archive, "media-assets-main")
|
|
|
|
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_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
|