"""缩略图生成器单元测试 - 纯逻辑函数.""" from __future__ import annotations import pytest from video_processing.thumbnail_generator import _format_seek_time class TestFormatSeekTime: """_format_seek_time 时间格式化测试.""" def test_zero_seconds(self): """0秒.""" result = _format_seek_time(0) assert result == "00:00:00.00" def test_less_than_one_second(self): """小于1秒.""" result = _format_seek_time(0.5) assert result == "00:00:00.50" def test_few_seconds(self): """几秒.""" result = _format_seek_time(5.5) assert result == "00:00:05.50" def test_one_minute(self): """1分钟.""" result = _format_seek_time(60.0) assert result == "00:01:00.00" def test_minutes_and_seconds(self): """分+秒.""" result = _format_seek_time(125.5) assert result == "00:02:05.50" def test_one_hour(self): """1小时.""" result = _format_seek_time(3600.0) assert result == "01:00:00.00" def test_hours_minutes_seconds(self): """时+分+秒.""" result = _format_seek_time(3725.25) assert result == "01:02:05.25" def test_long_duration(self): """长视频(2小时以上).""" result = _format_seek_time(7384.12) assert result == "02:03:04.12" def test_precision_two_decimal(self): """两位小数精度.""" result = _format_seek_time(3.14159) assert result == "00:00:03.14" def test_always_two_digit_hours(self): """小时始终两位数字.""" result = _format_seek_time(3600 * 9) assert result.startswith("09:") def test_always_two_digit_minutes(self): """分钟始终两位数字.""" result = _format_seek_time(300) # 5分钟 parts = result.split(":") assert parts[1] == "05" def test_float_input(self): """浮点数输入.""" result = _format_seek_time(10.0) assert isinstance(result, str) assert result == "00:00:10.00" def test_int_input(self): """整数输入.""" result = _format_seek_time(30) assert result == "00:00:30.00" def test_format_structure(self): """格式结构正确:HH:MM:SS.xx.""" result = _format_seek_time(3661.5) # 格式: HH:MM:SS.xx parts = result.split(":") assert len(parts) == 3 assert "." in parts[2] sec_parts = parts[2].split(".") assert len(sec_parts) == 2 assert len(sec_parts[1]) == 2 # 两位小数 # ── MediaKit 智能抽帧集成测试 ────────────────────────────────────────── class TestExtractFramesViaMediakit: """_extract_frames_via_mediakit 函数测试.""" def test_mediakit_not_configured_returns_none(self, tmp_path, monkeypatch): """MediaKit 未配置时返回 None.""" from video_processing.thumbnail_generator import _extract_frames_via_mediakit # Mock get_mediakit_client 返回不可用客户端 class FakeClient: is_available = False monkeypatch.setattr( "packages.shared.mediakit_client.get_mediakit_client", lambda: FakeClient(), ) video_file = tmp_path / "test.mp4" video_file.write_bytes(b"fake video") result = _extract_frames_via_mediakit(str(video_file), "plan1", 3) assert result is None def test_mediakit_success_returns_frames(self, tmp_path, monkeypatch): """MediaKit 成功时返回帧列表.""" from video_processing.thumbnail_generator import _extract_frames_via_mediakit class FakeClient: is_available = True def extract_frames(self, video_url, strategy, max_frames): return [ {"image_url": "https://example.com/frame1.jpg", "timestamp": 1.5}, {"image_url": "https://example.com/frame2.jpg", "timestamp": 3.2}, ] monkeypatch.setattr( "packages.shared.mediakit_client.get_mediakit_client", lambda: FakeClient(), ) # Mock upload_to_oss monkeypatch.setattr( "video_processing.oss_helpers.upload_to_oss", lambda path, key: f"https://oss.example.com/{key}", ) # Mock httpx.get for downloading frame import httpx class FakeResponse: status_code = 200 content = b"fake image data" def raise_for_status(self): pass monkeypatch.setattr("httpx.get", lambda url, **kw: FakeResponse()) video_file = tmp_path / "test.mp4" video_file.write_bytes(b"fake video") result = _extract_frames_via_mediakit(str(video_file), "plan1", 2) assert result is not None assert len(result) == 2 assert result[0]["timestamp"] == 1.5 assert result[1]["timestamp"] == 3.2 def test_mediakit_failure_returns_none(self, tmp_path, monkeypatch): """MediaKit 调用失败时返回 None.""" from video_processing.thumbnail_generator import _extract_frames_via_mediakit class FakeClient: is_available = True def extract_frames(self, video_url, strategy, max_frames): return None monkeypatch.setattr( "packages.shared.mediakit_client.get_mediakit_client", lambda: FakeClient(), ) monkeypatch.setattr( "video_processing.oss_helpers.upload_to_oss", lambda path, key: f"https://oss.example.com/{key}", ) video_file = tmp_path / "test.mp4" video_file.write_bytes(b"fake video") result = _extract_frames_via_mediakit(str(video_file), "plan1", 3) assert result is None class TestExtractAndUploadCoverFramesFallback: """extract_and_upload_cover_frames 降级逻辑测试.""" def test_fallback_to_ffmpeg_when_mediakit_fails(self, tmp_path, monkeypatch): """MediaKit 失败时降级到 ffmpeg 抽帧.""" from video_processing.thumbnail_generator import extract_and_upload_cover_frames # Mock MediaKit 返回 None(未配置或失败) class FakeClient: is_available = False monkeypatch.setattr( "packages.shared.mediakit_client.get_mediakit_client", lambda: FakeClient(), ) # Mock ffmpeg 抽帧 monkeypatch.setattr( "video_processing.thumbnail_generator.extract_first_frame", lambda video_path, output_path, **kw: output_path, ) # Mock upload monkeypatch.setattr( "video_processing.oss_helpers.upload_to_oss", lambda path, key: f"https://oss.example.com/{key}", ) # Mock probe_duration monkeypatch.setattr( "video_processing.ffmpeg_utils.probe_duration", lambda path: 60.0, ) video_file = tmp_path / "test.mp4" video_file.write_bytes(b"fake video") result = extract_and_upload_cover_frames(str(video_file), "plan1", num_frames=2) assert len(result) == 2 assert all("url" in item for item in result) assert all("position" in item for item in result)