Files
xiaoxia-saas/tests/unit/test_intro_outro_config.py

538 lines
17 KiB
Python
Executable File

"""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