Files
xiaoxia-saas/tests/unit/test_cover_frame_pre_extract.py
T
xiaoxia 0301370dd8
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 / Frontend Unit Tests (push) Successful in 1m32s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m45s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m20s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m19s
CI/CD Pipeline / Integration Tests (push) Successful in 2m0s
CI/CD Pipeline / Unit Tests (push) Successful in 9m9s
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 / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 20m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m53s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m50s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
refactor: 统一封面生成管道 — 从渲染后视频抽帧作为封面 (#1371)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-14 22:16:00 +08:00

150 lines
6.2 KiB
Python
Raw 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)
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()