c873bb635f
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 55s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m13s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m43s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m46s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m51s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m6s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m59s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m14s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m53s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m6s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m12s
AI Code Review / AI Code Review (pull_request) Failing after 6m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 39s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m55s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m9s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m13s
CI/CD Pipeline / Unit Tests (push) Successful in 14m24s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m30s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
168 lines
6.3 KiB
Python
Executable File
168 lines
6.3 KiB
Python
Executable File
"""P0/P1 修复单元测试 — 一键生成 P0 问题 + P1 校验.
|
||
|
||
覆盖:
|
||
P0-2: OSS 上传失败抛异常 + URL 可访问性校验
|
||
P0-3: FFmpeg 失败时完整 stderr 日志
|
||
P1: template_id 存在性校验 + asset_ids 归属校验
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
# 添加 worker app 到 sys.path
|
||
_WORKER_ROOT = Path(__file__).resolve().parents[2] / "apps" / "worker"
|
||
if str(_WORKER_ROOT) not in sys.path:
|
||
sys.path.insert(0, str(_WORKER_ROOT))
|
||
|
||
|
||
# ── P0-1: _download_library_assets ────────────────────────────────────────────
|
||
|
||
|
||
class TestOSSUploadAndVerify:
|
||
"""P0-2: OSS 上传失败抛异常 + URL 可访问性校验."""
|
||
|
||
def test_verify_url_accessible_success(self):
|
||
"""URL 可访问时返回 True."""
|
||
from worker_app.tasks.generation import _verify_url_accessible
|
||
|
||
mock_response = MagicMock()
|
||
mock_response.status = 200
|
||
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
||
mock_response.__exit__ = MagicMock(return_value=False)
|
||
|
||
with patch("urllib.request.OpenerDirector.open", return_value=mock_response):
|
||
assert _verify_url_accessible("https://example.com/test.mp4") is True
|
||
|
||
def test_verify_url_accessible_failure(self):
|
||
"""URL 不可访问时返回 False."""
|
||
from worker_app.tasks.generation import _verify_url_accessible
|
||
|
||
with patch("urllib.request.OpenerDirector.open", side_effect=Exception("connection refused")):
|
||
assert _verify_url_accessible("https://example.com/test.mp4") is False
|
||
|
||
def test_verify_url_404(self):
|
||
"""URL 返回 404 时返回 False."""
|
||
from worker_app.tasks.generation import _verify_url_accessible
|
||
|
||
mock_response = MagicMock()
|
||
mock_response.status = 404
|
||
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
||
mock_response.__exit__ = MagicMock(return_value=False)
|
||
|
||
with patch("urllib.request.OpenerDirector.open", return_value=mock_response):
|
||
assert _verify_url_accessible("https://example.com/test.mp4") is False
|
||
|
||
|
||
# ── P0-3: FFmpeg stderr 日志 ─────────────────────────────────────────────────
|
||
|
||
|
||
class TestFFmpegStderrLogging:
|
||
"""P0-3: FFmpeg 失败时完整 stderr 打到日志."""
|
||
|
||
def test_run_ffmpeg_logs_stderr_on_failure(self, caplog):
|
||
"""run_ffmpeg 失败时记录 stderr 到日志."""
|
||
from video_processing.ffmpeg_utils import run_ffmpeg
|
||
|
||
error = subprocess.CalledProcessError(
|
||
returncode=183,
|
||
cmd=["ffmpeg", "-y", "-i", "input.mp4", "output.mp4"],
|
||
output="",
|
||
stderr="Error message from ffmpeg: filter graph error details here",
|
||
)
|
||
|
||
with patch("subprocess.run", side_effect=error):
|
||
with caplog.at_level(logging.ERROR):
|
||
with pytest.raises(subprocess.CalledProcessError):
|
||
run_ffmpeg(["ffmpeg", "-y", "-i", "input.mp4", "output.mp4"])
|
||
|
||
assert "FFmpeg 命令失败" in caplog.text
|
||
assert "exit_code=183" in caplog.text
|
||
assert "filter graph error" in caplog.text
|
||
|
||
def test_run_ffmpeg_success(self):
|
||
"""run_ffmpeg 成功时正常返回."""
|
||
from video_processing.ffmpeg_utils import run_ffmpeg
|
||
|
||
mock_result = MagicMock()
|
||
mock_result.stdout = "output"
|
||
mock_result.stderr = ""
|
||
|
||
with patch("subprocess.run", return_value=mock_result):
|
||
stdout, stderr = run_ffmpeg(["ffmpeg", "-version"])
|
||
assert stdout == "output"
|
||
assert stderr == ""
|
||
|
||
|
||
# ── P1: template_id + asset_ids 校验 ─────────────────────────────────────────
|
||
|
||
|
||
class TestP1Validations:
|
||
"""P1: template_id 存在性校验 + asset_ids 归属校验."""
|
||
|
||
def test_validate_template_exists_success(self):
|
||
"""模板存在时不抛异常."""
|
||
from worker_app.tasks.generation import _validate_template_exists
|
||
|
||
mock_template = MagicMock()
|
||
mock_template.id = "tmpl_001"
|
||
mock_template.name = "Test Template"
|
||
mock_template.is_active = True
|
||
mock_template.status = "active"
|
||
mock_template.version = 1
|
||
|
||
session = MagicMock()
|
||
|
||
# EditTemplateModel 查询返回 None(走旧模板系统 fallback)
|
||
edit_query = MagicMock()
|
||
edit_filter = MagicMock()
|
||
edit_query.filter.return_value = edit_filter
|
||
edit_filter.first.return_value = None
|
||
|
||
# TemplateModel 查询返回 mock_template
|
||
old_query = MagicMock()
|
||
old_filter = MagicMock()
|
||
old_query.filter.return_value = old_filter
|
||
old_filter.first.return_value = mock_template
|
||
|
||
def _query_side_effect(model):
|
||
# 根据 model 类型返回不同的 query mock
|
||
name = getattr(model, "__name__", "")
|
||
if "EditTemplate" in name:
|
||
return edit_query
|
||
return old_query
|
||
|
||
session.query.side_effect = _query_side_effect
|
||
|
||
with patch("worker_app.tasks.generation.SessionLocal", return_value=session):
|
||
_validate_template_exists("tmpl_001") # 不抛异常
|
||
|
||
def test_validate_template_exists_not_found(self):
|
||
"""模板不存在时抛 ValueError."""
|
||
from worker_app.tasks.generation import _validate_template_exists
|
||
|
||
session = MagicMock()
|
||
|
||
# 两个系统查询都返回 None
|
||
def _query_side_effect(model):
|
||
q = MagicMock()
|
||
f = MagicMock()
|
||
q.filter.return_value = f
|
||
f.first.return_value = None
|
||
return q
|
||
|
||
session.query.side_effect = _query_side_effect
|
||
|
||
with patch("worker_app.tasks.generation.SessionLocal", return_value=session):
|
||
with pytest.raises(ValueError, match="模板不存在"):
|
||
_validate_template_exists("tmpl_nonexistent")
|
||
|
||
|
||
# ── P1: 一键生成 clip 级效果层映射 ───────────────────────────────────────────
|