6c70056a12
CI/CD Pipeline / Check if frontend-only change (push) Waiting to run
CI/CD Pipeline / Validate - Code Quality (push) Waiting to run
CI/CD Pipeline / Validate - Type Check (mypy) (push) Waiting to run
CI/CD Pipeline / Validate - Migration (alembic) (push) Waiting to run
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Blocked by required conditions
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Web Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging API Image (push) Waiting to run
CI/CD Pipeline / Build Staging Web Image (push) Waiting to run
CI/CD Pipeline / Build Staging Worker Image (push) Waiting to run
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (push) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (push) Blocked by required conditions
CI/CD Pipeline / CI Gate (push) Blocked by required conditions
233 lines
9.1 KiB
Python
233 lines
9.1 KiB
Python
"""测试 compose_video 任务中封面生成集成."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
class TestComposeVideoCoverIntegration:
|
|
"""测试视频合成任务中的封面生成集成."""
|
|
|
|
@pytest.fixture
|
|
def mock_job_service(self):
|
|
"""模拟 JobService."""
|
|
service = MagicMock()
|
|
service.get_job.return_value = MagicMock(
|
|
id="job_123",
|
|
payload={"plan_id": "plan_456"},
|
|
)
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def mock_db(self):
|
|
"""模拟数据库会话."""
|
|
return MagicMock()
|
|
|
|
@pytest.fixture
|
|
def mock_render_result(self):
|
|
"""模拟渲染结果."""
|
|
result = MagicMock()
|
|
result.success = True
|
|
result.output_path = Path("/tmp/output/video_123.mp4")
|
|
result.output_url = "https://example.com/video_123.mp4"
|
|
result.duration = 30.0
|
|
result.clip_count = 5
|
|
result.width = 1080
|
|
result.height = 1920
|
|
result.file_size = 1024000
|
|
return result
|
|
|
|
@pytest.fixture
|
|
def mock_plan_with_cover_enabled(self):
|
|
"""模拟启用封面的 plan."""
|
|
plan = MagicMock()
|
|
plan.id = "plan_456"
|
|
plan.config = {
|
|
"cover_config": {
|
|
"enabled": True,
|
|
"mode": "smart",
|
|
}
|
|
}
|
|
return plan
|
|
|
|
@pytest.fixture
|
|
def mock_plan_with_cover_disabled(self):
|
|
"""模拟禁用封面的 plan."""
|
|
plan = MagicMock()
|
|
plan.id = "plan_456"
|
|
plan.config = {
|
|
"cover_config": {
|
|
"enabled": False,
|
|
}
|
|
}
|
|
return plan
|
|
|
|
@pytest.fixture
|
|
def mock_plan_without_cover_config(self):
|
|
"""模拟没有 cover_config 的 plan."""
|
|
plan = MagicMock()
|
|
plan.id = "plan_456"
|
|
plan.config = {}
|
|
return plan
|
|
|
|
def test_cover_generation_called_when_enabled(
|
|
self,
|
|
mock_job_service,
|
|
mock_db,
|
|
mock_render_result,
|
|
mock_plan_with_cover_enabled,
|
|
):
|
|
"""测试封面生成在启用时被调用."""
|
|
from worker_app.tasks.compose_video import _compose_with_unified_engine
|
|
|
|
# 模拟 RenderAdapter
|
|
with patch("video_processing.render_adapter.RenderAdapter") as MockAdapter:
|
|
adapter_instance = MagicMock()
|
|
adapter_instance.render_plan.return_value = mock_render_result
|
|
adapter_instance.validate_plan.return_value = (True, [], [], 5, 5)
|
|
MockAdapter.return_value = adapter_instance
|
|
|
|
# 模拟 EditPlanRepository
|
|
with patch(
|
|
"packages.adapters.sqlalchemy_impl.edit_plan_repository.SQLAlchemyEditPlanRepository"
|
|
) as MockPlanRepo:
|
|
plan_repo_instance = MagicMock()
|
|
plan_repo_instance.get.return_value = mock_plan_with_cover_enabled
|
|
MockPlanRepo.return_value = plan_repo_instance
|
|
|
|
# 模拟 generate_cover_from_plan
|
|
with patch("video_processing.cover_generator.generate_cover_from_plan") as mock_gen_cover:
|
|
mock_gen_cover.return_value = Path("/tmp/output/cover_plan_456.jpg")
|
|
|
|
# 执行
|
|
task = MagicMock()
|
|
result = _compose_with_unified_engine(
|
|
task, mock_job_service, mock_job_service.get_job(), "plan_456", mock_db
|
|
)
|
|
|
|
# 验证封面生成被调用
|
|
mock_gen_cover.assert_called_once()
|
|
call_args = mock_gen_cover.call_args
|
|
assert call_args[0][0] == mock_plan_with_cover_enabled # plan
|
|
assert call_args[0][1] == mock_render_result.output_path # video_path
|
|
assert call_args[0][2] == mock_render_result.output_path.parent # output_dir
|
|
|
|
# 验证结果包含 cover_url
|
|
assert "cover_url" in result["result"]
|
|
assert result["result"]["cover_url"] == "/covers/plan_456.jpg"
|
|
|
|
def test_cover_generation_skipped_when_disabled(
|
|
self,
|
|
mock_job_service,
|
|
mock_db,
|
|
mock_render_result,
|
|
mock_plan_with_cover_disabled,
|
|
):
|
|
"""测试封面生成在禁用时被跳过."""
|
|
from worker_app.tasks.compose_video import _compose_with_unified_engine
|
|
|
|
with patch("video_processing.render_adapter.RenderAdapter") as MockAdapter:
|
|
adapter_instance = MagicMock()
|
|
adapter_instance.render_plan.return_value = mock_render_result
|
|
adapter_instance.validate_plan.return_value = (True, [], [], 5, 5)
|
|
MockAdapter.return_value = adapter_instance
|
|
|
|
with patch(
|
|
"packages.adapters.sqlalchemy_impl.edit_plan_repository.SQLAlchemyEditPlanRepository"
|
|
) as MockPlanRepo:
|
|
plan_repo_instance = MagicMock()
|
|
plan_repo_instance.get.return_value = mock_plan_with_cover_disabled
|
|
MockPlanRepo.return_value = plan_repo_instance
|
|
|
|
with patch("video_processing.cover_generator.generate_cover_from_plan") as mock_gen_cover:
|
|
task = MagicMock()
|
|
result = _compose_with_unified_engine(
|
|
task, mock_job_service, mock_job_service.get_job(), "plan_456", mock_db
|
|
)
|
|
|
|
# 验证封面生成未被调用
|
|
mock_gen_cover.assert_not_called()
|
|
|
|
# 验证结果中 cover_url 为 None
|
|
assert "cover_url" in result["result"]
|
|
assert result["result"]["cover_url"] is None
|
|
|
|
def test_cover_generation_skipped_when_no_config(
|
|
self,
|
|
mock_job_service,
|
|
mock_db,
|
|
mock_render_result,
|
|
mock_plan_without_cover_config,
|
|
):
|
|
"""测试没有 cover_config 时封面生成被跳过."""
|
|
from worker_app.tasks.compose_video import _compose_with_unified_engine
|
|
|
|
with patch("video_processing.render_adapter.RenderAdapter") as MockAdapter:
|
|
adapter_instance = MagicMock()
|
|
adapter_instance.render_plan.return_value = mock_render_result
|
|
adapter_instance.validate_plan.return_value = (True, [], [], 5, 5)
|
|
MockAdapter.return_value = adapter_instance
|
|
|
|
with patch(
|
|
"packages.adapters.sqlalchemy_impl.edit_plan_repository.SQLAlchemyEditPlanRepository"
|
|
) as MockPlanRepo:
|
|
plan_repo_instance = MagicMock()
|
|
plan_repo_instance.get.return_value = mock_plan_without_cover_config
|
|
MockPlanRepo.return_value = plan_repo_instance
|
|
|
|
with patch("video_processing.cover_generator.generate_cover_from_plan") as mock_gen_cover:
|
|
task = MagicMock()
|
|
result = _compose_with_unified_engine(
|
|
task, mock_job_service, mock_job_service.get_job(), "plan_456", mock_db
|
|
)
|
|
|
|
# 验证封面生成未被调用
|
|
mock_gen_cover.assert_not_called()
|
|
|
|
# 验证结果中 cover_url 为 None
|
|
assert "cover_url" in result["result"]
|
|
assert result["result"]["cover_url"] is None
|
|
|
|
def test_cover_generation_failure_does_not_break_video(
|
|
self,
|
|
mock_job_service,
|
|
mock_db,
|
|
mock_render_result,
|
|
mock_plan_with_cover_enabled,
|
|
):
|
|
"""测试封面生成失败不影响视频合成."""
|
|
from worker_app.tasks.compose_video import _compose_with_unified_engine
|
|
|
|
with patch("video_processing.render_adapter.RenderAdapter") as MockAdapter:
|
|
adapter_instance = MagicMock()
|
|
adapter_instance.render_plan.return_value = mock_render_result
|
|
adapter_instance.validate_plan.return_value = (True, [], [], 5, 5)
|
|
MockAdapter.return_value = adapter_instance
|
|
|
|
with patch(
|
|
"packages.adapters.sqlalchemy_impl.edit_plan_repository.SQLAlchemyEditPlanRepository"
|
|
) as MockPlanRepo:
|
|
plan_repo_instance = MagicMock()
|
|
plan_repo_instance.get.return_value = mock_plan_with_cover_enabled
|
|
MockPlanRepo.return_value = plan_repo_instance
|
|
|
|
with patch("video_processing.cover_generator.generate_cover_from_plan") as mock_gen_cover:
|
|
# 模拟封面生成抛出异常
|
|
mock_gen_cover.side_effect = Exception("FFmpeg failed")
|
|
|
|
task = MagicMock()
|
|
result = _compose_with_unified_engine(
|
|
task, mock_job_service, mock_job_service.get_job(), "plan_456", mock_db
|
|
)
|
|
|
|
# 验证视频合成仍然成功
|
|
assert result["status"] == "completed"
|
|
assert "result" in result
|
|
assert result["result"]["output_url"] == mock_render_result.output_url
|
|
|
|
# 验证结果中 cover_url 为 None
|
|
assert result["result"]["cover_url"] is None
|