"""video_concat 视频拼接配置单测.""" import pytest from domain.video_concat import ( ALLOWED_VIDEO_EXTENSIONS, CONCAT_DEMUXER_REQUIRED_PARAMS, MAX_CONCAT_SEGMENTS, ConcatConfig, ConcatSegment, ) # ── 常量测试 ───────────────────────────────────────────────────────────────── class TestConstants: """模块常量""" def test_max_concat_segments(self): assert MAX_CONCAT_SEGMENTS == 50 def test_allowed_extensions(self): assert ".mp4" in ALLOWED_VIDEO_EXTENSIONS assert ".mov" in ALLOWED_VIDEO_EXTENSIONS assert ".avi" in ALLOWED_VIDEO_EXTENSIONS assert ".mkv" in ALLOWED_VIDEO_EXTENSIONS assert ".webm" in ALLOWED_VIDEO_EXTENSIONS assert ".flv" in ALLOWED_VIDEO_EXTENSIONS assert ".wmv" in ALLOWED_VIDEO_EXTENSIONS def test_concat_demuxer_params(self): params = CONCAT_DEMUXER_REQUIRED_PARAMS assert "codec_name" in params assert "width" in params assert "height" in params assert "r_frame_rate" in params assert "pix_fmt" in params assert "sample_rate" in params assert "channels" in params assert "audio_codec" in params assert len(params) == 8 # ── ConcatSegment ──────────────────────────────────────────────────────────── class TestConcatSegmentDefaults: """ConcatSegment 默认值""" def test_required_path(self): s = ConcatSegment(video_path="/video.mp4") assert s.video_path == "/video.mp4" assert s.start_time == 0.0 assert s.duration == 0.0 assert s.has_audio is True def test_all_custom(self): s = ConcatSegment( video_path="/clip.mp4", start_time=5.0, duration=10.0, has_audio=False, ) assert s.video_path == "/clip.mp4" assert s.start_time == 5.0 assert s.duration == 10.0 assert s.has_audio is False class TestConcatSegmentFromDict: """ConcatSegment.from_dict""" def test_none_returns_empty_path(self): s = ConcatSegment.from_dict(None) assert s.video_path == "" assert s.is_valid is False def test_empty_dict(self): s = ConcatSegment.from_dict({}) assert s.video_path == "" def test_not_dict(self): s = ConcatSegment.from_dict("not a dict") assert s.video_path == "" def test_full_dict(self): s = ConcatSegment.from_dict( { "video_path": "/clip.mp4", "start_time": 2.5, "duration": 15.0, "has_audio": False, } ) assert s.video_path == "/clip.mp4" assert s.start_time == 2.5 assert s.duration == 15.0 assert s.has_audio is False def test_invalid_start_time_falls_back(self): s = ConcatSegment.from_dict({"video_path": "/a.mp4", "start_time": "bad"}) assert s.start_time == 0.0 def test_negative_start_time_clamped(self): s = ConcatSegment.from_dict({"video_path": "/a.mp4", "start_time": -5.0}) assert s.start_time == 0.0 def test_invalid_duration_falls_back(self): s = ConcatSegment.from_dict({"video_path": "/a.mp4", "duration": None}) assert s.duration == 0.0 def test_negative_duration_clamped(self): s = ConcatSegment.from_dict({"video_path": "/a.mp4", "duration": -10.0}) assert s.duration == 0.0 def test_has_audio_default_true(self): s = ConcatSegment.from_dict({"video_path": "/a.mp4"}) assert s.has_audio is True def test_has_audio_false(self): s = ConcatSegment.from_dict({"video_path": "/a.mp4", "has_audio": False}) assert s.has_audio is False def test_path_is_string(self): s = ConcatSegment.from_dict({"video_path": 123}) assert s.video_path == "123" class TestConcatSegmentProperties: """ConcatSegment 属性方法""" def test_is_valid_true(self): s = ConcatSegment(video_path="/a.mp4") assert s.is_valid is True def test_is_valid_false_empty(self): s = ConcatSegment(video_path="") assert s.is_valid is False def test_effective_duration_positive(self): s = ConcatSegment(video_path="/a.mp4", duration=10.0) assert s.effective_duration == 10.0 def test_effective_duration_zero(self): s = ConcatSegment(video_path="/a.mp4", duration=0.0) assert s.effective_duration == 0.0 def test_effective_duration_negative(self): s = ConcatSegment(video_path="/a.mp4", duration=-5.0) assert s.effective_duration == 0.0 # ── ConcatConfig ───────────────────────────────────────────────────────────── class TestConcatConfigDefaults: """ConcatConfig 默认值""" def test_default_values(self): c = ConcatConfig() assert c.segments == [] assert c.output_width == 0 assert c.output_height == 0 assert c.output_fps == 0.0 assert c.force_reencode is False assert c.transition == "none" assert c.transition_duration == 0.3 class TestConcatConfigFromDict: """ConcatConfig.from_config_dict""" def test_none_returns_default(self): c = ConcatConfig.from_config_dict(None) assert c.segments == [] def test_empty_dict_returns_default(self): c = ConcatConfig.from_config_dict({}) assert c.segments == [] def test_not_dict_returns_default(self): c = ConcatConfig.from_config_dict("config") assert c.segments == [] def test_single_segment(self): c = ConcatConfig.from_config_dict( { "segments": [ {"video_path": "/a.mp4", "duration": 10.0}, ], } ) assert len(c.segments) == 1 assert c.segments[0].video_path == "/a.mp4" def test_multiple_segments(self): c = ConcatConfig.from_config_dict( { "segments": [ {"video_path": "/a.mp4", "duration": 10.0}, {"video_path": "/b.mp4", "duration": 20.0}, {"video_path": "/c.mp4", "duration": 15.0}, ], } ) assert len(c.segments) == 3 def test_skip_no_path_segments(self): c = ConcatConfig.from_config_dict( { "segments": [ {"video_path": "/a.mp4"}, {"video_path": ""}, {"duration": 5.0}, # 没有 video_path {"video_path": "/b.mp4"}, ], } ) assert len(c.segments) == 2 def test_skip_non_dict_segments(self): c = ConcatConfig.from_config_dict( { "segments": [ {"video_path": "/a.mp4"}, "not a dict", 123, None, {"video_path": "/b.mp4"}, ], } ) assert len(c.segments) == 2 def test_output_resolution(self): c = ConcatConfig.from_config_dict( { "segments": [{"video_path": "/a.mp4"}], "output_width": 1920, "output_height": 1080, } ) assert c.output_width == 1920 assert c.output_height == 1080 def test_output_width_clamped(self): c = ConcatConfig.from_config_dict({"output_width": -100}) assert c.output_width == 0 def test_invalid_output_width_falls_back(self): c = ConcatConfig.from_config_dict({"output_width": "wide"}) assert c.output_width == 0 def test_output_fps(self): c = ConcatConfig.from_config_dict({"output_fps": 30.0}) assert c.output_fps == 30.0 def test_output_fps_clamped(self): c = ConcatConfig.from_config_dict({"output_fps": -1.0}) assert c.output_fps == 0.0 def test_invalid_output_fps_falls_back(self): c = ConcatConfig.from_config_dict({"output_fps": "fast"}) assert c.output_fps == 0.0 def test_force_reencode_true(self): c = ConcatConfig.from_config_dict({"force_reencode": True}) assert c.force_reencode is True def test_transition_crossfade(self): c = ConcatConfig.from_config_dict({"transition": "crossfade"}) assert c.transition == "crossfade" def test_transition_duration(self): c = ConcatConfig.from_config_dict({"transition_duration": 1.0}) assert c.transition_duration == 1.0 def test_transition_duration_min_clamped(self): c = ConcatConfig.from_config_dict({"transition_duration": 0.01}) # max(0.1, 0.01) = 0.1 assert c.transition_duration == 0.1 # 代码里 transition_duration = max(0.1, ...),默认 0.3 # 0.01 < 0.1 ,所以被钳制到 0.1 def test_invalid_transition_duration_falls_back(self): c = ConcatConfig.from_config_dict({"transition_duration": "long"}) assert c.transition_duration == 0.3 def test_segments_not_list_ignored(self): c = ConcatConfig.from_config_dict({"segments": "not a list"}) assert c.segments == [] class TestConcatConfigProperties: """ConcatConfig 属性方法""" def _make_config(self, n=3): return ConcatConfig.from_config_dict( { "segments": [{"video_path": f"/s{i}.mp4", "duration": 10.0 + i} for i in range(n)], } ) def test_has_effect_true(self): c = self._make_config(3) assert c.has_effect is True def test_has_effect_false_one_segment(self): c = self._make_config(1) assert c.has_effect is False def test_has_effect_false_empty(self): c = ConcatConfig() assert c.has_effect is False def test_valid_segment_count(self): c = self._make_config(5) assert c.valid_segment_count == 5 def test_total_segments_alias(self): c = self._make_config(4) assert c.total_segments == 4 assert c.total_segments == c.valid_segment_count def test_first_valid_segment(self): c = self._make_config(3) first = c.first_valid_segment assert first is not None assert first.video_path == "/s0.mp4" def test_first_valid_segment_empty(self): c = ConcatConfig() assert c.first_valid_segment is None def test_estimated_total_duration(self): c = ConcatConfig( segments=[ ConcatSegment("/a.mp4", duration=10.0), ConcatSegment("/b.mp4", duration=20.0), ConcatSegment("/c.mp4", duration=0.0), # 不计入 ] ) assert c.estimated_total_duration == 30.0 def test_estimated_total_duration_empty(self): c = ConcatConfig() assert c.estimated_total_duration == 0.0 def test_clamp_segments_within_limit(self): c = self._make_config(10) original = len(c.segments) c.clamp_segments(max_segments=50) assert len(c.segments) == original def test_clamp_segments_over_limit(self): c = self._make_config(10) c.clamp_segments(max_segments=3) assert len(c.segments) == 3 assert c.segments[0].video_path == "/s0.mp4" assert c.segments[2].video_path == "/s2.mp4" def test_clamp_segments_default_max(self): # 默认应该是 MAX_CONCAT_SEGMENTS c = ConcatConfig(segments=[ConcatSegment(f"/s{i}.mp4") for i in range(100)]) c.clamp_segments() assert len(c.segments) == MAX_CONCAT_SEGMENTS class TestTransitionDurationClamp: """transition_duration 钳制边界""" def test_min_boundary_01(self): c = ConcatConfig.from_config_dict({"transition_duration": 0.1}) assert c.transition_duration == 0.1 def test_below_min_clamped(self): c = ConcatConfig.from_config_dict({"transition_duration": 0.05}) # max(0.1, 0.05) = 0.1 assert c.transition_duration == 0.1 def test_large_duration_ok(self): c = ConcatConfig.from_config_dict({"transition_duration": 5.0}) assert c.transition_duration == 5.0 def test_zero_clamped(self): c = ConcatConfig.from_config_dict({"transition_duration": 0.0}) # max(0.1, 0.0) = 0.1 assert c.transition_duration == 0.1 def test_negative_clamped(self): c = ConcatConfig.from_config_dict({"transition_duration": -1.0}) # max(0.1, -1.0) = 0.1 assert c.transition_duration == 0.1