"""intro_outro_config 模块单测 — 纯逻辑,无外部依赖.""" from __future__ import annotations import pytest from packages.domain.intro_outro_config import ( INTRO_OUTRO_TYPE_FOLLOW, INTRO_OUTRO_TYPE_NONE, INTRO_OUTRO_TYPE_TEXT, INTRO_OUTRO_TYPE_VIDEO, TRANSITION_FADE, IntroOutroConfig, ) # ── 默认值 ──────────────────────────────────────────────────────────────────── class TestIntroOutroConfigDefaults: def test_default_disabled(self): cfg = IntroOutroConfig() assert cfg.enabled is False assert cfg.intro_type == INTRO_OUTRO_TYPE_NONE assert cfg.outro_type == INTRO_OUTRO_TYPE_NONE assert cfg.transition_effect == TRANSITION_FADE assert cfg.transition_duration == 0.5 def test_default_intro_text(self): cfg = IntroOutroConfig() assert cfg.intro_background == "#000000" assert cfg.intro_title == "" assert cfg.intro_subtitle == "" assert cfg.intro_title_color == "white" assert cfg.intro_title_size == 48 assert cfg.intro_subtitle_color == "gray" assert cfg.intro_subtitle_size == 24 assert cfg.intro_duration == 3.0 def test_default_outro_text(self): cfg = IntroOutroConfig() assert cfg.outro_background == "#000000" assert cfg.outro_title == "感谢观看" assert cfg.outro_subtitle == "点赞关注不迷路" assert cfg.outro_title_color == "white" assert cfg.outro_title_size == 48 assert cfg.outro_subtitle_color == "gray" assert cfg.outro_subtitle_size == 24 assert cfg.outro_duration == 3.0 # ── from_dict ──────────────────────────────────────────────────────────────── class TestIntroOutroConfigFromDict: def test_none_data_disabled(self): cfg = IntroOutroConfig.from_dict(None) assert cfg.enabled is False def test_empty_dict_disabled(self): cfg = IntroOutroConfig.from_dict({}) assert cfg.enabled is False def test_enabled_false(self): cfg = IntroOutroConfig.from_dict({"enabled": False}) assert cfg.enabled is False def test_enabled_but_no_intro_outro(self): cfg = IntroOutroConfig.from_dict({"enabled": True}) assert cfg.enabled is True assert cfg.has_intro is False assert cfg.has_outro is False def test_intro_video(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "intro": { "type": "video", "video_path": "/tmp/intro.mp4", "duration": 2.5, }, } ) assert cfg.enabled is True assert cfg.intro_type == "video" assert cfg.intro_video_path == "/tmp/intro.mp4" assert cfg.intro_duration == 2.5 assert cfg.has_intro is True def test_intro_text(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "intro": { "type": "text", "title": "欢迎来到", "subtitle": "我的频道", "background": "#FF0000", "title_color": "yellow", "title_size": 64, "subtitle_color": "white", "subtitle_size": 32, }, } ) assert cfg.intro_type == "text" assert cfg.intro_title == "欢迎来到" assert cfg.intro_subtitle == "我的频道" assert cfg.intro_background == "#FF0000" assert cfg.intro_title_size == 64 assert cfg.intro_subtitle_size == 32 assert cfg.has_intro is True def test_intro_video_path_alias(self): # video 和 video_path 都支持 cfg = IntroOutroConfig.from_dict( { "enabled": True, "intro": {"type": "video", "video": "/tmp/a.mp4"}, } ) assert cfg.intro_video_path == "/tmp/a.mp4" def test_outro_video(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "outro": { "type": "video", "video_path": "/tmp/outro.mp4", "duration": 4.0, }, } ) assert cfg.outro_type == "video" assert cfg.outro_video_path == "/tmp/outro.mp4" assert cfg.outro_duration == 4.0 assert cfg.has_outro is True def test_outro_text(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "outro": { "type": "text", "title": "谢谢观看", "subtitle": "下期再见", }, } ) assert cfg.outro_type == "text" assert cfg.outro_title == "谢谢观看" assert cfg.outro_subtitle == "下期再见" assert cfg.has_outro is True def test_outro_follow(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "outro": {"type": "follow", "title": "关注我"}, } ) assert cfg.outro_type == "follow" assert cfg.has_outro is True def test_outro_default_title_when_empty(self): # 空字符串标题会回退到默认值 cfg = IntroOutroConfig.from_dict( { "enabled": True, "outro": {"type": "text", "title": ""}, } ) assert cfg.outro_title == "感谢观看" def test_outro_default_subtitle_when_empty(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "outro": {"type": "text", "subtitle": ""}, } ) assert cfg.outro_subtitle == "点赞关注不迷路" def test_transition_config(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "transition": "slide", "transition_duration": 1.0, } ) assert cfg.transition_effect == "slide" assert cfg.transition_duration == 1.0 def test_invalid_intro_duration_fallback(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "intro": {"type": "text", "title": "hi", "duration": "bad"}, } ) assert cfg.intro_duration == 3.0 def test_invalid_outro_duration_fallback(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "outro": {"type": "text", "title": "hi", "duration": "bad"}, } ) assert cfg.outro_duration == 3.0 def test_invalid_title_size_fallback(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "intro": {"type": "text", "title": "hi", "title_size": "bad"}, } ) assert cfg.intro_title_size == 48 def test_invalid_transition_duration_fallback(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "transition_duration": "bad", } ) assert cfg.transition_duration == 0.5 def test_intro_is_none_dict(self): # intro 可能是 None cfg = IntroOutroConfig.from_dict( { "enabled": True, "intro": None, "outro": None, } ) assert cfg.enabled is True assert cfg.intro_type == "none" def test_full_config(self): cfg = IntroOutroConfig.from_dict( { "enabled": True, "intro": { "type": "text", "title": "开场", "subtitle": "精彩马上开始", "background": "#123456", "title_color": "white", "title_size": 72, "subtitle_color": "gray", "subtitle_size": 28, "duration": 2.0, }, "outro": { "type": "text", "title": "结束", "subtitle": "再见", "background": "#654321", "duration": 3.5, }, "transition": "wipe", "transition_duration": 0.8, } ) assert cfg.has_intro is True assert cfg.has_outro is True assert cfg.intro_title == "开场" assert cfg.outro_title == "结束" assert cfg.transition_effect == "wipe" assert cfg.transition_duration == 0.8 # ── has_intro / has_outro ──────────────────────────────────────────────────── class TestHasIntroHasOutro: def test_disabled_no_intro_outro(self): cfg = IntroOutroConfig(enabled=False) assert cfg.has_intro is False assert cfg.has_outro is False def test_enabled_none_type(self): cfg = IntroOutroConfig( enabled=True, intro_type="none", outro_type="none", ) assert cfg.has_intro is False assert cfg.has_outro is False def test_intro_video_type(self): cfg = IntroOutroConfig(enabled=True, intro_type="video", intro_video_path="a.mp4") assert cfg.has_intro is True def test_intro_text_type(self): cfg = IntroOutroConfig(enabled=True, intro_type="text", intro_title="Hi") assert cfg.has_intro is True def test_outro_video(self): cfg = IntroOutroConfig(enabled=True, outro_type="video", outro_video_path="a.mp4") assert cfg.has_outro is True def test_outro_text(self): cfg = IntroOutroConfig(enabled=True, outro_type="text", outro_title="Bye") assert cfg.has_outro is True def test_outro_follow(self): cfg = IntroOutroConfig(enabled=True, outro_type="follow", outro_title="Follow") assert cfg.has_outro is True def test_intro_follow_not_valid(self): # intro 不支持 follow 类型 cfg = IntroOutroConfig(enabled=True, intro_type="follow") assert cfg.has_intro is False # ── total_extra_duration ───────────────────────────────────────────────────── class TestTotalExtraDuration: def test_disabled_zero(self): cfg = IntroOutroConfig(enabled=False) assert cfg.total_extra_duration == 0.0 def test_both_intro_outro(self): cfg = IntroOutroConfig( enabled=True, intro_type="text", intro_title="Hi", intro_duration=2.0, outro_type="text", outro_title="Bye", outro_duration=3.0, ) assert cfg.total_extra_duration == 5.0 def test_only_intro(self): cfg = IntroOutroConfig( enabled=True, intro_type="video", intro_video_path="a.mp4", intro_duration=2.5, ) assert cfg.total_extra_duration == 2.5 def test_only_outro(self): cfg = IntroOutroConfig( enabled=True, outro_type="video", outro_video_path="a.mp4", outro_duration=4.0, ) assert cfg.total_extra_duration == 4.0 def test_zero_duration_ignored(self): cfg = IntroOutroConfig( enabled=True, intro_type="text", intro_title="Hi", intro_duration=0.0, outro_type="text", outro_title="Bye", outro_duration=0.0, ) assert cfg.total_extra_duration == 0.0 # ── validate ───────────────────────────────────────────────────────────────── class TestValidate: def test_disabled_always_valid(self): cfg = IntroOutroConfig(enabled=False) ok, err = cfg.validate() assert ok is True assert err == "" def test_none_type_valid(self): cfg = IntroOutroConfig(enabled=True, intro_type="none", outro_type="none") ok, err = cfg.validate() assert ok is True def test_video_intro_without_path_invalid(self): cfg = IntroOutroConfig( enabled=True, intro_type="video", intro_video_path="", outro_type="none", ) ok, err = cfg.validate() assert ok is False assert "片头" in err and "video_path" in err def test_text_intro_without_title_invalid(self): cfg = IntroOutroConfig( enabled=True, intro_type="text", intro_title="", outro_type="none", ) ok, err = cfg.validate() assert ok is False assert "片头" in err and "title" in err def test_video_outro_without_path_invalid(self): cfg = IntroOutroConfig( enabled=True, intro_type="none", outro_type="video", outro_video_path="", ) ok, err = cfg.validate() assert ok is False assert "片尾" in err and "video_path" in err def test_text_outro_without_title_invalid(self): cfg = IntroOutroConfig( enabled=True, intro_type="none", outro_type="text", outro_title="", ) ok, err = cfg.validate() assert ok is False assert "片尾" in err and "title" in err def test_follow_outro_without_title_invalid(self): cfg = IntroOutroConfig( enabled=True, intro_type="none", outro_type="follow", outro_title="", ) ok, err = cfg.validate() assert ok is False assert "片尾" in err and "title" in err def test_zero_intro_duration_invalid(self): cfg = IntroOutroConfig( enabled=True, intro_type="text", intro_title="Hi", intro_duration=0, outro_type="none", ) ok, err = cfg.validate() assert ok is False assert "片头时长" in err def test_negative_intro_duration_invalid(self): cfg = IntroOutroConfig( enabled=True, intro_type="text", intro_title="Hi", ) object.__setattr__(cfg, "intro_duration", -1.0) ok, err = cfg.validate() assert ok is False assert "片头时长" in err def test_zero_outro_duration_invalid(self): cfg = IntroOutroConfig( enabled=True, intro_type="none", outro_type="text", outro_title="Bye", outro_duration=0, ) ok, err = cfg.validate() assert ok is False assert "片尾时长" in err def test_negative_transition_duration_invalid(self): cfg = IntroOutroConfig( enabled=True, transition_duration=-0.5, ) ok, err = cfg.validate() assert ok is False assert "转场" in err def test_zero_title_size_invalid(self): cfg = IntroOutroConfig(enabled=True) object.__setattr__(cfg, "intro_title_size", 0) ok, err = cfg.validate() assert ok is False assert "片头" in err and "字号" in err def test_zero_subtitle_size_invalid(self): cfg = IntroOutroConfig(enabled=True) object.__setattr__(cfg, "outro_subtitle_size", 0) ok, err = cfg.validate() assert ok is False assert "片尾" in err and "副标题字号" in err def test_valid_video_intro_outro(self): cfg = IntroOutroConfig( enabled=True, intro_type="video", intro_video_path="/tmp/i.mp4", intro_duration=2.0, outro_type="video", outro_video_path="/tmp/o.mp4", outro_duration=3.0, ) ok, err = cfg.validate() assert ok is True, f"expected valid but got: {err}" def test_valid_text_intro_outro(self): cfg = IntroOutroConfig( enabled=True, intro_type="text", intro_title="Hi", intro_duration=2.0, outro_type="text", outro_title="Bye", outro_duration=3.0, ) ok, err = cfg.validate() assert ok is True, f"expected valid but got: {err}" def test_invalid_intro_type(self): cfg = IntroOutroConfig(enabled=True, intro_type="invalid") ok, err = cfg.validate() assert ok is False assert "片头类型" in err def test_invalid_outro_type(self): cfg = IntroOutroConfig(enabled=True, outro_type="invalid") ok, err = cfg.validate() assert ok is False assert "片尾类型" in err