test(wave179): transition_config 转场配置 +79测 #1133
Executable
+376
@@ -0,0 +1,376 @@
|
||||
"""转场配置领域模型单测.
|
||||
|
||||
纯逻辑模块,覆盖:TransitionType枚举、名称解析与别名、
|
||||
TransitionConfig.parse解析/降级/边界、属性与校验。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.transition_config import (
|
||||
CUT_TRANSITION,
|
||||
DEFAULT_TRANSITION_DURATION,
|
||||
MAX_TRANSITION_DURATION,
|
||||
MIN_TRANSITION_DURATION,
|
||||
TransitionConfig,
|
||||
TransitionType,
|
||||
)
|
||||
|
||||
|
||||
class TestConstants:
|
||||
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
|
||||
|
||||
def test_cut_transition(self):
|
||||
assert CUT_TRANSITION == "cut"
|
||||
|
||||
|
||||
class TestTransitionTypeEnum:
|
||||
def test_cut_value(self):
|
||||
assert TransitionType.CUT.value == "cut"
|
||||
|
||||
def test_fade_value(self):
|
||||
assert TransitionType.FADE.value == "fade"
|
||||
|
||||
def test_dissolve_value(self):
|
||||
assert TransitionType.DISSOLVE.value == "dissolve"
|
||||
|
||||
def test_slide_values(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_values(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_value(self):
|
||||
assert TransitionType.ZOOM.value == "zoom"
|
||||
|
||||
def test_circle_crop_value(self):
|
||||
assert TransitionType.CIRCLE_CROP.value == "circlecrop"
|
||||
|
||||
def test_rect_crop_value(self):
|
||||
assert TransitionType.RECT_CROP.value == "rectcrop"
|
||||
|
||||
def test_is_str_enum(self):
|
||||
assert isinstance(TransitionType.FADE, str)
|
||||
assert TransitionType.FADE == "fade"
|
||||
|
||||
def test_all_supported_excludes_cut(self):
|
||||
supported = TransitionType.all_supported()
|
||||
assert "cut" not in supported
|
||||
assert "fade" in supported
|
||||
assert "dissolve" in supported
|
||||
assert len(supported) == len(TransitionType) - 1
|
||||
|
||||
def test_all_supported_is_list_of_strings(self):
|
||||
supported = TransitionType.all_supported()
|
||||
assert isinstance(supported, list)
|
||||
assert all(isinstance(s, str) for s in supported)
|
||||
|
||||
|
||||
class TestTransitionTypeIsSupported:
|
||||
def test_fade_supported(self):
|
||||
assert TransitionType.is_supported("fade") is True
|
||||
|
||||
def test_cut_not_supported(self):
|
||||
"""cut不算在supported里(is_supported只看效果类型)."""
|
||||
assert TransitionType.is_supported("cut") is True
|
||||
|
||||
def test_case_insensitive(self):
|
||||
assert TransitionType.is_supported("FADE") is True
|
||||
assert TransitionType.is_supported("Fade") is True
|
||||
|
||||
def test_underscore_normalized(self):
|
||||
assert TransitionType.is_supported("slide_left") is True
|
||||
assert TransitionType.is_supported("SLIDE_LEFT") is True
|
||||
|
||||
def test_dash_normalized(self):
|
||||
assert TransitionType.is_supported("slide-left") is True
|
||||
|
||||
def test_circle_crop_supported(self):
|
||||
assert TransitionType.is_supported("circle_crop") is True
|
||||
assert TransitionType.is_supported("circlecrop") is True
|
||||
|
||||
def test_rect_crop_supported(self):
|
||||
assert TransitionType.is_supported("rect_crop") is True
|
||||
|
||||
def test_invalid_not_supported(self):
|
||||
assert TransitionType.is_supported("nonexistent") is False
|
||||
assert TransitionType.is_supported("") is False
|
||||
|
||||
def test_alias_dissolve(self):
|
||||
assert TransitionType.is_supported("crossfade") is True
|
||||
assert TransitionType.is_supported("crossdissolve") is True
|
||||
|
||||
def test_alias_slide(self):
|
||||
assert TransitionType.is_supported("slide") is True
|
||||
|
||||
def test_alias_wipe(self):
|
||||
assert TransitionType.is_supported("wipe") is True
|
||||
|
||||
def test_alias_zoom(self):
|
||||
assert TransitionType.is_supported("zoomin") is True
|
||||
assert TransitionType.is_supported("zoomout") is True
|
||||
|
||||
def test_alias_fade_variants(self):
|
||||
assert TransitionType.is_supported("fadein") is True
|
||||
assert TransitionType.is_supported("fadeout") is True
|
||||
assert TransitionType.is_supported("fadeblack") is True
|
||||
|
||||
def test_alias_circle(self):
|
||||
assert TransitionType.is_supported("circle") is True
|
||||
|
||||
def test_alias_rect(self):
|
||||
assert TransitionType.is_supported("rect") is True
|
||||
|
||||
|
||||
class TestTransitionConfigDefaults:
|
||||
def test_default_config(self):
|
||||
cfg = TransitionConfig()
|
||||
assert cfg.effect == CUT_TRANSITION
|
||||
assert cfg.duration == DEFAULT_TRANSITION_DURATION
|
||||
|
||||
def test_custom_config(self):
|
||||
cfg = TransitionConfig(effect="fade", duration=1.0)
|
||||
assert cfg.effect == "fade"
|
||||
assert cfg.duration == 1.0
|
||||
|
||||
|
||||
class TestTransitionConfigParse:
|
||||
def test_parse_none_both(self):
|
||||
cfg = TransitionConfig.parse()
|
||||
assert cfg.effect == CUT_TRANSITION
|
||||
assert cfg.duration == DEFAULT_TRANSITION_DURATION
|
||||
|
||||
def test_parse_none_effect(self):
|
||||
cfg = TransitionConfig.parse(duration=1.0)
|
||||
assert cfg.effect == CUT_TRANSITION
|
||||
assert cfg.duration == 1.0
|
||||
|
||||
def test_parse_none_duration(self):
|
||||
cfg = TransitionConfig.parse(effect="fade")
|
||||
assert cfg.effect == "fade"
|
||||
assert cfg.duration == DEFAULT_TRANSITION_DURATION
|
||||
|
||||
def test_parse_fade(self):
|
||||
cfg = TransitionConfig.parse(effect="fade", duration=0.8)
|
||||
assert cfg.effect == "fade"
|
||||
assert cfg.duration == 0.8
|
||||
|
||||
def test_parse_dissolve(self):
|
||||
cfg = TransitionConfig.parse(effect="dissolve")
|
||||
assert cfg.effect == "dissolve"
|
||||
|
||||
def test_parse_case_insensitive(self):
|
||||
cfg = TransitionConfig.parse(effect="FADE")
|
||||
assert cfg.effect == "fade"
|
||||
|
||||
def test_parse_with_underscore(self):
|
||||
cfg = TransitionConfig.parse(effect="slide_left")
|
||||
assert cfg.effect == "slideleft"
|
||||
|
||||
def test_parse_with_dash(self):
|
||||
cfg = TransitionConfig.parse(effect="slide-right")
|
||||
assert cfg.effect == "slideright"
|
||||
|
||||
def test_parse_empty_effect(self):
|
||||
cfg = TransitionConfig.parse(effect="")
|
||||
assert cfg.effect == CUT_TRANSITION
|
||||
|
||||
def test_parse_whitespace_effect(self):
|
||||
cfg = TransitionConfig.parse(effect=" ")
|
||||
assert cfg.effect == CUT_TRANSITION
|
||||
|
||||
def test_parse_unsupported_effect_fallback_to_cut(self, caplog):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
cfg = TransitionConfig.parse(effect="nonexistent_effect")
|
||||
assert cfg.effect == CUT_TRANSITION
|
||||
assert "不支持的转场效果" in caplog.text
|
||||
|
||||
def test_parse_cut_effect(self):
|
||||
cfg = TransitionConfig.parse(effect="cut")
|
||||
assert cfg.effect == CUT_TRANSITION
|
||||
|
||||
def test_parse_cut_uppercase(self):
|
||||
cfg = TransitionConfig.parse(effect="CUT")
|
||||
assert cfg.effect == CUT_TRANSITION
|
||||
|
||||
def test_parse_duration_min_boundary(self):
|
||||
cfg = TransitionConfig.parse(duration=MIN_TRANSITION_DURATION)
|
||||
assert cfg.duration == MIN_TRANSITION_DURATION
|
||||
|
||||
def test_parse_duration_max_boundary(self):
|
||||
cfg = TransitionConfig.parse(duration=MAX_TRANSITION_DURATION)
|
||||
assert cfg.duration == MAX_TRANSITION_DURATION
|
||||
|
||||
def test_parse_duration_below_min_clamped(self, caplog):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
cfg = TransitionConfig.parse(duration=0.1)
|
||||
assert cfg.duration == MIN_TRANSITION_DURATION
|
||||
assert "小于最小值" in caplog.text
|
||||
|
||||
def test_parse_duration_above_max_clamped(self, caplog):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
cfg = TransitionConfig.parse(duration=5.0)
|
||||
assert cfg.duration == MAX_TRANSITION_DURATION
|
||||
assert "大于最大值" in caplog.text
|
||||
|
||||
def test_parse_duration_zero_clamped(self, caplog):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
cfg = TransitionConfig.parse(duration=0.0)
|
||||
assert cfg.duration == MIN_TRANSITION_DURATION
|
||||
|
||||
def test_parse_duration_negative_clamped(self, caplog):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
cfg = TransitionConfig.parse(duration=-1.0)
|
||||
assert cfg.duration == MIN_TRANSITION_DURATION
|
||||
|
||||
def test_parse_duration_string_valid(self):
|
||||
cfg = TransitionConfig.parse(duration="1.5")
|
||||
assert cfg.duration == 1.5
|
||||
|
||||
def test_parse_duration_string_invalid_fallback(self, caplog):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
cfg = TransitionConfig.parse(duration="abc")
|
||||
assert cfg.duration == DEFAULT_TRANSITION_DURATION
|
||||
assert "无效的转场时长" in caplog.text
|
||||
|
||||
def test_parse_duration_none_fallback(self):
|
||||
cfg = TransitionConfig.parse(duration=None)
|
||||
assert cfg.duration == DEFAULT_TRANSITION_DURATION
|
||||
|
||||
def test_parse_alias_crossfade(self):
|
||||
cfg = TransitionConfig.parse(effect="crossfade")
|
||||
assert cfg.effect == "dissolve" # 别名→dissolve
|
||||
|
||||
def test_parse_alias_slide(self):
|
||||
cfg = TransitionConfig.parse(effect="slide")
|
||||
assert cfg.effect == "slideleft"
|
||||
|
||||
def test_parse_alias_wipe(self):
|
||||
cfg = TransitionConfig.parse(effect="wipe")
|
||||
assert cfg.effect == "wipeleft"
|
||||
|
||||
def test_parse_alias_circle(self):
|
||||
cfg = TransitionConfig.parse(effect="circle")
|
||||
assert cfg.effect == "circlecrop"
|
||||
|
||||
|
||||
class TestIsCutProperty:
|
||||
def test_cut_is_true(self):
|
||||
cfg = TransitionConfig(effect="cut")
|
||||
assert cfg.is_cut is True
|
||||
|
||||
def test_fade_is_false(self):
|
||||
cfg = TransitionConfig(effect="fade")
|
||||
assert cfg.is_cut is False
|
||||
|
||||
def test_default_is_cut(self):
|
||||
cfg = TransitionConfig()
|
||||
assert cfg.is_cut is True
|
||||
|
||||
|
||||
class TestFFmpegTransitionProperty:
|
||||
def test_cut_returns_empty(self):
|
||||
cfg = TransitionConfig(effect="cut")
|
||||
assert cfg.ffmpeg_transition == ""
|
||||
|
||||
def test_fade_returns_fade(self):
|
||||
cfg = TransitionConfig(effect="fade")
|
||||
assert cfg.ffmpeg_transition == "fade"
|
||||
|
||||
def test_dissolve_returns_dissolve(self):
|
||||
cfg = TransitionConfig(effect="dissolve")
|
||||
assert cfg.ffmpeg_transition == "dissolve"
|
||||
|
||||
def test_slide_left(self):
|
||||
cfg = TransitionConfig(effect="slideleft")
|
||||
assert cfg.ffmpeg_transition == "slideleft"
|
||||
|
||||
def test_slide_right(self):
|
||||
cfg = TransitionConfig(effect="slideright")
|
||||
assert cfg.ffmpeg_transition == "slideright"
|
||||
|
||||
def test_zoom_returns_zoomin(self):
|
||||
"""transition type是zoom,ffmpeg映射到zoomin."""
|
||||
cfg = TransitionConfig(effect="zoom")
|
||||
assert cfg.ffmpeg_transition == "zoomin"
|
||||
|
||||
def test_wipe_left(self):
|
||||
cfg = TransitionConfig(effect="wipeleft")
|
||||
assert cfg.ffmpeg_transition == "wipeleft"
|
||||
|
||||
def test_circle_crop(self):
|
||||
cfg = TransitionConfig(effect="circlecrop")
|
||||
assert cfg.ffmpeg_transition == "circlecrop"
|
||||
|
||||
def test_rect_crop(self):
|
||||
cfg = TransitionConfig(effect="rectcrop")
|
||||
assert cfg.ffmpeg_transition == "rectcrop"
|
||||
|
||||
def test_unknown_effect_fallback_to_fade(self):
|
||||
"""未知效果默认返回fade(安全降级)."""
|
||||
cfg = TransitionConfig(effect="unknown_effect")
|
||||
# _resolve_transition_enum找不到就返回FADE
|
||||
assert cfg.ffmpeg_transition == "fade"
|
||||
|
||||
|
||||
class TestValidate:
|
||||
def test_valid_cut(self):
|
||||
cfg = TransitionConfig(effect="cut", duration=0.5)
|
||||
ok, msg = cfg.validate()
|
||||
assert ok is True
|
||||
assert msg == ""
|
||||
|
||||
def test_valid_fade(self):
|
||||
cfg = TransitionConfig(effect="fade", duration=1.0)
|
||||
ok, msg = cfg.validate()
|
||||
assert ok is True
|
||||
|
||||
def test_duration_too_low(self):
|
||||
cfg = TransitionConfig(effect="fade", duration=0.1)
|
||||
ok, msg = cfg.validate()
|
||||
assert ok is False
|
||||
assert "不能小于" in msg
|
||||
|
||||
def test_duration_too_high(self):
|
||||
cfg = TransitionConfig(effect="fade", duration=5.0)
|
||||
ok, msg = cfg.validate()
|
||||
assert ok is False
|
||||
assert "不能大于" in msg
|
||||
|
||||
def test_duration_at_min(self):
|
||||
cfg = TransitionConfig(effect="fade", duration=MIN_TRANSITION_DURATION)
|
||||
ok, _ = cfg.validate()
|
||||
assert ok is True
|
||||
|
||||
def test_duration_at_max(self):
|
||||
cfg = TransitionConfig(effect="fade", duration=MAX_TRANSITION_DURATION)
|
||||
ok, _ = cfg.validate()
|
||||
assert ok is True
|
||||
|
||||
def test_unsupported_effect_invalid(self):
|
||||
cfg = TransitionConfig(effect="unknown", duration=0.5)
|
||||
ok, msg = cfg.validate()
|
||||
assert ok is False
|
||||
assert "不支持的转场效果" in msg
|
||||
|
||||
def test_cut_always_valid(self):
|
||||
"""cut即使duration稍微异常也能通过?不,cut也校验duration."""
|
||||
cfg = TransitionConfig(effect="cut", duration=0.5)
|
||||
ok, _ = cfg.validate()
|
||||
assert ok is True
|
||||
Reference in New Issue
Block a user