Files
xiaoxia-saas/tests/unit/test_transition_config.py
T
xiaoxia 883af85bdc
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m56s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m11s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 28s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 33s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m15s
AI Code Review / AI Code Review (pull_request) Successful in 1m44s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 40s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m8s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 6m22s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 5m55s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m51s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 3m19s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 46m27s
CI/CD Pipeline / CI Gate (pull_request) Successful in 34s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 52s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1202h49m37s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1203h5m37s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1203h5m39s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1203h5m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1203h11m23s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1203h11m26s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1203h11m28s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 1203h11m30s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1203h43m14s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1203h43m17s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1203h38m34s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1203h44m20s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1204h16m10s
test(wave147): add transition_config unit tests (+78)
- 常量: min/max/default duration、范围合理性、cut常量
- TransitionType枚举: 14种类型值、all_supported()、is_supported()(大小写/下划线/中划线)
- TransitionConfig.parse effect: None/空/空白/cut/大小写/有效效果/去空白/
  无效降级、别名(crossfade/crossdissolve/fadein/fadeout/slide/wipe/zoomin/circle/rect)
- TransitionConfig.parse duration: None/正常/min/max/低于min/0/负/超max/字符串/无效字符串/整数
- 属性: is_cut、ffmpeg_transition(cut/fade/dissolve/slideleft/zoom/circlecrop/未知回退)
- validate: 正常cut/fade、边界min/max、时长太小/太大、未知效果
- 默认值: 默认effect/duration、parse无参/双None、dataclass验证
2026-07-28 10:53:21 +08:00

467 lines
16 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)