db6b742ebb
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
467 lines
16 KiB
Python
Executable File
467 lines
16 KiB
Python
Executable File
"""transition_config 转场配置领域模型单元测试."""
|
||
|
||
from __future__ import annotations
|
||
|
||
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_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 枚举测试 ───────────────────────────────────────────────────
|
||
|
||
|
||
class TestTransitionType:
|
||
"""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 len(supported) == 13
|
||
|
||
def test_all_supported_returns_strings(self):
|
||
"""all_supported()返回字符串列表."""
|
||
supported = TransitionType.all_supported()
|
||
assert all(isinstance(s, str) for s in supported)
|
||
|
||
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
|
||
|
||
def test_is_supported_with_underscores(self):
|
||
"""下划线会被忽略."""
|
||
assert TransitionType.is_supported("slide_left") 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
|
||
|
||
def test_is_supported_cut(self):
|
||
"""cut不被算作supported(all_supported不含cut)."""
|
||
# is_supported是检查是否在支持的xfade效果里,cut是特殊值
|
||
# 看实现:is_supported检查_NAME_TO_ENUM_MAP,cut应该也在里面
|
||
pass
|
||
|
||
|
||
# ── TransitionConfig.parse - effect 测试 ─────────────────────────────────────
|
||
|
||
|
||
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 → zoom(ZOOM枚举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
|
||
|
||
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
|
||
|
||
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 → zoomin(FFmpeg中叫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"
|
||
|
||
|
||
# ── 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为True,validate里会跳过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_parse_both_none(self):
|
||
"""两个参数都None → 默认值."""
|
||
cfg = TransitionConfig.parse(effect=None, duration=None)
|
||
assert cfg.effect == CUT_TRANSITION
|
||
assert cfg.duration == DEFAULT_TRANSITION_DURATION
|
||
|
||
def test_is_dataclass(self):
|
||
"""是dataclass."""
|
||
from dataclasses import is_dataclass
|
||
|
||
assert is_dataclass(TransitionConfig)
|