Files
xiaoxia-saas/tests/unit/test_render_adapter.py
T
CI Bot d1627d5aaf
CI Build & Deploy Pipeline / Build Staging API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (pull_request) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 10s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 36s
AI Code Review / AI Code Review (pull_request) Successful in 1m5s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Successful in 2m3s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m44s
Auto Approve CI PRs / Auto Approve on CI Green (pull_request) Successful in 2m58s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m18s
fix: black format code quality issues
2026-07-17 12:20:53 +08:00

814 lines
29 KiB
Python
Executable File

"""RenderAdapter 单元测试 — Phase 2.
测试适配层的计划加载、素材下载、引擎调用、结果上传等逻辑。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
from video_processing.render_adapter import RenderAdapter
# ── Fixtures ──────────────────────────────────────────────────────────────────
@dataclass
class FakeClip:
"""模拟 EditPlanClip。"""
id: str
plan_id: str = "plan_001"
clip_type: str = "main"
order: int = 0
asset_id: str = ""
text_content: str = ""
start_time: float = 0.0
duration: float = 0.0
transition_effect: str = "cut"
status: str = "ready"
config: dict[str, Any] = field(default_factory=dict)
@dataclass
class FakePlan:
"""模拟 EditPlan。"""
id: str = "plan_001"
name: str = "测试计划"
status: str = "editing"
config: dict[str, Any] = field(default_factory=dict)
def _make_clip(
clip_id: str,
clip_type: str = "main",
order: int = 0,
asset_id: str | None = None,
duration: float = 5.0,
status: str = "ready",
transition_effect: str = "cut",
config: dict[str, Any] | None = None,
) -> FakeClip:
# asset_id 为 None 时生成默认值,为空字符串时保留空串
if asset_id is None:
asset_id = f"asset_{clip_id}.mp4"
return FakeClip(
id=clip_id,
clip_type=clip_type,
order=order,
asset_id=asset_id,
duration=duration,
status=status,
transition_effect=transition_effect,
config=config or {},
)
def _make_adapter(
plan: FakePlan | None = None,
clips: list[FakeClip] | None = None,
) -> tuple[RenderAdapter, MagicMock, MagicMock]:
"""创建测试用的 RenderAdapter 及 mock repo。
Returns:
(adapter, mock_plan_repo, mock_clip_repo)
"""
mock_db = MagicMock()
adapter = RenderAdapter(mock_db)
# 替换内部 repo
mock_plan_repo = MagicMock()
mock_clip_repo = MagicMock()
adapter._plan_repo = mock_plan_repo
adapter._clip_repo = mock_clip_repo
# 设置默认返回
if plan is not None:
mock_plan_repo.get.return_value = plan
if clips is not None:
mock_clip_repo.list_by_plan.return_value = clips
return adapter, mock_plan_repo, mock_clip_repo
# ── validate_plan 测试 ───────────────────────────────────────────────────────
class TestValidatePlan:
def test_plan_not_found(self):
"""计划不存在时校验失败。"""
adapter, mock_plan_repo, _ = _make_adapter(plan=None)
mock_plan_repo.get.return_value = None
valid, errors, warnings, ready_count, total_count = adapter.validate_plan("plan_001")
assert not valid
assert len(errors) == 1
assert "不存在" in errors[0]
assert ready_count == 0
assert total_count == 0
def test_no_clips(self):
"""没有任何片段时校验失败。"""
plan = FakePlan(id="plan_001", status="editing")
adapter, _, mock_clip_repo = _make_adapter(plan=plan, clips=[])
valid, errors, warnings, ready_count, total_count = adapter.validate_plan("plan_001")
assert not valid
assert any("没有任何片段" in e for e in errors)
def test_no_ready_clips(self):
"""没有 ready 片段时校验失败。"""
plan = FakePlan(id="plan_001", status="editing")
clips = [
_make_clip("c1", status="pending"),
_make_clip("c2", status="pending"),
]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
valid, errors, warnings, ready_count, total_count = adapter.validate_plan("plan_001")
assert not valid
assert any("没有就绪" in e for e in errors)
assert ready_count == 0
assert total_count == 2
def test_ready_clip_no_asset(self):
"""ready 片段没有 asset_id 时报错。"""
plan = FakePlan(id="plan_001", status="editing")
clips = [
_make_clip("c1", asset_id=""),
]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
valid, errors, warnings, ready_count, total_count = adapter.validate_plan("plan_001")
assert not valid
assert any("没有分配素材" in e for e in errors)
def test_valid_plan(self):
"""正常计划校验通过。"""
plan = FakePlan(id="plan_001", status="editing")
clips = [
_make_clip("c1", order=0, duration=3.0),
_make_clip("c2", order=1, duration=4.0),
]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
valid, errors, warnings, ready_count, total_count = adapter.validate_plan("plan_001")
assert valid
assert len(errors) == 0
assert ready_count == 2
assert total_count == 2
def test_wrong_status(self):
"""计划状态不正确时报错。"""
plan = FakePlan(id="plan_001", status="draft")
clips = [_make_clip("c1")]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
valid, errors, _, _, _ = adapter.validate_plan("plan_001")
assert not valid
assert any("状态不正确" in e for e in errors)
def test_mixed_status_with_warnings(self):
"""混合状态时有 pending/failed 警告。"""
plan = FakePlan(id="plan_001", status="editing")
clips = [
_make_clip("c1", order=0, status="ready"),
_make_clip("c2", order=1, status="pending"),
_make_clip("c3", order=2, status="failed"),
]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
valid, errors, warnings, ready_count, total_count = adapter.validate_plan("plan_001")
assert valid
assert any("pending" in w for w in warnings)
assert any("failed" in w for w in warnings)
assert ready_count == 1
assert total_count == 3
# ── render_plan 测试 ─────────────────────────────────────────────────────────
class TestRenderPlan:
def test_plan_not_found(self):
"""计划不存在时返回失败。"""
adapter, mock_plan_repo, _ = _make_adapter(plan=None)
mock_plan_repo.get.return_value = None
result = adapter.render_plan("plan_001")
assert not result.success
assert "不存在" in result.error_message
def test_no_ready_clips(self):
"""没有 ready 片段时返回失败。"""
plan = FakePlan(id="plan_001", status="editing")
clips = [_make_clip("c1", status="pending")]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
result = adapter.render_plan("plan_001")
assert not result.success
assert "没有可渲染" in result.error_message
assert result.clip_count == 0
@patch("video_processing.render_adapter.download_asset")
def test_all_assets_download_fail(self, mock_download):
"""所有素材下载失败时返回失败。"""
mock_download.return_value = False
plan = FakePlan(id="plan_001", status="editing")
clips = [_make_clip("c1", order=0, duration=5.0)]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
result = adapter.render_plan("plan_001")
assert not result.success
assert "素材下载失败" in result.error_message
@patch("video_processing.render_adapter.upload_to_oss")
@patch("video_processing.render_adapter.UnifiedRenderService")
@patch("video_processing.render_adapter.download_asset")
def test_successful_render(self, mock_download, mock_render_cls, mock_upload, tmp_path):
"""完整渲染流程成功。"""
# 素材下载成功
def _fake_download(asset_id, local_path):
local_path.parent.mkdir(parents=True, exist_ok=True)
local_path.write_bytes(b"fake video data")
return True
mock_download.side_effect = _fake_download
# 渲染成功
mock_render = MagicMock()
mock_render.render.return_value = MagicMock(
output_path=tmp_path / "output.mp4",
duration=10.0,
file_size=102400,
width=1280,
height=720,
)
mock_render_cls.return_value = mock_render
# 上传成功
mock_upload.return_value = "https://oss.example.com/rendered/plan_001/job_001.mp4"
plan = FakePlan(id="plan_001", status="editing")
clips = [
_make_clip("c1", order=0, duration=5.0),
_make_clip("c2", order=1, duration=5.0),
]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
result = adapter.render_plan(
"plan_001",
job_id="job_001",
work_dir=tmp_path / "work",
)
assert result.success
assert result.output_url.startswith("https://")
assert result.duration == 10.0
assert result.width == 1280
assert result.height == 720
assert result.clip_count == 2
# 验证 UnifiedRenderService 被正确调用
mock_render_cls.assert_called_once()
call_kwargs = mock_render_cls.call_args
assert call_kwargs.kwargs["plan"] is plan
assert len(call_kwargs.kwargs["clips"]) == 2
assert len(call_kwargs.kwargs["asset_path_map"]) == 2
@patch("video_processing.render_adapter.download_asset")
def test_progress_callback(self, mock_download, tmp_path):
"""进度回调被正确触发。"""
def _fake_download(asset_id, local_path):
local_path.parent.mkdir(parents=True, exist_ok=True)
local_path.write_bytes(b"fake data")
return True
mock_download.side_effect = _fake_download
# 模拟渲染异常,避免走到最后
with patch("video_processing.render_adapter.UnifiedRenderService") as mock_render_cls:
mock_render = MagicMock()
mock_render.render.side_effect = RuntimeError("render error")
mock_render_cls.return_value = mock_render
plan = FakePlan(id="plan_001", status="editing")
clips = [_make_clip("c1", order=0, duration=5.0)]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
progress_values = []
def progress_cb(progress: float, stage: str) -> None:
progress_values.append((progress, stage))
adapter.render_plan(
"plan_001",
work_dir=tmp_path / "work",
progress_cb=progress_cb,
)
# 即使渲染失败,前期进度也应该上报了
assert len(progress_values) > 0
# 第一个进度应该是加载计划
assert progress_values[0][1] == "加载剪辑计划"
@patch("video_processing.render_adapter.download_asset")
def test_partial_asset_download(self, mock_download, tmp_path):
"""部分素材下载失败时,只使用成功的素材。"""
download_results = [True, False, True] # 3个素材中2个成功
def _fake_download(asset_id, local_path):
idx = hash(asset_id) % 3
if download_results[idx]:
local_path.parent.mkdir(parents=True, exist_ok=True)
local_path.write_bytes(b"fake data")
return True
return False
mock_download.side_effect = _fake_download
with patch("video_processing.render_adapter.UnifiedRenderService") as mock_render_cls:
mock_render = MagicMock()
mock_render.render.return_value = MagicMock(
output_path=tmp_path / "out.mp4",
duration=5.0,
file_size=1024,
width=1280,
height=720,
)
mock_render_cls.return_value = mock_render
with patch("video_processing.render_adapter.upload_to_oss", return_value="https://example.com/out.mp4"):
plan = FakePlan(id="plan_001", status="editing")
clips = [
_make_clip("c1", order=0, duration=3.0, asset_id="asset_001.mp4"),
_make_clip("c2", order=1, duration=3.0, asset_id="asset_002.mp4"),
_make_clip("c3", order=2, duration=3.0, asset_id="asset_003.mp4"),
]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
result = adapter.render_plan(
"plan_001",
work_dir=tmp_path / "work",
)
# 至少有部分素材成功,渲染应该进行
# (具体成功数量取决于 hash 结果,但至少1个成功就能渲染)
assert result.success or "素材下载失败" in result.error_message
# ── _download_assets 测试 ────────────────────────────────────────────────────
class TestDownloadAssets:
@patch("video_processing.render_adapter.download_asset")
def test_all_download_success(self, mock_download, tmp_path):
"""全部素材下载成功。"""
mock_download.return_value = True
clips = [
_make_clip("c1", order=0, asset_id="key1.mp4"),
_make_clip("c2", order=1, asset_id="key2.mp4"),
]
asset_path_map, rendered_ids, failed_ids = RenderAdapter._download_assets(clips, tmp_path)
assert len(asset_path_map) == 2
assert "key1.mp4" in asset_path_map
assert "key2.mp4" in asset_path_map
assert len(rendered_ids) == 2
assert len(failed_ids) == 0
assert mock_download.call_count == 2
@patch("video_processing.render_adapter.download_asset")
def test_empty_asset_id_skipped(self, mock_download, tmp_path):
"""空 asset_id 的片段被跳过。"""
clips = [
_make_clip("c1", order=0, asset_id=""),
_make_clip("c2", order=1, asset_id="key2.mp4"),
]
mock_download.return_value = True
asset_path_map, rendered_ids, failed_ids = RenderAdapter._download_assets(clips, tmp_path)
assert len(asset_path_map) == 1
assert "key2.mp4" in asset_path_map
assert "c1" in failed_ids
assert "c2" in rendered_ids
assert mock_download.call_count == 1 # 只调用了一次下载
@patch("video_processing.render_adapter.download_asset")
def test_all_download_fail(self, mock_download, tmp_path):
"""全部下载失败返回空字典。"""
mock_download.return_value = False
clips = [
_make_clip("c1", order=0, asset_id="key1.mp4"),
]
asset_path_map, rendered_ids, failed_ids = RenderAdapter._download_assets(clips, tmp_path)
assert len(asset_path_map) == 0
assert len(rendered_ids) == 0
assert "c1" in failed_ids
@patch("video_processing.render_adapter.download_asset")
def test_partial_download_failure(self, mock_download, tmp_path):
"""部分下载失败时正确区分成功/失败。"""
results = {"key1.mp4": True, "key2.mp4": False, "key3.mp4": True}
def _fake_download(asset_id, local_path):
local_path.parent.mkdir(parents=True, exist_ok=True)
local_path.write_bytes(b"fake")
return results.get(asset_id, False)
mock_download.side_effect = _fake_download
clips = [
_make_clip("c1", order=0, asset_id="key1.mp4"),
_make_clip("c2", order=1, asset_id="key2.mp4"),
_make_clip("c3", order=2, asset_id="key3.mp4"),
]
asset_path_map, rendered_ids, failed_ids = RenderAdapter._download_assets(clips, tmp_path)
assert len(asset_path_map) == 2
assert "c1" in rendered_ids
assert "c3" in rendered_ids
assert "c2" in failed_ids
@patch("video_processing.render_adapter.download_asset")
def test_duplicate_asset_downloaded_once(self, mock_download, tmp_path):
"""同一素材被多个 clip 引用时只下载一次。"""
mock_download.return_value = True
clips = [
_make_clip("c1", order=0, asset_id="shared.mp4"),
_make_clip("c2", order=1, asset_id="shared.mp4"),
]
asset_path_map, rendered_ids, failed_ids = RenderAdapter._download_assets(clips, tmp_path)
assert len(asset_path_map) == 1
assert mock_download.call_count == 1
assert "c1" in rendered_ids
assert "c2" in rendered_ids
# ── _prepare_bgm 测试 ────────────────────────────────────────────────────────
class TestPrepareBgm:
"""BGM 音频准备逻辑测试。"""
def test_bgm_disabled_returns_none(self, tmp_path):
"""BGM 未启用时返回 None。"""
plan = FakePlan(config={"bgm": {"enabled": False}})
adapter = RenderAdapter(MagicMock())
result = adapter._prepare_bgm(plan, tmp_path, "plan_001")
assert result is None
def test_bgm_no_config_returns_none(self, tmp_path):
"""没有 BGM 配置时返回 None。"""
plan = FakePlan(config={})
adapter = RenderAdapter(MagicMock())
result = adapter._prepare_bgm(plan, tmp_path, "plan_001")
assert result is None
def test_bgm_from_url(self, tmp_path):
"""从 URL 下载 BGM。"""
plan = FakePlan(
config={
"bgm": {
"enabled": True,
"audio_url": "https://example.com/bgm.mp3",
}
}
)
adapter = RenderAdapter(MagicMock())
with patch("video_processing.url_security.safe_download_file") as mock_download:
def _fake_download(url, path, **kwargs):
Path(path).parent.mkdir(parents=True, exist_ok=True)
Path(path).write_bytes(b"fake mp3 data")
mock_download.side_effect = _fake_download
result = adapter._prepare_bgm(plan, tmp_path, "plan_001")
assert result is not None
assert Path(result).exists()
def test_bgm_from_url_failure_falls_through(self, tmp_path):
"""URL 下载失败时不抛异常,继续尝试其他来源。"""
plan = FakePlan(
config={
"bgm": {
"enabled": True,
"audio_url": "https://example.com/bgm.mp3",
"preset_id": "preset_001",
}
}
)
adapter = RenderAdapter(MagicMock())
with patch("video_processing.url_security.safe_download_file") as mock_download:
mock_download.side_effect = Exception("download failed")
# 预设库返回 None(没有这个预设)
with patch("packages.domain.preset_bgm.get_preset_bgm") as mock_preset:
mock_preset.return_value = None
result = adapter._prepare_bgm(plan, tmp_path, "plan_001")
assert result is None # 所有来源都失败时返回 None
def test_bgm_from_asset_library(self, tmp_path):
"""从素材库下载 BGM。"""
plan = FakePlan(
config={
"bgm": {
"enabled": True,
"asset_id": "asset_bgm_001",
}
}
)
mock_db = MagicMock()
mock_asset = MagicMock()
mock_asset.file_url = "oss://bgm/sample.mp3"
mock_query = MagicMock()
mock_query.first.return_value = mock_asset
mock_db.query.return_value = mock_query
adapter = RenderAdapter(mock_db)
with patch("video_processing.render_adapter.download_asset") as mock_download:
def _fake_download(storage_key, local_path):
Path(local_path).parent.mkdir(parents=True, exist_ok=True)
Path(local_path).write_bytes(b"fake bgm")
return True
mock_download.side_effect = _fake_download
result = adapter._prepare_bgm(plan, tmp_path, "plan_001")
assert result is not None
assert Path(result).exists()
def test_bgm_from_preset_library(self, tmp_path):
"""从预设 BGM 库下载。"""
plan = FakePlan(
config={
"bgm": {
"enabled": True,
"preset_id": "preset_calm",
}
}
)
adapter = RenderAdapter(MagicMock())
with patch("packages.domain.preset_bgm.get_preset_bgm") as mock_preset:
mock_preset.return_value = MagicMock(audio_url="https://cdn.example.com/preset_calm.mp3")
with patch("video_processing.url_security.safe_download_file") as mock_download:
def _fake_download(url, path, **kwargs):
Path(path).parent.mkdir(parents=True, exist_ok=True)
Path(path).write_bytes(b"preset bgm data")
mock_download.side_effect = _fake_download
result = adapter._prepare_bgm(plan, tmp_path, "plan_001")
assert result is not None
assert Path(result).exists()
def test_bgm_url_takes_priority_over_asset(self, tmp_path):
"""URL 优先级高于素材库。"""
plan = FakePlan(
config={
"bgm": {
"enabled": True,
"audio_url": "https://example.com/bgm.mp3",
"asset_id": "asset_bgm_001",
}
}
)
adapter = RenderAdapter(MagicMock())
with patch("video_processing.url_security.safe_download_file") as mock_url_download:
def _fake_url_download(url, path, **kwargs):
Path(path).parent.mkdir(parents=True, exist_ok=True)
Path(path).write_bytes(b"url bgm")
mock_url_download.side_effect = _fake_url_download
with patch("video_processing.render_adapter.download_asset") as mock_asset_download:
result = adapter._prepare_bgm(plan, tmp_path, "plan_001")
assert result is not None
mock_url_download.assert_called_once()
mock_asset_download.assert_not_called() # URL 成功后不会再走素材库
# ── _get_asr_service 测试 ────────────────────────────────────────────────────
class TestGetAsrService:
"""ASR 服务初始化测试。"""
def test_returns_service_when_available(self):
"""ASR 服务可用时返回实例。"""
mock_service = MagicMock()
with patch("services.asr_service_factory.get_asr_service") as mock_get:
mock_get.return_value = mock_service
result = RenderAdapter._get_asr_service()
assert result is mock_service
def test_returns_none_when_import_fails(self):
"""ASR 服务导入失败时返回 None(不阻断主流程)。"""
with patch("services.asr_service_factory.get_asr_service") as mock_get:
mock_get.side_effect = ImportError("asr module not found")
result = RenderAdapter._get_asr_service()
assert result is None
def test_returns_none_when_init_fails(self):
"""ASR 服务初始化失败时返回 None(不阻断主流程)。"""
with patch("services.asr_service_factory.get_asr_service") as mock_get:
mock_get.side_effect = RuntimeError("ASR init failed")
result = RenderAdapter._get_asr_service()
assert result is None
# ── render_plan BGM/ASR 集成测试 ─────────────────────────────────────────────
class TestRenderPlanWithBgmAsr:
"""渲染流程中 BGM 和 ASR 的集成测试。"""
@patch("video_processing.render_adapter.upload_to_oss")
@patch("video_processing.render_adapter.UnifiedRenderService")
@patch("video_processing.render_adapter.download_asset")
def test_bgm_passed_to_render_service(self, mock_download, mock_render_cls, mock_upload, tmp_path):
"""BGM 路径被正确传递给 UnifiedRenderService。"""
def _fake_download(asset_id, local_path):
local_path.parent.mkdir(parents=True, exist_ok=True)
local_path.write_bytes(b"fake video")
return True
mock_download.side_effect = _fake_download
mock_render = MagicMock()
mock_render.render.return_value = MagicMock(
output_path=tmp_path / "out.mp4",
duration=5.0,
file_size=1024,
width=1280,
height=720,
)
mock_render_cls.return_value = mock_render
mock_upload.return_value = "https://oss.example.com/out.mp4"
plan = FakePlan(
id="plan_bgm",
config={
"bgm": {
"enabled": True,
"preset_id": "preset_001",
}
},
)
clips = [_make_clip("c1", order=0, duration=5.0)]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
# Mock BGM 下载
with patch.object(adapter, "_prepare_bgm", return_value=str(tmp_path / "bgm.mp3")):
with patch.object(adapter, "_get_asr_service", return_value=None):
result = adapter.render_plan(
"plan_bgm",
work_dir=tmp_path / "work",
)
assert result.success
# 验证 UnifiedRenderService 收到了 bgm_path
call_kwargs = mock_render_cls.call_args
assert call_kwargs.kwargs["bgm_path"] == str(tmp_path / "bgm.mp3")
@patch("video_processing.render_adapter.upload_to_oss")
@patch("video_processing.render_adapter.UnifiedRenderService")
@patch("video_processing.render_adapter.download_asset")
def test_asr_service_passed_to_render_service(self, mock_download, mock_render_cls, mock_upload, tmp_path):
"""ASR 服务被正确传递给 UnifiedRenderService。"""
def _fake_download(asset_id, local_path):
local_path.parent.mkdir(parents=True, exist_ok=True)
local_path.write_bytes(b"fake video")
return True
mock_download.side_effect = _fake_download
mock_render = MagicMock()
mock_render.render.return_value = MagicMock(
output_path=tmp_path / "out.mp4",
duration=5.0,
file_size=1024,
width=1280,
height=720,
)
mock_render_cls.return_value = mock_render
mock_upload.return_value = "https://oss.example.com/out.mp4"
mock_asr = MagicMock()
plan = FakePlan(id="plan_asr")
clips = [_make_clip("c1", order=0, duration=5.0)]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
with patch.object(adapter, "_prepare_bgm", return_value=None):
with patch.object(adapter, "_get_asr_service", return_value=mock_asr):
result = adapter.render_plan(
"plan_asr",
work_dir=tmp_path / "work",
)
assert result.success
call_kwargs = mock_render_cls.call_args
assert call_kwargs.kwargs["asr_service"] is mock_asr
@patch("video_processing.render_adapter.upload_to_oss")
@patch("video_processing.render_adapter.UnifiedRenderService")
@patch("video_processing.render_adapter.download_asset")
def test_rendered_and_failed_clip_ids_in_result(self, mock_download, mock_render_cls, mock_upload, tmp_path):
"""渲染结果中包含成功和失败的 clip id 列表。"""
# 成功/失败映射,通过 asset_id 区分
download_map = {
"good_001.mp4": True,
"bad_002.mp4": False,
"good_003.mp4": True,
}
def _fake_download(asset_id, local_path):
local_path.parent.mkdir(parents=True, exist_ok=True)
ok = download_map.get(asset_id, False)
if ok:
local_path.write_bytes(b"fake video")
return ok
mock_download.side_effect = _fake_download
mock_render = MagicMock()
mock_render.render.return_value = MagicMock(
output_path=tmp_path / "out.mp4",
duration=3.0,
file_size=512,
width=1280,
height=720,
)
mock_render_cls.return_value = mock_render
mock_upload.return_value = "https://oss.example.com/out.mp4"
plan = FakePlan(id="plan_mixed")
clips = [
_make_clip("c_good1", order=0, asset_id="good_001.mp4"),
_make_clip("c_bad", order=1, asset_id="bad_002.mp4"),
_make_clip("c_good2", order=2, asset_id="good_003.mp4"),
]
adapter, _, _ = _make_adapter(plan=plan, clips=clips)
result = adapter.render_plan(
"plan_mixed",
work_dir=tmp_path / "work",
)
assert result.success
assert "c_good1" in result.rendered_clip_ids
assert "c_good2" in result.rendered_clip_ids
assert "c_bad" in result.failed_clip_ids
assert len(result.rendered_clip_ids) == 2
assert len(result.failed_clip_ids) == 1