"""XFade 转场滤镜构建领域模型单元测试.""" from __future__ import annotations import pytest from packages.domain.xfade_builder import ( DEFAULT_TRANSITION_DURATION, SUPPORTED_TRANSITIONS, XFADE_TRANSITION_MAP, XFade_TRANSITION_NAMES, build_xfade_filter_chain, chain_filters, resolve_xfade_transition, ) # ── 常量测试 ───────────────────────────────────────────────────────────────── class TestConstants: def test_default_transition_duration(self): assert DEFAULT_TRANSITION_DURATION == 0.5 def test_xfade_transition_map_not_empty(self): assert len(XFADE_TRANSITION_MAP) > 0 def test_supported_transitions(self): assert "fade" in SUPPORTED_TRANSITIONS assert "dissolve" in SUPPORTED_TRANSITIONS def test_xfade_transition_names(self): assert "fade" in XFade_TRANSITION_NAMES assert "dissolve" in XFade_TRANSITION_NAMES # ── chain_filters 测试 ────────────────────────────────────────────────────── class TestChainFilters: 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"], "v0") assert result == "[0:v]scale=1280:720,fps=25[v0]" def test_empty_filters(self): result = chain_filters([], "out") assert result == "[0:v][out]" def test_custom_input_label(self): result = chain_filters(["scale=640:480"], "out", input_label="1:v") assert result == "[1:v]scale=640:480[out]" def test_three_filters(self): result = chain_filters(["trim=0:5", "setpts=PTS-STARTPTS", "fps=30"], "v1") assert result == "[0:v]trim=0:5,setpts=PTS-STARTPTS,fps=30[v1]" # ── resolve_xfade_transition 测试 ─────────────────────────────────────────── class TestResolveXfadeTransition: def test_fade(self): assert resolve_xfade_transition("fade") == "fade" def test_dissolve(self): assert resolve_xfade_transition("dissolve") == "dissolve" def test_crossfade_maps_to_dissolve(self): assert resolve_xfade_transition("crossfade") == "dissolve" def test_slideleft(self): assert resolve_xfade_transition("slideleft") == "slideleft" def test_slide_left_maps_to_slideleft(self): assert resolve_xfade_transition("slide_left") == "slideleft" def test_slide_default_left(self): assert resolve_xfade_transition("slide") == "slideleft" def test_slideup(self): assert resolve_xfade_transition("slideup") == "slideup" def test_zoom_maps_to_zoomin(self): assert resolve_xfade_transition("zoom") == "zoomin" def test_zoomin(self): assert resolve_xfade_transition("zoomin") == "zoomin" def test_wipe_default_left(self): assert resolve_xfade_transition("wipe") == "wipeleft" def test_wipeup(self): assert resolve_xfade_transition("wipeup") == "wipeup" def test_circle_maps_to_circlecrop(self): assert resolve_xfade_transition("circle") == "circlecrop" def test_rect_maps_to_rectcrop(self): assert resolve_xfade_transition("rect") == "rectcrop" def test_unknown_falls_back_to_fade(self): assert resolve_xfade_transition("nonexistent_effect") == "fade" def test_empty_string_falls_back_to_fade(self): assert resolve_xfade_transition("") == "fade" def test_enum_with_value_attribute(self): """测试带 .value 属性的枚举对象.""" class FakeEnum: def __init__(self, val): self.value = val assert resolve_xfade_transition(FakeEnum("fade")) == "fade" assert resolve_xfade_transition(FakeEnum("slideleft")) == "slideleft" assert resolve_xfade_transition(FakeEnum("unknown")) == "fade" # ── build_xfade_filter_chain 测试 ─────────────────────────────────────────── class TestBuildXfadeFilterChain: # ── 边界情况 ────────────────────────────────────────────────────── def test_empty_clips(self): result, duration = build_xfade_filter_chain([], [], []) assert result == "" assert duration == 0.0 def test_single_clip(self): result, duration = build_xfade_filter_chain([10.0], ["v0"], ["none"]) assert "copy" in result assert "[v0]copy[outv]" in result assert duration == 10.0 def test_single_clip_custom_output_label(self): result, duration = build_xfade_filter_chain([5.0], ["a0"], ["none"], output_label="final") assert "[a0]copy[final]" in result assert duration == 5.0 # ── 两片段基础测试 ──────────────────────────────────────────────── def test_two_clips_basic(self): result, duration = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"]) assert "xfade=transition=fade" in result assert "[v0][v1]" in result assert "[outv]" in result # 总时长 = 10 + 10 - 0.5 = 19.5 assert abs(duration - 19.5) < 0.01 def test_two_clips_custom_duration(self): result, duration = build_xfade_filter_chain( [10.0, 10.0], ["v0", "v1"], ["none", "fade"], transition_duration=1.0, ) assert "duration=1.000" in result # 总时长 = 10 + 10 - 1.0 = 19.0 assert abs(duration - 19.0) < 0.01 def test_two_clips_offset(self): """两片段时 offset 应该为 0(cumulative - td * 1 = 10 - 0.5 = 9.5?不对)。 对于两个片段: - cumulative = clip_durations[0] = 10.0 - offset = max(0, cumulative - td * i) = max(0, 10.0 - 0.5 * 1) = 9.5 - duration=0.5, offset=9.5 """ result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"]) assert "offset=9.500" in result # ── 多片段测试 ──────────────────────────────────────────────────── def test_three_clips(self): result, duration = build_xfade_filter_chain( [5.0, 5.0, 5.0], ["v0", "v1", "v2"], ["none", "fade", "dissolve"], ) # 应该有两个 xfade assert result.count("xfade=") == 2 # 第一个 xfade 输出标签 xf1,第二个 xfade 输出 outv assert "xf1" in result assert "[outv]" in result # 总时长 ≈ 5 + 5 + 5 - 0.5 - 0.5 = 14.0 assert abs(duration - 14.0) < 0.1 def test_five_clips(self): result, duration = build_xfade_filter_chain( [3.0, 3.0, 3.0, 3.0, 3.0], ["v0", "v1", "v2", "v3", "v4"], ["none", "fade", "fade", "fade", "fade"], ) assert result.count("xfade=") == 4 # 总时长 ≈ 15 - 4 * 0.5 = 13.0 assert abs(duration - 13.0) < 0.2 # ── 转场效果测试 ────────────────────────────────────────────────── def test_dissolve_transition(self): result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "dissolve"]) assert "transition=dissolve" in result def test_slideleft_transition(self): result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "slideleft"]) assert "transition=slideleft" in result def test_cut_uses_fade(self): """cut 转场效果应该回退到 fade.""" result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "cut"]) # cut 不是 XFADE_TRANSITION_MAP 的键,所以会回退到 fade assert "transition=fade" in result def test_transitions_shorter_than_clips(self): """如果 transitions 列表比 clips 短,剩余的用 'cut'(→ fade).""" result, _ = build_xfade_filter_chain( [5.0, 5.0, 5.0], ["v0", "v1", "v2"], ["none"], # 只有一个 ) # 第二个转场(index 2)会回退到 cut → fade assert result.count("transition=fade") == 2 # ── 时长钳制测试 ────────────────────────────────────────────────── def test_short_first_clip_truncates_transition(self): """第一个片段非常短,转场时长应该被钳制.""" result, duration = build_xfade_filter_chain( [0.3, 10.0], ["v0", "v1"], ["none", "fade"], transition_duration=1.0, ) # offset = max(0, 0.3 - 1.0 * 1) = 0.0 # available = max(0, 0.3 - 0.0) = 0.3 # safe_td = min(1.0, 0.3, 剩余 10.0, clip_durations[1] 10.0) = 0.3 assert "duration=0.300" in result assert abs(duration - 10.0) < 0.01 # 0.3 + 10.0 - 0.3 = 10.0 def test_zero_duration_clips(self): """零时长片段的边界情况.""" result, duration = build_xfade_filter_chain([0.0, 5.0], ["v0", "v1"], ["none", "fade"]) # 第一个片段 0 时长,转场时长应该被钳制到最小值 0.001 # offset = max(0, 0 - 0.5) = 0 # available = max(0, 0 - 0) = 0 # safe_td = min(0.5, 0, ...) = min(0.5, 0, 5.0, 5.0) = 0 → max(0.001, 0) = 0.001 assert "duration=0.001" in result def test_very_long_transition_duration(self): """转场时长超过所有片段时长.""" result, duration = build_xfade_filter_chain( [2.0, 2.0], ["v0", "v1"], ["none", "fade"], transition_duration=5.0, ) # offset = max(0, 2.0 - 5.0) = 0 # available = max(0, 2.0 - 0) = 2.0 # safe_td = min(5.0, 2.0, 剩余 2.0, 2.0) = 2.0 assert "duration=2.000" in result assert abs(duration - 2.0) < 0.01 # 2 + 2 - 2 = 2 # ── 标签测试 ────────────────────────────────────────────────────── def test_custom_labels(self): result, _ = build_xfade_filter_chain( [10.0, 10.0], ["clip_a", "clip_b"], ["none", "fade"], output_label="final_v", ) assert "[clip_a][clip_b]" in result assert "[final_v]" in result def test_intermediate_labels_three_clips(self): result, _ = build_xfade_filter_chain([5.0, 5.0, 5.0], ["v0", "v1", "v2"], ["none", "fade", "fade"]) # 第一个 xfade 输出 xf1 assert "[xf1][v2]" in result or result.count("[xf1]") >= 1 # ── 总时长计算验证 ──────────────────────────────────────────────── def test_total_duration_two_equal_clips(self): _, duration = build_xfade_filter_chain([8.0, 8.0], ["v0", "v1"], ["none", "fade"]) # 8 + 8 - 0.5 = 15.5 assert abs(duration - 15.5) < 0.01 def test_total_duration_no_transition_impossible(self): """即使 transition_duration=0,也有最小 0.001 的钳制.""" _, duration = build_xfade_filter_chain( [10.0, 10.0], ["v0", "v1"], ["none", "fade"], transition_duration=0.0, ) # transition_duration=0,但 safe_td 有下限 0.001 assert duration < 20.0 # 应该小于 20(有重叠) assert duration > 19.9 # 但接近 20 # ── 滤镜字符串格式验证 ──────────────────────────────────────────── def test_filter_format_contains_xfade_keyword(self): result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"]) assert "xfade=" in result def test_filter_uses_semicolon_separator(self): """多步 xfade 之间用分号分隔.""" result, _ = build_xfade_filter_chain([5.0, 5.0, 5.0], ["v0", "v1", "v2"], ["none", "fade", "fade"]) assert ";" in result # 3个片段 → 2个xfade → 1个分号 assert result.count("xfade=") == 2 assert result.count(";") == 1 def test_filter_has_transition_param(self): result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"]) assert "transition=fade" in result def test_filter_has_duration_param(self): result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"]) assert "duration=" in result def test_filter_has_offset_param(self): result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"]) assert "offset=" in result # ── 各种转场效果遍历测试 ────────────────────────────────────────── @pytest.mark.parametrize( "transition_name", list(XFADE_TRANSITION_MAP.keys()), ) def test_all_supported_transitions(self, transition_name): """所有支持的转场效果都应该能正确生成滤镜.""" result, duration = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", transition_name]) expected = XFADE_TRANSITION_MAP[transition_name] assert f"transition={expected}" in result assert duration > 0 # ── 四片段复杂场景 ──────────────────────────────────────────────── def test_four_clips_different_durations(self): durations = [3.0, 5.0, 2.0, 7.0] result, duration = build_xfade_filter_chain( durations, ["v0", "v1", "v2", "v3"], ["none", "fade", "dissolve", "slideleft"], ) assert result.count("xfade=") == 3 # 总时长 = sum(durations) - 3 * 0.5 ≈ 17 - 1.5 = 15.5 assert abs(duration - 15.5) < 0.2 # ── transition_duration = 0 的边界 ─────────────────────────────── def test_zero_transition_duration_minimum_clamped(self): result, _ = build_xfade_filter_chain( [10.0, 10.0], ["v0", "v1"], ["none", "fade"], transition_duration=0.0, ) # 至少 0.001 assert "duration=0.001" in result # ── 单片段自定义输出标签 ───────────────────────────────────────── def test_single_clip_output_label(self): result, _ = build_xfade_filter_chain([5.0], ["v0"], ["none"], output_label="result") assert "[v0]copy[result]" in result