Files
xiaoxia-saas/tests/unit/test_cover_frame_pre_extract.py
T
xiaoxia 6dc8497083
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 / 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
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 34s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m13s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m21s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m32s
AI Code Review / AI Code Review (pull_request) Successful in 1m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m48s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m46s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m37s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 4m15s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 7m14s
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 / Integration Tests (pull_request) Successful in 4m58s
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
fix(test): expect Exception not RuntimeError in cleanup failure test
2026-08-14 21:23:03 +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=Exception("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(Exception):
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()