"""测试 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 标记 → True(faststart 模式).""" 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