Files
xiaoxia-saas/tests/unit/test_cover_frame_pre_extract.py
CI Bot 7938eb5dda
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 / Validate - Migration (alembic) (push) Successful in 2m32s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m45s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m45s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
style: auto-format with black + isort + prettier [skip ci-format-check]
2026-08-16 07:20:15 +00:00

155 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for unified cover frame extraction pipeline.
统一封面管道测试:
- extract_first_frame: 从已渲染视频抽取封面帧
- 封面天然带标题(ASS 字幕已烧录到视频中)
"""
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
class TestExtractFirstFrame(unittest.TestCase):
"""extract_first_frame 单元测试."""
@patch("video_processing.ffmpeg_utils.run_ffmpeg")
@patch("video_processing.ffmpeg_utils.probe_duration", return_value=10.0)
def test_extracts_frame_at_default_ratio(self, mock_probe, mock_run):
"""默认在视频 15% 处抽帧."""
from video_processing.thumbnail_generator import extract_first_frame
# Mock run_ffmpeg 创建输出文件(ffmpeg 真实行为)
mock_run.side_effect = lambda cmd, **kw: Path(cmd[-1]).write_bytes(b"\xff\xd8\xff\xe0fake")
with tempfile.NamedTemporaryFile(suffix=".mp4") as video:
Path(video.name).write_bytes(b"fake video")
result = extract_first_frame(video.name)
self.assertTrue(Path(result).exists())
# 验证 ffmpeg 被调用
mock_run.assert_called()
cmd = mock_run.call_args[0][0]
self.assertIn("-vframes", cmd)
self.assertIn("1", cmd)
Path(result).unlink(missing_ok=True)
@patch("video_processing.ffmpeg_utils.run_ffmpeg")
@patch("video_processing.ffmpeg_utils.probe_duration", return_value=10.0)
def test_custom_seek_ratio(self, mock_probe, mock_run):
"""自定义抽帧位置."""
from video_processing.thumbnail_generator import extract_first_frame
mock_run.side_effect = lambda cmd, **kw: Path(cmd[-1]).write_bytes(b"\xff\xd8\xff\xe0fake")
with tempfile.NamedTemporaryFile(suffix=".mp4") as video:
Path(video.name).write_bytes(b"fake video")
result = extract_first_frame(video.name, seek_ratio=0.5)
self.assertTrue(Path(result).exists())
# 50% of 10s = 5s
cmd = mock_run.call_args[0][0]
ss_idx = cmd.index("-ss") + 1
seek_val = cmd[ss_idx]
# Should be around 5 seconds
self.assertIn("05", seek_val)
Path(result).unlink(missing_ok=True)
@patch("video_processing.ffmpeg_utils.run_ffmpeg")
@patch("video_processing.ffmpeg_utils.probe_duration", return_value=10.0)
def test_output_path_parameter(self, mock_probe, mock_run):
"""指定输出路径."""
from video_processing.thumbnail_generator import extract_first_frame
with tempfile.NamedTemporaryFile(suffix=".mp4") as video:
Path(video.name).write_bytes(b"fake video")
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as out:
pass # just get a path
# Create the file so ffmpeg "succeeds"
mock_run.side_effect = lambda *a, **k: Path(out.name).write_bytes(b"fake image")
result = extract_first_frame(video.name, output_path=out.name)
self.assertEqual(result, out.name)
Path(out.name).unlink(missing_ok=True)
@patch("video_processing.ffmpeg_utils.run_ffmpeg")
@patch("video_processing.ffmpeg_utils.probe_duration", return_value=10.0)
def test_keeps_original_resolution_by_default(self, mock_probe, mock_run):
"""默认保持原始分辨率(width=-1, height=-1."""
from video_processing.thumbnail_generator import extract_first_frame
mock_run.side_effect = lambda cmd, **kw: Path(cmd[-1]).write_bytes(b"\xff\xd8\xff\xe0fake")
with tempfile.NamedTemporaryFile(suffix=".mp4") as video:
Path(video.name).write_bytes(b"fake video")
result = extract_first_frame(video.name)
cmd = mock_run.call_args[0][0]
vf_idx = cmd.index("-vf") + 1
vf_filter = cmd[vf_idx]
# Should NOT have scale filter (only format)
self.assertNotIn("scale", vf_filter)
self.assertIn("format", vf_filter)
Path(result).unlink(missing_ok=True)
@patch("video_processing.ffmpeg_utils.run_ffmpeg")
@patch("video_processing.ffmpeg_utils.probe_duration", return_value=10.0)
def test_custom_width_triggers_scale(self, mock_probe, mock_run):
"""指定宽度时添加 scale 滤镜."""
from video_processing.thumbnail_generator import extract_first_frame
mock_run.side_effect = lambda cmd, **kw: Path(cmd[-1]).write_bytes(b"\xff\xd8\xff\xe0fake")
with tempfile.NamedTemporaryFile(suffix=".mp4") as video:
Path(video.name).write_bytes(b"fake video")
result = extract_first_frame(video.name, width=640)
cmd = mock_run.call_args[0][0]
vf_idx = cmd.index("-vf") + 1
vf_filter = cmd[vf_idx]
self.assertIn("scale=640", vf_filter)
Path(result).unlink(missing_ok=True)
@patch("video_processing.ffmpeg_utils.run_ffmpeg", side_effect=RuntimeError("fail"))
@patch("video_processing.ffmpeg_utils.probe_duration", return_value=10.0)
def test_cleanup_temp_file_on_failure(self, mock_probe, mock_run):
"""失败时清理临时文件."""
from video_processing.thumbnail_generator import extract_first_frame
with tempfile.NamedTemporaryFile(suffix=".mp4") as video:
Path(video.name).write_bytes(b"fake video")
with self.assertRaises(RuntimeError):
extract_first_frame(video.name)
import pytest
pytest.skip("RenderAdapterResult.cover_url 已被 cover_candidates 替代,测试待更新", allow_module_level=True)
class TestRenderAdapterCoverUrl(unittest.TestCase):
"""RenderAdapterResult.cover_url 字段测试."""
def test_result_has_cover_url_field(self):
"""RenderAdapterResult 包含 cover_url 字段."""
from video_processing.render_adapter import RenderAdapterResult
result = RenderAdapterResult(success=True, cover_url="https://example.com/cover.jpg")
self.assertEqual(result.cover_url, "https://example.com/cover.jpg")
def test_result_cover_url_defaults_empty(self):
"""cover_url 默认为空字符串."""
from video_processing.render_adapter import RenderAdapterResult
result = RenderAdapterResult(success=True)
self.assertEqual(result.cover_url, "")
if __name__ == "__main__":
unittest.main()