"""render_layer_utils 模块单元测试.""" from __future__ import annotations from dataclasses import dataclass, field from typing import Any import pytest 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, ) # ── 辅助数据类 ────────────────────────────────────────────────────────────── @dataclass class FakeClip: duration: float = 0.0 actual_duration: float = 0.0 playback_speed: Any = 1.0 @dataclass class FakeLayer: role: str = "main" clips: list[FakeClip] = field(default_factory=list) # ── 常量验证 ──────────────────────────────────────────────────────────────── class TestConstants: def test_layer_z_index_has_expected_keys(self): assert set(LAYER_Z_INDEX.keys()) == { "background", "broll", "main", "overlay", "corner_voice", "audio", } def test_layer_z_index_ordering(self): assert LAYER_Z_INDEX["background"] < LAYER_Z_INDEX["main"] assert LAYER_Z_INDEX["main"] == LAYER_Z_INDEX["broll"] assert LAYER_Z_INDEX["overlay"] > LAYER_Z_INDEX["main"] assert LAYER_Z_INDEX["corner_voice"] > LAYER_Z_INDEX["main"] assert LAYER_Z_INDEX["audio"] > LAYER_Z_INDEX["overlay"] def test_pip_default_scale_positive(self): assert 0 < PIP_DEFAULT_SCALE < 1 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 "overlay" not in MAIN_LAYER_ROLES # ── resolve_layer_role ────────────────────────────────────────────────────── class TestResolveLayerRole: 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_maps_to_overlay(self): assert resolve_layer_role("overlay") == "overlay" def test_corner_voice_maps_to_corner_voice(self): assert resolve_layer_role("corner_voice") == "corner_voice" def test_background_maps_to_background(self): assert resolve_layer_role("background") == "background" def test_b_roll_maps_to_broll(self): assert resolve_layer_role("b_roll") == "broll" def test_main_defaults_to_main(self): assert resolve_layer_role("main") == "main" 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_other_role_stays_main(self): assert resolve_layer_role("main", {"role": "overlay"}) == "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_type_defaults_to_main(self): assert resolve_layer_role("unknown_type") == "main" # ── get_layer_z_index ────────────────────────────────────────────────────── class TestGetLayerZIndex: def test_known_roles(self): for role, expected in LAYER_Z_INDEX.items(): assert get_layer_z_index(role) == expected def test_unknown_role_returns_zero(self): assert get_layer_z_index("nonexistent") == 0 def test_empty_string_returns_zero(self): assert get_layer_z_index("") == 0 # ── clip_effective_duration ──────────────────────────────────────────────── class TestClipEffectiveDuration: def test_explicit_duration_no_actual(self): assert clip_effective_duration(5.0) == 5.0 def test_explicit_duration_with_shorter_actual(self): assert clip_effective_duration(5.0, 3.0) == 3.0 def test_explicit_duration_with_longer_actual(self): assert clip_effective_duration(5.0, 10.0) == 5.0 def test_zero_duration_uses_actual(self): assert clip_effective_duration(0, 8.0) == 8.0 def test_negative_duration_uses_actual(self): 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 def test_no_args_returns_zero(self): assert clip_effective_duration(0) == 0.0 def test_equal_duration_and_actual(self): assert clip_effective_duration(5.0, 5.0) == 5.0 # ── clip_playback_speed ──────────────────────────────────────────────────── 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_defaults_to_one(self): assert clip_playback_speed(0) == 1.0 def test_negative_speed_defaults_to_one(self): assert clip_playback_speed(-1.0) == 1.0 def test_none_defaults_to_one(self): assert clip_playback_speed(None) == 1.0 def test_string_defaults_to_one(self): assert clip_playback_speed("fast") == 1.0 def test_int_speed(self): assert clip_playback_speed(2) == 2.0 # ── clip_adjusted_duration ───────────────────────────────────────────────── class TestClipAdjustedDuration: def test_normal_speed_same_as_effective(self): assert clip_adjusted_duration(5.0, 10.0, 1.0) == 5.0 def test_double_speed_half_duration(self): assert clip_adjusted_duration(10.0, 10.0, 2.0) == pytest.approx(5.0) def test_half_speed_double_duration(self): assert clip_adjusted_duration(5.0, 10.0, 0.5) == pytest.approx(10.0) def test_invalid_speed_uses_default(self): assert clip_adjusted_duration(5.0, 10.0, 0) == 5.0 def test_zero_duration(self): assert clip_adjusted_duration(0, 0, 1.0) == 0.0 def test_actual_duration_only(self): assert clip_adjusted_duration(0, 8.0, 1.0) == 8.0 def test_actual_duration_only_with_speed(self): assert clip_adjusted_duration(0, 8.0, 2.0) == pytest.approx(4.0) def test_very_close_to_normal_speed(self): # 1.0000001 应该被认为接近 1.0,不做除法 result = clip_adjusted_duration(5.0, 10.0, 1.0 + 1e-10) assert result == 5.0 # ── estimate_total_duration ──────────────────────────────────────────────── class TestEstimateTotalDuration: def test_empty_layers(self): assert estimate_total_duration([]) == 0.0 def test_no_main_layer(self): layers = [FakeLayer(role="overlay", clips=[FakeClip(duration=5.0)])] assert estimate_total_duration(layers) == 0.0 def test_single_clip_main_layer(self): layers = [FakeLayer(role="main", clips=[FakeClip(duration=5.0)])] assert estimate_total_duration(layers) == pytest.approx(5.0) def test_multiple_clips_no_transition(self): layers = [ FakeLayer( role="main", clips=[ FakeClip(duration=3.0), FakeClip(duration=2.0), FakeClip(duration=5.0), ], ) ] assert estimate_total_duration(layers) == pytest.approx(10.0) def test_multiple_clips_with_transition(self): layers = [ FakeLayer( role="main", clips=[ FakeClip(duration=3.0), FakeClip(duration=2.0), FakeClip(duration=5.0), ], ) ] # 3 + 2 + 5 - 2 * 0.5 = 9.0 assert estimate_total_duration(layers, transition_duration=0.5) == pytest.approx(9.0) def test_prefers_main_over_broll(self): layers = [ FakeLayer(role="broll", clips=[FakeClip(duration=10.0)]), FakeLayer(role="main", clips=[FakeClip(duration=5.0)]), ] assert estimate_total_duration(layers) == pytest.approx(5.0) def test_prefers_broll_over_background(self): layers = [ FakeLayer(role="background", clips=[FakeClip(duration=10.0)]), FakeLayer(role="broll", clips=[FakeClip(duration=5.0)]), ] assert estimate_total_duration(layers) == pytest.approx(5.0) def test_main_layer_empty_clips(self): layers = [FakeLayer(role="main", clips=[])] assert estimate_total_duration(layers) == 0.0 def test_minimum_duration(self): layers = [ FakeLayer( role="main", clips=[ FakeClip(duration=0.01), FakeClip(duration=0.01), ], ) ] result = estimate_total_duration(layers, transition_duration=0.5) assert result >= 0.1 def test_with_playback_speed(self): layers = [ FakeLayer( role="main", clips=[ FakeClip(duration=10.0, playback_speed=2.0), FakeClip(duration=10.0, playback_speed=0.5), ], ) ] # 5 + 20 = 25 assert estimate_total_duration(layers) == pytest.approx(25.0) # ── can_pass_through ────────────────────────────────────────────────────── class TestCanPassThrough: def test_single_main_clip_no_effects(self): layers = [FakeLayer(role="main", clips=[FakeClip(duration=5.0)])] assert can_pass_through(layers) is True def test_single_broll_clip(self): layers = [FakeLayer(role="broll", clips=[FakeClip(duration=5.0)])] assert can_pass_through(layers) is True def test_single_background_clip(self): layers = [FakeLayer(role="background", clips=[FakeClip(duration=5.0)])] assert can_pass_through(layers) is True def test_multiple_layers(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_overlay_layer(self): layers = [FakeLayer(role="overlay", clips=[FakeClip(duration=5.0)])] assert can_pass_through(layers) is False def test_multiple_clips_in_layer(self): layers = [ FakeLayer( role="main", clips=[ FakeClip(duration=3.0), FakeClip(duration=2.0), ], ) ] assert can_pass_through(layers) is False def test_with_stickers(self): layers = [FakeLayer(role="main", clips=[FakeClip(duration=5.0)])] assert can_pass_through(layers, has_stickers=True) is False def test_with_watermark(self): layers = [FakeLayer(role="main", clips=[FakeClip(duration=5.0)])] assert can_pass_through(layers, has_watermark=True) is False def test_with_stickers_and_watermark(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_layer_list(self): assert can_pass_through([]) is False def test_audio_layer_only(self): layers = [FakeLayer(role="audio", clips=[FakeClip(duration=5.0)])] assert can_pass_through(layers) is False