Files
xiaoxia-saas/tests/unit/test_video_validation.py
T
CI Bot a43f200183
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m19s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m53s
AI Code Review / AI Code Review (pull_request) Failing after 2m12s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m29s
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 API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E 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 20s
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 36s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 36s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1m41s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m32s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m14s
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 1m20s
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
style: auto-format with black + isort + prettier [skip ci-format-check]
2026-08-02 06:20:41 +00:00

334 lines
12 KiB
Python
Executable File
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.
"""测试 video_validation 模块 — 渲染输出校验."""
from __future__ import annotations
import json
import subprocess
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from video_processing.video_validation import (
FFMPEG_EXIT_CODES,
VideoValidationResult,
_check_moov_atom,
get_exit_code_message,
validate_video_output,
)
# ── get_exit_code_message 测试 ────────────────────────────────────────────────
class TestGetExitCodeMessage:
"""FFmpeg 退出码映射测试."""
def test_known_exit_codes(self):
"""已知退出码返回有意义的描述."""
assert "成功" in get_exit_code_message(0)
assert "通用错误" in get_exit_code_message(1)
assert "OOM" in get_exit_code_message(137)
assert "段错误" in get_exit_code_message(139)
assert "滤镜错误" in get_exit_code_message(183)
assert "素材异常" in get_exit_code_message(234)
def test_signal_termination(self):
"""信号终止(exit > 128 且不在映射表中)返回信号编号."""
msg = get_exit_code_message(130) # SIGINT = 130 - 128 = 2
assert "信号 2" in msg
def test_unknown_exit_code(self):
"""未知退出码返回通用错误."""
msg = get_exit_code_message(42)
assert "未知错误" in msg
assert "42" in msg
def test_all_mapped_codes_have_description(self):
"""所有映射的退出码都有名称和描述."""
for code, (name, desc) in FFMPEG_EXIT_CODES.items():
assert name, f"exit_code {code} 缺少名称"
assert desc, f"exit_code {code} 缺少描述"
# ── _check_moov_atom 测试 ─────────────────────────────────────────────────────
class TestCheckMoovAtom:
"""moov atom 检测测试."""
def test_file_with_moov_in_head(self):
"""文件头部包含 moov 标记 → Truefaststart 模式)."""
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
# 写入一些假数据,中间包含 moov 标记
f.write(b"\x00" * 100)
f.write(b"moov")
f.write(b"\x00" * 1000)
f.flush()
path = Path(f.name)
try:
assert _check_moov_atom(path) is True
finally:
path.unlink()
def test_file_with_moov_in_tail(self):
"""文件尾部包含 moov 标记 → True(普通 MP4 模式)."""
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
# 写一个较大的文件,moov 在尾部
f.write(b"\x00" * 100_000)
f.write(b"moov")
f.write(b"\x00" * 100)
f.flush()
path = Path(f.name)
try:
assert _check_moov_atom(path) is True
finally:
path.unlink()
def test_file_without_moov(self):
"""文件不含 moov 标记 → False(截断/损坏的 MP4."""
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
# 写入纯随机数据,不含 moov
f.write(b"\x00\x01\x02\x03" * 1000)
f.flush()
path = Path(f.name)
try:
assert _check_moov_atom(path) is False
finally:
path.unlink()
def test_empty_file(self):
"""空文件 → False."""
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
path = Path(f.name)
try:
assert _check_moov_atom(path) is False
finally:
path.unlink()
def test_tiny_file(self):
"""极小文件(< 8 bytes)→ False."""
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
f.write(b"\x00\x01")
f.flush()
path = Path(f.name)
try:
assert _check_moov_atom(path) is False
finally:
path.unlink()
def test_nonexistent_file(self):
"""不存在的文件 → False."""
assert _check_moov_atom(Path("/nonexistent/file.mp4")) is False
# ── validate_video_output 测试 ────────────────────────────────────────────────
class TestValidateVideoOutput:
"""渲染输出完整性校验测试."""
def test_file_not_exists(self):
"""文件不存在 → valid=False."""
result = validate_video_output("/nonexistent/video.mp4")
assert result.valid is False
assert result.file_exists is False
assert "不存在" in result.error_message
def test_empty_file(self):
"""空文件 → valid=False."""
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
path = Path(f.name)
try:
result = validate_video_output(path)
assert result.valid is False
assert "大小" in result.error_message or "过小" in result.error_message
finally:
path.unlink()
def test_tiny_file(self):
"""极小文件(< 1KB)→ valid=False."""
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
f.write(b"\x00" * 500)
path = Path(f.name)
try:
result = validate_video_output(path)
assert result.valid is False
assert "过小" in result.error_message
finally:
path.unlink()
def test_file_without_moov(self):
"""文件有大小但无 moov atom → valid=False."""
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
f.write(b"\x00\x01\x02\x03" * 2000) # 8KB, 无 moov
path = Path(f.name)
try:
result = validate_video_output(path)
assert result.valid is False
assert "moov" in result.error_message.lower()
finally:
path.unlink()
@patch("video_processing.video_validation.subprocess.run")
def test_valid_video(self, mock_run):
"""完整的有效视频 → valid=True."""
# mock ffprobe 返回
mock_result = MagicMock()
mock_result.stdout = json.dumps(
{
"streams": [
{
"width": 1080,
"height": 1920,
"duration": "10.5",
"codec_name": "h264",
}
],
"format": {"duration": "10.5"},
}
)
mock_result.returncode = 0
mock_run.return_value = mock_result
# 创建含 moov 的文件
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
f.write(b"\x00" * 100)
f.write(b"moov")
f.write(b"\x00" * 5000)
path = Path(f.name)
try:
result = validate_video_output(path)
assert result.valid is True
assert result.file_exists is True
assert result.moov_atom_found is True
assert result.has_video_stream is True
assert result.width == 1080
assert result.height == 1920
assert result.duration == 10.5
assert result.error_message == ""
finally:
path.unlink()
@patch("video_processing.video_validation.subprocess.run")
def test_no_video_stream(self, mock_run):
"""文件有 moov 但无视频流 → valid=False."""
mock_result = MagicMock()
mock_result.stdout = json.dumps({"streams": [], "format": {"duration": "0"}})
mock_result.returncode = 0
mock_run.return_value = mock_result
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
f.write(b"\x00" * 100)
f.write(b"moov")
f.write(b"\x00" * 5000)
path = Path(f.name)
try:
result = validate_video_output(path)
assert result.valid is False
assert "无视频流" in result.error_message
finally:
path.unlink()
@patch("video_processing.video_validation.subprocess.run")
def test_ffprobe_timeout(self, mock_run):
"""ffprobe 超时 → valid=False."""
mock_run.side_effect = subprocess.TimeoutExpired(cmd="ffprobe", timeout=15)
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
f.write(b"\x00" * 100)
f.write(b"moov")
f.write(b"\x00" * 5000)
path = Path(f.name)
try:
result = validate_video_output(path)
assert result.valid is False
assert "超时" in result.error_message
finally:
path.unlink()
@patch("video_processing.video_validation.subprocess.run")
def test_ffprobe_error(self, mock_run):
"""ffprobe 执行失败 → valid=False."""
mock_run.side_effect = subprocess.CalledProcessError(returncode=1, cmd="ffprobe", stderr="Invalid data found")
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
f.write(b"\x00" * 100)
f.write(b"moov")
f.write(b"\x00" * 5000)
path = Path(f.name)
try:
result = validate_video_output(path)
assert result.valid is False
assert "ffprobe" in result.error_message.lower()
finally:
path.unlink()
@patch("video_processing.video_validation.subprocess.run")
def test_short_video_warning(self, mock_run):
"""极短视频(< min_duration)→ 仍 valid=True 但有警告."""
mock_result = MagicMock()
mock_result.stdout = json.dumps(
{
"streams": [{"width": 100, "height": 100, "duration": "0.05", "codec_name": "h264"}],
"format": {"duration": "0.05"},
}
)
mock_result.returncode = 0
mock_run.return_value = mock_result
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
f.write(b"\x00" * 100)
f.write(b"moov")
f.write(b"\x00" * 5000)
path = Path(f.name)
try:
# 默认 min_duration=0.1,视频只有 0.05s → 应该仍然 valid(仅警告)
result = validate_video_output(path)
assert result.valid is True
finally:
path.unlink()
# ── VideoValidationResult 数据结构测试 ────────────────────────────────────────
class TestVideoValidationResult:
"""VideoValidationResult 数据结构测试."""
def test_default_invalid(self):
"""默认构造结果为无效."""
result = VideoValidationResult(valid=False)
assert result.valid is False
assert result.is_valid is False
assert result.file_size == 0
assert result.error_message == ""
def test_valid_result(self):
"""有效结果属性正确."""
result = VideoValidationResult(
valid=True,
file_exists=True,
file_size=1024000,
moov_atom_found=True,
has_video_stream=True,
duration=15.3,
width=1920,
height=1080,
)
assert result.is_valid is True
assert result.duration == 15.3
assert result.width == 1920