From 193788d7d2c1bd4f1241a574b8b5fdb08428be16 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 16:30:20 +0800 Subject: [PATCH] =?UTF-8?q?test(wave180):=20render=5Flayer=5Futils?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=9B=BE=E5=B1=82=E5=B7=A5=E5=85=B7=20+78?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/domain/test_render_layer_utils.py | 406 +++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100755 tests/unit/domain/test_render_layer_utils.py diff --git a/tests/unit/domain/test_render_layer_utils.py b/tests/unit/domain/test_render_layer_utils.py new file mode 100755 index 000000000..3b9ba9090 --- /dev/null +++ b/tests/unit/domain/test_render_layer_utils.py @@ -0,0 +1,406 @@ +"""渲染图层工具函数单测. + +纯函数模块,覆盖:图层角色映射、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 -- 2.54.0