"""FFmpeg工具函数纯逻辑测试 — chain_filters / resolve_xfade_transition / build_xfade_filter_chain.""" from __future__ import annotations import pytest from video_processing.ffmpeg_utils import ( XFADE_TRANSITION_MAP, build_xfade_filter_chain, chain_filters, resolve_xfade_transition, ) class TestChainFilters: """chain_filters 滤镜串联测试.""" def test_single_filter(self): """单个滤镜.""" result = chain_filters(["scale=1280:720"], "v0") assert result == "[0:v]scale=1280:720[v0]" def test_multiple_filters(self): """多个滤镜用逗号连接.""" result = chain_filters(["scale=1280:720", "fps=25", "format=yuv420p"], "out") assert result == "[0:v]scale=1280:720,fps=25,format=yuv420p[out]" def test_empty_filters(self): """空滤镜列表.""" result = chain_filters([], "v0") assert result == "[0:v][v0]" def test_custom_input_label(self): """自定义输入标签.""" result = chain_filters(["scale=640:480"], "v1", input_label="1:v") assert result == "[1:v]scale=640:480[v1]" class TestResolveXfadeTransition: """resolve_xfade_transition 转场名称映射测试.""" def test_direct_match_fade(self): """fade直接匹配.""" assert resolve_xfade_transition("fade") == "fade" def test_direct_match_dissolve(self): """dissolve直接匹配.""" assert resolve_xfade_transition("dissolve") == "dissolve" def test_alias_crossfade(self): """crossfade别名→dissolve.""" assert resolve_xfade_transition("crossfade") == "dissolve" def test_alias_slide_left(self): """slide_left别名→slideleft.""" assert resolve_xfade_transition("slide_left") == "slideleft" def test_unknown_fallback_to_fade(self): """未知值回退到fade.""" assert resolve_xfade_transition("nonexistent_effect") == "fade" def test_empty_string_fallback(self): """空字符串回退.""" assert resolve_xfade_transition("") == "fade" def test_enum_value_support(self): """支持带value属性的枚举对象.""" class FakeEnum: value = "slideup" assert resolve_xfade_transition(FakeEnum()) == "slideup" def test_all_map_keys_resolve(self): """映射表中所有key都能解析到有效值.""" for key in XFADE_TRANSITION_MAP: result = resolve_xfade_transition(key) assert result and isinstance(result, str) assert result != "" def test_cut_is_special_fallback(self): """cut不在映射表中→回退到fade(硬切由调用方处理).""" # cut是特殊值,不在映射表里 result = resolve_xfade_transition("cut") # 不在映射表里就fallback到fade assert result == "fade" class TestBuildXfadeFilterChain: """build_xfade_filter_chain 转场滤镜链构建测试.""" def test_zero_clips(self): """0个片段→空字符串+0时长.""" filter_str, total_dur = build_xfade_filter_chain([], [], []) assert filter_str == "" assert total_dur == 0.0 def test_single_clip(self): """1个片段→直接copy,总时长等于片段时长.""" filter_str, total_dur = build_xfade_filter_chain([10.0], ["v0"], [], output_label="outv") assert "[v0]copy[outv]" in filter_str assert total_dur == pytest.approx(10.0) def test_two_clips_basic(self): """2个片段基本转场.""" filter_str, total_dur = build_xfade_filter_chain( [5.0, 5.0], ["v0", "v1"], ["", "fade"], transition_duration=0.5, output_label="outv", ) assert "xfade=transition=fade" in filter_str assert "offset=" in filter_str # 总时长 = 5 + 5 - 转场重叠 assert total_dur == pytest.approx(9.5) def test_three_clips_chain(self): """3个片段形成链式转场.""" filter_str, total_dur = build_xfade_filter_chain( [3.0, 4.0, 5.0], ["v0", "v1", "v2"], ["", "fade", "dissolve"], transition_duration=0.5, output_label="out", ) # 应该有2个xfade操作 assert filter_str.count("xfade=") == 2 assert "transition=fade" in filter_str assert "transition=dissolve" in filter_str # 总时长 = 3+4+5 - 2*0.5 = 11 assert total_dur == pytest.approx(11.0) def test_transition_duration_clamped_to_clip(self): """转场时长不能超过单个片段时长.""" filter_str, total_dur = build_xfade_filter_chain( [2.0, 1.0], ["v0", "v1"], ["", "fade"], transition_duration=3.0, # 比第二个片段还长 output_label="outv", ) # 转场时长被钳制到第二个片段时长(1.0) assert "duration=1.000" in filter_str assert total_dur == pytest.approx(2.0) # 2 + 1 - 1 = 2 def test_very_short_clip_min_transition(self): """极短片段至少保留1ms转场.""" filter_str, total_dur = build_xfade_filter_chain( [1.0, 0.0001], ["v0", "v1"], ["", "fade"], transition_duration=0.5, output_label="outv", ) # 至少有1ms assert "duration=0.001" in filter_str def test_transition_offset_calculation(self): """offset计算验证.""" filter_str, _ = build_xfade_filter_chain( [10.0, 10.0], ["v0", "v1"], ["", "fade"], transition_duration=1.0, output_label="outv", ) # offset = max(0, 10 - 1*1) = 9 assert "offset=9.000" in filter_str def test_fewer_transitions_than_clips(self): """转场列表比片段少时使用cut(fallback to fade).""" filter_str, total_dur = build_xfade_filter_chain( [5.0, 5.0, 5.0], ["v0", "v1", "v2"], ["fade"], # 只有1个转场,第2个转场缺省 transition_duration=0.5, output_label="out", ) # 应该有2个xfade assert filter_str.count("xfade=") == 2 # 第二个xfade的转场是cut→fade fallback assert filter_str.count("transition=fade") == 2 def test_output_label_final_clip(self): """最后一个xfade的输出标签是output_label.""" filter_str, _ = build_xfade_filter_chain( [3.0, 4.0, 5.0], ["v0", "v1", "v2"], ["", "fade", "slideleft"], output_label="final_v", ) assert filter_str.rstrip().endswith("[final_v]") def test_intermediate_labels(self): """中间步骤使用xf1, xf2等标签(从i=1开始计数).""" filter_str, _ = build_xfade_filter_chain( [2.0, 3.0, 4.0, 5.0], ["v0", "v1", "v2", "v3"], ["", "fade", "fade", "fade"], output_label="out", ) # 4个片段3次xfade,中间标签是xf1, xf2 assert "[xf1]" in filter_str assert "[xf2]" in filter_str # 最后一个是[out] assert filter_str.rstrip().endswith("[out]")