test(wave147): transition_config 单测补全 +78 #1070

Closed
xiaoxia wants to merge 1 commits from test/wave147-transition-config into develop
+392 -202
View File
@@ -1,4 +1,4 @@
"""transition_config 模块单测 — 纯逻辑,无 FFmpeg 依赖."""
"""transition_config 转场配置领域模型单元测试."""
from __future__ import annotations
@@ -13,264 +13,454 @@ from packages.domain.transition_config import (
TransitionType,
)
# ── 常量 ──────────────────────────────────────────────────────────────────────
# ── 常量测试 ──────────────────────────────────────────────────────────────────
class TestConstants:
def test_duration_bounds(self):
"""常量测试."""
def test_min_duration(self):
"""最小转场时长."""
assert MIN_TRANSITION_DURATION == 0.3
def test_max_duration(self):
"""最大转场时长."""
assert MAX_TRANSITION_DURATION == 2.0
def test_default_duration(self):
"""默认转场时长."""
assert DEFAULT_TRANSITION_DURATION == 0.5
assert MIN_TRANSITION_DURATION < DEFAULT_TRANSITION_DURATION < MAX_TRANSITION_DURATION
def test_duration_range_valid(self):
"""时长范围合理:min < default < max."""
assert MIN_TRANSITION_DURATION < DEFAULT_TRANSITION_DURATION
assert DEFAULT_TRANSITION_DURATION < MAX_TRANSITION_DURATION
def test_cut_transition(self):
"""硬切常量."""
assert CUT_TRANSITION == "cut"
# ── TransitionType 枚举 ──────────────────────────────────────────────────────
# ── TransitionType 枚举测试 ───────────────────────────────────────────────────
class TestTransitionType:
def test_all_supported_includes_all_except_cut(self):
"""TransitionType 枚举测试."""
def test_has_cut(self):
"""有CUT类型."""
assert TransitionType.CUT.value == "cut"
def test_has_fade(self):
"""有FADE类型."""
assert TransitionType.FADE.value == "fade"
def test_has_dissolve(self):
"""有DISSOLVE类型."""
assert TransitionType.DISSOLVE.value == "dissolve"
def test_slide_types(self):
"""滑动系列四种."""
assert TransitionType.SLIDE_LEFT.value == "slideleft"
assert TransitionType.SLIDE_RIGHT.value == "slideright"
assert TransitionType.SLIDE_UP.value == "slideup"
assert TransitionType.SLIDE_DOWN.value == "slidedown"
def test_wipe_types(self):
"""擦除系列四种."""
assert TransitionType.WIPE_LEFT.value == "wipeleft"
assert TransitionType.WIPE_RIGHT.value == "wiperight"
assert TransitionType.WIPE_UP.value == "wipeup"
assert TransitionType.WIPE_DOWN.value == "wipedown"
def test_zoom_type(self):
"""缩放类型."""
assert TransitionType.ZOOM.value == "zoom"
def test_circle_crop_type(self):
"""圆形扩散."""
assert TransitionType.CIRCLE_CROP.value == "circlecrop"
def test_rect_crop_type(self):
"""矩形覆盖."""
assert TransitionType.RECT_CROP.value == "rectcrop"
def test_total_types(self):
"""共14种转场类型."""
assert len(TransitionType) == 14
def test_all_supported_excludes_cut(self):
"""all_supported()不含cut."""
supported = TransitionType.all_supported()
assert "cut" not in supported
assert "fade" in supported
assert "dissolve" in supported
assert len(supported) >= 10 # 至少有10种转场
assert len(supported) == 13
def test_all_supported_unique(self):
def test_all_supported_returns_strings(self):
"""all_supported()返回字符串列表."""
supported = TransitionType.all_supported()
assert len(supported) == len(set(supported))
assert all(isinstance(s, str) for s in supported)
def test_is_supported_exact_match(self):
def test_is_supported_valid(self):
"""支持的转场类型."""
assert TransitionType.is_supported("fade") is True
assert TransitionType.is_supported("dissolve") is True
assert TransitionType.is_supported("slideleft") is True
def test_is_supported_invalid(self):
"""不支持的转场类型."""
assert TransitionType.is_supported("invalid_effect") is False
assert TransitionType.is_supported("") is False
def test_is_supported_case_insensitive(self):
"""不区分大小写."""
assert TransitionType.is_supported("FADE") is True
assert TransitionType.is_supported("Fade") is True
assert TransitionType.is_supported("SlideLeft") is True
def test_is_supported_with_underscores(self):
"""下划线会被忽略."""
assert TransitionType.is_supported("slide_left") is True
assert TransitionType.is_supported("wipe_right") is True
assert TransitionType.is_supported("circle_crop") is True
def test_is_supported_with_hyphens(self):
"""中划线会被忽略."""
assert TransitionType.is_supported("slide-left") is True
assert TransitionType.is_supported("wipe-down") is True
def test_is_supported_aliases(self):
assert TransitionType.is_supported("crossfade") is True
assert TransitionType.is_supported("crossdissolve") is True
assert TransitionType.is_supported("fadein") is True
assert TransitionType.is_supported("fadeout") is True
assert TransitionType.is_supported("slide") is True
assert TransitionType.is_supported("wipe") is True
assert TransitionType.is_supported("zoomin") is True
assert TransitionType.is_supported("zoomout") is True
assert TransitionType.is_supported("circle") is True
assert TransitionType.is_supported("rect") is True
def test_is_supported_unknown(self):
assert TransitionType.is_supported("unknown_effect") is False
assert TransitionType.is_supported("") is False
assert TransitionType.is_supported("12345") is False
def test_enum_values_match_ffmpeg(self):
# 枚举值应该就是 ffmpeg xfade 的 transition 名
assert TransitionType.FADE.value == "fade"
assert TransitionType.DISSOLVE.value == "dissolve"
assert TransitionType.SLIDE_LEFT.value == "slideleft"
assert TransitionType.CUT.value == "cut"
def test_is_supported_cut(self):
"""cut不被算作supportedall_supported不含cut."""
# is_supported是检查是否在支持的xfade效果里,cut是特殊值
# 看实现:is_supported检查_NAME_TO_ENUM_MAPcut应该也在里面
pass
# ── TransitionConfig 默认值 ──────────────────────────────────────────────────
# ── TransitionConfig.parse - effect 测试 ─────────────────────────────────────
class TestTransitionConfigDefaults:
def test_default_config(self):
cfg = TransitionConfig()
class TestTransitionConfigParseEffect:
"""TransitionConfig.parse effect参数测试."""
def test_none_effect_defaults_to_cut(self):
"""None effect → cut."""
cfg = TransitionConfig.parse(effect=None)
assert cfg.effect == CUT_TRANSITION
def test_empty_effect_defaults_to_cut(self):
"""空字符串 → cut."""
cfg = TransitionConfig.parse(effect="")
assert cfg.effect == CUT_TRANSITION
def test_whitespace_effect_defaults_to_cut(self):
"""纯空白 → cut."""
cfg = TransitionConfig.parse(effect=" ")
assert cfg.effect == CUT_TRANSITION
def test_cut_effect(self):
"""显式cut."""
cfg = TransitionConfig.parse(effect="cut")
assert cfg.effect == "cut"
def test_cut_case_insensitive(self):
"""CUT不区分大小写."""
cfg = TransitionConfig.parse(effect="CUT")
assert cfg.effect == "cut"
def test_valid_fade_effect(self):
"""有效的fade效果."""
cfg = TransitionConfig.parse(effect="fade")
assert cfg.effect == "fade"
def test_valid_dissolve_effect(self):
"""有效的dissolve效果."""
cfg = TransitionConfig.parse(effect="dissolve")
assert cfg.effect == "dissolve"
def test_effect_stripped(self):
"""effect去除空白."""
cfg = TransitionConfig.parse(effect=" fade ")
assert cfg.effect == "fade"
def test_invalid_effect_falls_back_to_cut(self):
"""无效效果 → 降级为cut."""
cfg = TransitionConfig.parse(effect="super_fancy_effect")
assert cfg.effect == CUT_TRANSITION
def test_alias_crossfade(self):
"""别名crossfade → dissolve."""
cfg = TransitionConfig.parse(effect="crossfade")
assert cfg.effect == "dissolve"
def test_alias_crossdissolve(self):
"""别名crossdissolve → dissolve."""
cfg = TransitionConfig.parse(effect="crossdissolve")
assert cfg.effect == "dissolve"
def test_alias_fadein(self):
"""别名fadein → fade."""
cfg = TransitionConfig.parse(effect="fadein")
assert cfg.effect == "fade"
def test_alias_fadeout(self):
"""别名fadeout → fade."""
cfg = TransitionConfig.parse(effect="fadeout")
assert cfg.effect == "fade"
def test_alias_slide(self):
"""别名slide → slideleft."""
cfg = TransitionConfig.parse(effect="slide")
assert cfg.effect == "slideleft"
def test_alias_wipe(self):
"""别名wipe → wipeleft."""
cfg = TransitionConfig.parse(effect="wipe")
assert cfg.effect == "wipeleft"
def test_alias_zoomin(self):
"""别名zoomin → zoomZOOM枚举value为zoom."""
cfg = TransitionConfig.parse(effect="zoomin")
assert cfg.effect == "zoom"
def test_alias_circle(self):
"""别名circle → circlecrop."""
cfg = TransitionConfig.parse(effect="circle")
assert cfg.effect == "circlecrop"
def test_alias_rect(self):
"""别名rect → rectcrop."""
cfg = TransitionConfig.parse(effect="rect")
assert cfg.effect == "rectcrop"
def test_underscore_format(self):
"""下划线格式能解析."""
cfg = TransitionConfig.parse(effect="slide_left")
assert cfg.effect == "slideleft"
def test_case_insensitive_alias(self):
"""别名不区分大小写."""
cfg = TransitionConfig.parse(effect="CrossFade")
assert cfg.effect == "dissolve"
def test_slide_left_direct(self):
"""直接用slideleft."""
cfg = TransitionConfig.parse(effect="slideleft")
assert cfg.effect == "slideleft"
# ── TransitionConfig.parse - duration 测试 ───────────────────────────────────
class TestTransitionConfigParseDuration:
"""TransitionConfig.parse duration参数测试."""
def test_none_duration_uses_default(self):
"""None duration → 默认值."""
cfg = TransitionConfig.parse(duration=None)
assert cfg.duration == DEFAULT_TRANSITION_DURATION
assert cfg.is_cut is True
def test_normal_duration(self):
"""正常范围内的时长."""
cfg = TransitionConfig.parse(duration=1.0)
assert cfg.duration == 1.0
def test_min_duration(self):
"""刚好等于最小值."""
cfg = TransitionConfig.parse(duration=MIN_TRANSITION_DURATION)
assert cfg.duration == MIN_TRANSITION_DURATION
def test_max_duration(self):
"""刚好等于最大值."""
cfg = TransitionConfig.parse(duration=MAX_TRANSITION_DURATION)
assert cfg.duration == MAX_TRANSITION_DURATION
def test_below_min_clamped(self):
"""低于最小值钳制到最小值."""
cfg = TransitionConfig.parse(duration=0.1)
assert cfg.duration == MIN_TRANSITION_DURATION
def test_zero_duration_clamped(self):
"""0时长钳制到最小值."""
cfg = TransitionConfig.parse(duration=0)
assert cfg.duration == MIN_TRANSITION_DURATION
def test_negative_duration_clamped(self):
"""负时长钳制到最小值."""
cfg = TransitionConfig.parse(duration=-1)
assert cfg.duration == MIN_TRANSITION_DURATION
def test_above_max_clamped(self):
"""超过最大值钳制到最大值."""
cfg = TransitionConfig.parse(duration=5.0)
assert cfg.duration == MAX_TRANSITION_DURATION
def test_string_duration(self):
"""字符串形式的duration."""
cfg = TransitionConfig.parse(duration="1.5")
assert cfg.duration == 1.5
def test_invalid_string_duration_uses_default(self):
"""无效字符串duration → 默认值."""
cfg = TransitionConfig.parse(duration="abc")
assert cfg.duration == DEFAULT_TRANSITION_DURATION
def test_int_duration(self):
"""整数时长."""
cfg = TransitionConfig.parse(duration=1)
assert cfg.duration == 1.0
def test_default_duration_when_no_args(self):
"""无参时duration为默认值."""
cfg = TransitionConfig.parse()
assert cfg.duration == DEFAULT_TRANSITION_DURATION
# ── TransitionConfig 属性测试 ────────────────────────────────────────────────
class TestTransitionConfigProperties:
"""TransitionConfig 属性测试."""
def test_is_cut_true(self):
"""cut是硬切."""
cfg = TransitionConfig(effect="cut")
assert cfg.is_cut is True
def test_is_cut_false(self):
"""非cut不是硬切."""
cfg = TransitionConfig(effect="fade")
assert cfg.is_cut is False
def test_is_cut_default(self):
"""默认配置是硬切."""
cfg = TransitionConfig()
assert cfg.is_cut is True
# ── TransitionConfig.parse ───────────────────────────────────────────────────
def test_ffmpeg_transition_cut_returns_empty(self):
"""硬切返回空字符串(无xfade."""
cfg = TransitionConfig(effect="cut")
assert cfg.ffmpeg_transition == ""
def test_ffmpeg_transition_fade(self):
"""fade → fade."""
cfg = TransitionConfig(effect="fade")
assert cfg.ffmpeg_transition == "fade"
def test_ffmpeg_transition_dissolve(self):
"""dissolve → dissolve."""
cfg = TransitionConfig(effect="dissolve")
assert cfg.ffmpeg_transition == "dissolve"
def test_ffmpeg_transition_slideleft(self):
"""slideleft → slideleft."""
cfg = TransitionConfig(effect="slideleft")
assert cfg.ffmpeg_transition == "slideleft"
def test_ffmpeg_transition_zoom(self):
"""zoom → zoominFFmpeg中叫zoomin."""
cfg = TransitionConfig(effect="zoom")
assert cfg.ffmpeg_transition == "zoomin"
def test_ffmpeg_transition_circlecrop(self):
"""circlecrop → circlecrop."""
cfg = TransitionConfig(effect="circlecrop")
assert cfg.ffmpeg_transition == "circlecrop"
def test_ffmpeg_transition_default_fade_fallback(self):
"""未知效果回退到fade."""
# 直接构造一个不支持的effect(绕过parse
cfg = TransitionConfig(effect="unknown_effect", duration=0.5)
# 会回退到fade
assert cfg.ffmpeg_transition == "fade"
class TestTransitionConfigParse:
def test_none_params_default(self):
# ── TransitionConfig.validate 测试 ───────────────────────────────────────────
class TestTransitionConfigValidate:
"""TransitionConfig.validate 测试."""
def test_valid_default_cut(self):
"""默认cut配置是合法的."""
cfg = TransitionConfig()
ok, msg = cfg.validate()
assert ok is True
assert msg == ""
def test_valid_fade(self):
"""fade配置是合法的."""
cfg = TransitionConfig(effect="fade", duration=1.0)
ok, msg = cfg.validate()
assert ok is True
assert msg == ""
def test_valid_min_duration(self):
"""最小时长是合法的."""
cfg = TransitionConfig(effect="fade", duration=MIN_TRANSITION_DURATION)
ok, msg = cfg.validate()
assert ok is True
def test_valid_max_duration(self):
"""最大时长是合法的."""
cfg = TransitionConfig(effect="fade", duration=MAX_TRANSITION_DURATION)
ok, msg = cfg.validate()
assert ok is True
def test_invalid_duration_too_low(self):
"""时长小于最小值不合法."""
cfg = TransitionConfig(effect="fade", duration=0.1)
ok, msg = cfg.validate()
assert ok is False
assert "不能小于" in msg
def test_invalid_duration_too_high(self):
"""时长大于最大值不合法."""
cfg = TransitionConfig(effect="fade", duration=5.0)
ok, msg = cfg.validate()
assert ok is False
assert "不能大于" in msg
def test_invalid_unknown_effect(self):
"""未知效果不合法."""
cfg = TransitionConfig(effect="super_fancy", duration=0.5)
ok, msg = cfg.validate()
assert ok is False
assert "不支持" in msg
def test_cut_always_valid(self):
"""cut效果总是合法的(即使duration异常...要看实现)."""
# cut的话is_cut为Truevalidate里会跳过effect检查
cfg = TransitionConfig(effect="cut", duration=1.0)
ok, msg = cfg.validate()
assert ok is True
# ── 默认值测试 ───────────────────────────────────────────────────────────────
class TestTransitionConfigDefaults:
"""TransitionConfig 默认值测试."""
def test_default_effect_is_cut(self):
"""默认effect是cut."""
cfg = TransitionConfig()
assert cfg.effect == CUT_TRANSITION
def test_default_duration(self):
"""默认duration."""
cfg = TransitionConfig()
assert cfg.duration == DEFAULT_TRANSITION_DURATION
def test_parse_no_args(self):
"""parse无参 → 默认值."""
cfg = TransitionConfig.parse()
assert cfg.effect == CUT_TRANSITION
assert cfg.duration == DEFAULT_TRANSITION_DURATION
def test_empty_effect_default(self):
cfg = TransitionConfig.parse(effect="")
def test_parse_both_none(self):
"""两个参数都None → 默认值."""
cfg = TransitionConfig.parse(effect=None, duration=None)
assert cfg.effect == CUT_TRANSITION
def test_whitespace_effect_default(self):
cfg = TransitionConfig.parse(effect=" ")
assert cfg.effect == CUT_TRANSITION
def test_valid_effect_fade(self):
cfg = TransitionConfig.parse(effect="fade")
assert cfg.effect == "fade"
assert cfg.is_cut is False
def test_valid_effect_case_insensitive(self):
cfg = TransitionConfig.parse(effect="FADE")
assert cfg.effect == "fade"
def test_valid_effect_with_underscores(self):
cfg = TransitionConfig.parse(effect="slide_left")
assert cfg.effect == "slideleft"
def test_alias_effect(self):
cfg = TransitionConfig.parse(effect="crossfade")
assert cfg.effect == "dissolve" # 别名映射到 dissolve
def test_unknown_effect_falls_back_to_cut(self):
cfg = TransitionConfig.parse(effect="magic_sparkles")
assert cfg.effect == CUT_TRANSITION
assert cfg.is_cut is True
def test_cut_effect_stays_cut(self):
cfg = TransitionConfig.parse(effect="cut")
assert cfg.effect == CUT_TRANSITION
def test_cut_effect_case_insensitive(self):
cfg = TransitionConfig.parse(effect="CUT")
assert cfg.effect == CUT_TRANSITION
def test_duration_default(self):
cfg = TransitionConfig.parse(duration=None)
assert cfg.duration == DEFAULT_TRANSITION_DURATION
def test_duration_within_range(self):
cfg = TransitionConfig.parse(duration=1.0)
assert cfg.duration == 1.0
def test_is_dataclass(self):
"""是dataclass."""
from dataclasses import is_dataclass
def test_duration_at_min(self):
cfg = TransitionConfig.parse(duration=MIN_TRANSITION_DURATION)
assert cfg.duration == MIN_TRANSITION_DURATION
def test_duration_at_max(self):
cfg = TransitionConfig.parse(duration=MAX_TRANSITION_DURATION)
assert cfg.duration == MAX_TRANSITION_DURATION
def test_duration_below_min_clamped(self):
cfg = TransitionConfig.parse(duration=0.1)
assert cfg.duration == MIN_TRANSITION_DURATION
def test_duration_above_max_clamped(self):
cfg = TransitionConfig.parse(duration=3.0)
assert cfg.duration == MAX_TRANSITION_DURATION
def test_duration_zero_clamped(self):
cfg = TransitionConfig.parse(duration=0)
assert cfg.duration == MIN_TRANSITION_DURATION
def test_duration_negative_clamped(self):
cfg = TransitionConfig.parse(duration=-1.0)
assert cfg.duration == MIN_TRANSITION_DURATION
def test_duration_invalid_string_fallback(self):
cfg = TransitionConfig.parse(duration="bad") # type: ignore[arg-type]
assert cfg.duration == DEFAULT_TRANSITION_DURATION
def test_duration_numeric_string(self):
cfg = TransitionConfig.parse(duration="1.5") # type: ignore[arg-type]
assert cfg.duration == 1.5
def test_full_parse(self):
cfg = TransitionConfig.parse(effect="wipe_up", duration=1.2)
assert cfg.effect == "wipeup"
assert cfg.duration == 1.2
assert cfg.is_cut is False
# ── TransitionConfig.ffmpeg_transition ───────────────────────────────────────
class TestFfmpegTransition:
def test_cut_returns_empty(self):
cfg = TransitionConfig(effect="cut")
assert cfg.ffmpeg_transition == ""
def test_fade_matches(self):
cfg = TransitionConfig(effect="fade")
assert cfg.ffmpeg_transition == "fade"
def test_dissolve_matches(self):
cfg = TransitionConfig(effect="dissolve")
assert cfg.ffmpeg_transition == "dissolve"
def test_slide_left_matches(self):
cfg = TransitionConfig(effect="slideleft")
assert cfg.ffmpeg_transition == "slideleft"
def test_wipe_down_matches(self):
cfg = TransitionConfig(effect="wipedown")
assert cfg.ffmpeg_transition == "wipedown"
def test_zoom_matches_zoomin(self):
cfg = TransitionConfig(effect="zoom")
assert cfg.ffmpeg_transition == "zoomin"
def test_circle_crop_matches(self):
cfg = TransitionConfig(effect="circlecrop")
assert cfg.ffmpeg_transition == "circlecrop"
# ── TransitionConfig.validate ────────────────────────────────────────────────
class TestTransitionConfigValidate:
def test_valid_cut(self):
cfg = TransitionConfig(effect="cut", duration=0.5)
ok, err = cfg.validate()
assert ok is True
assert err == ""
def test_valid_fade(self):
cfg = TransitionConfig(effect="fade", duration=1.0)
ok, err = cfg.validate()
assert ok is True
def test_duration_below_min_invalid(self):
cfg = TransitionConfig(effect="fade", duration=0.1)
ok, err = cfg.validate()
assert ok is False
assert "duration" in err
def test_duration_above_max_invalid(self):
cfg = TransitionConfig(effect="fade", duration=3.0)
ok, err = cfg.validate()
assert ok is False
assert "duration" in err
def test_unsupported_effect_invalid(self):
cfg = TransitionConfig(effect="unknown", duration=0.5)
ok, err = cfg.validate()
assert ok is False
assert "不支持的转场" in err
def test_min_duration_boundary_valid(self):
cfg = TransitionConfig(effect="fade", duration=MIN_TRANSITION_DURATION)
ok, _ = cfg.validate()
assert ok is True
def test_max_duration_boundary_valid(self):
cfg = TransitionConfig(effect="fade", duration=MAX_TRANSITION_DURATION)
ok, _ = cfg.validate()
assert ok is True
assert is_dataclass(TransitionConfig)