"""渲染图层工具函数单测. 纯函数模块,覆盖:图层角色映射、z_index、 clip时长计算、总时长估算、直通判断。 """ from __future__ import annotations from dataclasses import dataclass, field from packages.domain.render_layer_utils import ( LAYER_Z_INDEX, MAIN_LAYER_ROLES, PIP_DEFAULT_SCALE, can_pass_through, clip_adjusted_duration, clip_effective_duration, clip_playback_speed, estimate_total_duration, get_layer_z_index, resolve_layer_role, ) class TestConstants: def test_layer_z_index_keys(self): assert "background" in LAYER_Z_INDEX assert "broll" in LAYER_Z_INDEX assert "main" in LAYER_Z_INDEX assert "overlay" in LAYER_Z_INDEX assert "corner_voice" in LAYER_Z_INDEX assert "audio" in LAYER_Z_INDEX def test_layer_z_index_values(self): assert LAYER_Z_INDEX["background"] == -1 assert LAYER_Z_INDEX["broll"] == 0 assert LAYER_Z_INDEX["main"] == 0 assert LAYER_Z_INDEX["overlay"] == 1 assert LAYER_Z_INDEX["corner_voice"] == 1 assert LAYER_Z_INDEX["audio"] == 2 def test_pip_default_scale(self): assert PIP_DEFAULT_SCALE == 0.25 def test_main_layer_roles(self): assert "main" in MAIN_LAYER_ROLES assert "broll" in MAIN_LAYER_ROLES assert "background" in MAIN_LAYER_ROLES assert len(MAIN_LAYER_ROLES) == 3 class TestResolveLayerRole: def test_main_default(self): assert resolve_layer_role("main") == "main" def test_intro_maps_to_main(self): assert resolve_layer_role("intro") == "main" def test_outro_maps_to_main(self): assert resolve_layer_role("outro") == "main" def test_overlay(self): assert resolve_layer_role("overlay") == "overlay" def test_corner_voice(self): assert resolve_layer_role("corner_voice") == "corner_voice" def test_background(self): assert resolve_layer_role("background") == "background" def test_b_roll(self): assert resolve_layer_role("b_roll") == "broll" def test_main_with_b_roll_role(self): assert resolve_layer_role("main", {"role": "b_roll"}) == "broll" def test_main_with_audio_role(self): assert resolve_layer_role("main", {"role": "audio"}) == "audio" def test_main_with_unknown_role(self): assert resolve_layer_role("main", {"role": "something"}) == "main" def test_overlay_ignores_config_role(self): """overlay类型不受config.role影响.""" assert resolve_layer_role("overlay", {"role": "b_roll"}) == "overlay" def test_intro_ignores_config_role(self): """intro类型不受config.role影响.""" assert resolve_layer_role("intro", {"role": "audio"}) == "main" def test_none_config(self): assert resolve_layer_role("main", None) == "main" def test_empty_config(self): assert resolve_layer_role("main", {}) == "main" def test_unknown_clip_type_defaults_to_main(self): """未知clip_type走default分支返回main.""" assert resolve_layer_role("unknown_type") == "main" class TestGetLayerZIndex: def test_background(self): assert get_layer_z_index("background") == -1 def test_main(self): assert get_layer_z_index("main") == 0 def test_broll(self): assert get_layer_z_index("broll") == 0 def test_overlay(self): assert get_layer_z_index("overlay") == 1 def test_corner_voice(self): assert get_layer_z_index("corner_voice") == 1 def test_audio(self): assert get_layer_z_index("audio") == 2 def test_unknown_returns_zero(self): assert get_layer_z_index("unknown_role") == 0 def test_empty_string_returns_zero(self): assert get_layer_z_index("") == 0 class TestClipEffectiveDuration: def test_duration_only(self): """只有duration,没有actual,用duration.""" assert clip_effective_duration(5.0) == 5.0 def test_duration_less_than_actual(self): """duration < actual,取duration.""" assert clip_effective_duration(3.0, 5.0) == 3.0 def test_duration_greater_than_actual(self): """duration > actual,取actual.""" assert clip_effective_duration(10.0, 5.0) == 5.0 def test_duration_equal_to_actual(self): assert clip_effective_duration(5.0, 5.0) == 5.0 def test_zero_duration_with_actual(self): """duration=0表示使用完整素材,取actual.""" assert clip_effective_duration(0.0, 8.0) == 8.0 def test_negative_duration_with_actual(self): """duration<0也取actual.""" assert clip_effective_duration(-1.0, 8.0) == 8.0 def test_zero_duration_zero_actual(self): assert clip_effective_duration(0.0, 0.0) == 0.0 def test_zero_actual_uses_duration(self): """actual=0时,duration>0就用duration.""" assert clip_effective_duration(5.0, 0.0) == 5.0 def test_both_zero(self): assert clip_effective_duration(0.0) == 0.0 class TestClipPlaybackSpeed: def test_normal_speed(self): assert clip_playback_speed(1.0) == 1.0 def test_fast_speed(self): assert clip_playback_speed(2.0) == 2.0 def test_slow_speed(self): assert clip_playback_speed(0.5) == 0.5 def test_zero_speed_fallback(self): assert clip_playback_speed(0) == 1.0 def test_negative_speed_fallback(self): assert clip_playback_speed(-1.0) == 1.0 def test_string_fallback(self): assert clip_playback_speed("fast") == 1.0 def test_none_fallback(self): assert clip_playback_speed(None) == 1.0 def test_int_speed(self): assert clip_playback_speed(2) == 2.0 def test_list_fallback(self): assert clip_playback_speed([1, 2]) == 1.0 def test_dict_fallback(self): assert clip_playback_speed({"speed": 2}) == 1.0 class TestClipAdjustedDuration: def test_normal_speed_no_change(self): assert clip_adjusted_duration(5.0, 10.0, 1.0) == 5.0 def test_double_speed_half_duration(self): assert clip_adjusted_duration(4.0, 10.0, 2.0) == 2.0 def test_half_speed_double_duration(self): assert clip_adjusted_duration(4.0, 10.0, 0.5) == 8.0 def test_uses_effective_duration(self): """duration>actual时取actual,再调速.""" assert clip_adjusted_duration(10.0, 4.0, 2.0) == 2.0 def test_default_speed(self): assert clip_adjusted_duration(5.0, 10.0) == 5.0 def test_invalid_speed_fallback(self): assert clip_adjusted_duration(5.0, 10.0, "bad") == 5.0 def test_zero_speed_fallback(self): assert clip_adjusted_duration(5.0, 10.0, 0) == 5.0 def test_very_close_to_one_speed(self): """速度接近1.0时直接返回base,不做除法.""" result = clip_adjusted_duration(5.0, 10.0, 1.0000001) assert result == 5.0 def test_zero_duration_zero_actual(self): assert clip_adjusted_duration(0.0, 0.0, 1.0) == 0.0 @dataclass class FakeClip: duration: float = 0.0 actual_duration: float = 0.0 playback_speed: float = 1.0 @dataclass class FakeLayer: role: str = "main" clips: list[FakeClip] = field(default_factory=list) class TestEstimateTotalDuration: def test_single_main_layer_single_clip(self): layers = [ FakeLayer(role="main", clips=[FakeClip(duration=5.0, actual_duration=10.0)]), ] assert estimate_total_duration(layers) == 5.0 def test_single_main_layer_multiple_clips(self): layers = [ FakeLayer( role="main", clips=[ FakeClip(duration=3.0, actual_duration=5.0), FakeClip(duration=2.0, actual_duration=4.0), ], ), ] assert estimate_total_duration(layers) == 5.0 def test_with_transition_duration(self): layers = [ FakeLayer( role="main", clips=[ FakeClip(duration=5.0), FakeClip(duration=5.0), ], ), ] # 总10s - 1个转场 * 0.5s = 9.5s assert estimate_total_duration(layers, transition_duration=0.5) == 9.5 def test_transition_with_many_clips(self): layers = [ FakeLayer( role="main", clips=[FakeClip(duration=3.0) for _ in range(5)], ), ] # 5个3s = 15s,4个转场 * 0.5s = 2s,总13s assert estimate_total_duration(layers, transition_duration=0.5) == 13.0 def test_prefers_main_over_broll(self): """main图层优先级高于broll.""" layers = [ FakeLayer(role="broll", clips=[FakeClip(duration=10.0)]), FakeLayer(role="main", clips=[FakeClip(duration=5.0)]), ] assert estimate_total_duration(layers) == 5.0 def test_prefers_broll_over_background(self): layers = [ FakeLayer(role="background", clips=[FakeClip(duration=20.0)]), FakeLayer(role="broll", clips=[FakeClip(duration=10.0)]), ] assert estimate_total_duration(layers) == 10.0 def test_no_main_layer(self): layers = [ FakeLayer(role="overlay", clips=[FakeClip(duration=5.0)]), ] # 没有主图层,返回0 assert estimate_total_duration(layers) == 0.0 def test_empty_layers(self): assert estimate_total_duration([]) == 0.0 def test_main_layer_no_clips(self): layers = [FakeLayer(role="main", clips=[])] assert estimate_total_duration(layers) == 0.0 def test_minimum_total_duration(self): """总时长最小为0.1s.""" layers = [ FakeLayer(role="main", clips=[FakeClip(duration=0.01, actual_duration=0.01)]), ] # 转场把总时长减到接近0时,会被钳制到0.1 result = estimate_total_duration(layers, transition_duration=10.0) assert result == 0.1 def test_with_playback_speed(self): layers = [ FakeLayer( role="main", clips=[FakeClip(duration=4.0, playback_speed=2.0)], ), ] # 4s / 2x = 2s assert estimate_total_duration(layers) == 2.0 def test_multiple_layers_picks_first_main(self): layers = [ FakeLayer(role="overlay", clips=[FakeClip(duration=1.0)]), FakeLayer(role="main", clips=[FakeClip(duration=5.0)]), FakeLayer(role="main", clips=[FakeClip(duration=10.0)]), # 不看这个 ] assert estimate_total_duration(layers) == 5.0 class TestCanPassThrough: def test_single_main_layer_single_clip(self): layers = [ FakeLayer(role="main", clips=[FakeClip(duration=5.0)]), ] assert can_pass_through(layers) is True def test_single_broll_layer_single_clip(self): layers = [ FakeLayer(role="broll", clips=[FakeClip(duration=5.0)]), ] assert can_pass_through(layers) is True def test_single_background_layer_single_clip(self): layers = [ FakeLayer(role="background", clips=[FakeClip(duration=5.0)]), ] assert can_pass_through(layers) is True def test_multiple_layers_false(self): layers = [ FakeLayer(role="main", clips=[FakeClip(duration=5.0)]), FakeLayer(role="overlay", clips=[FakeClip(duration=3.0)]), ] assert can_pass_through(layers) is False def test_single_layer_multiple_clips_false(self): layers = [ FakeLayer( role="main", clips=[FakeClip(duration=3.0), FakeClip(duration=2.0)], ), ] assert can_pass_through(layers) is False def test_overlay_layer_false(self): """overlay不是主图层角色.""" layers = [ FakeLayer(role="overlay", clips=[FakeClip(duration=5.0)]), ] assert can_pass_through(layers) is False def test_has_stickers_false(self): layers = [ FakeLayer(role="main", clips=[FakeClip(duration=5.0)]), ] assert can_pass_through(layers, has_stickers=True) is False def test_has_watermark_false(self): layers = [ FakeLayer(role="main", clips=[FakeClip(duration=5.0)]), ] assert can_pass_through(layers, has_watermark=True) is False def test_both_stickers_and_watermark_false(self): layers = [ FakeLayer(role="main", clips=[FakeClip(duration=5.0)]), ] assert can_pass_through(layers, has_stickers=True, has_watermark=True) is False def test_empty_layers_false(self): assert can_pass_through([]) is False def test_corner_voice_layer_false(self): layers = [ FakeLayer(role="corner_voice", clips=[FakeClip(duration=5.0)]), ] assert can_pass_through(layers) is False