"""转场配置领域模型单测. 纯逻辑模块,覆盖: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