ac667e60c9
CI/CD Pipeline / Check if frontend-only change (push) Blocked by required conditions
CI/CD Pipeline / Validate - Code Quality (push) Blocked by required conditions
CI/CD Pipeline / Validate - Type Check (mypy) (push) Blocked by required conditions
CI/CD Pipeline / Validate - Migration (alembic) (push) Blocked by required conditions
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Blocked by required conditions
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Web Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (push) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
277 lines
10 KiB
Python
Executable File
277 lines
10 KiB
Python
Executable File
"""transition_config 模块单测 — 纯逻辑,无 FFmpeg 依赖."""
|
|
|
|
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_duration_bounds(self):
|
|
assert MIN_TRANSITION_DURATION == 0.3
|
|
assert MAX_TRANSITION_DURATION == 2.0
|
|
assert DEFAULT_TRANSITION_DURATION == 0.5
|
|
assert MIN_TRANSITION_DURATION < DEFAULT_TRANSITION_DURATION < MAX_TRANSITION_DURATION
|
|
|
|
def test_cut_transition(self):
|
|
assert CUT_TRANSITION == "cut"
|
|
|
|
|
|
# ── TransitionType 枚举 ──────────────────────────────────────────────────────
|
|
|
|
|
|
class TestTransitionType:
|
|
def test_all_supported_includes_all_except_cut(self):
|
|
supported = TransitionType.all_supported()
|
|
assert "cut" not in supported
|
|
assert "fade" in supported
|
|
assert "dissolve" in supported
|
|
assert len(supported) >= 10 # 至少有10种转场
|
|
|
|
def test_all_supported_unique(self):
|
|
supported = TransitionType.all_supported()
|
|
assert len(supported) == len(set(supported))
|
|
|
|
def test_is_supported_exact_match(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_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"
|
|
|
|
|
|
# ── TransitionConfig 默认值 ──────────────────────────────────────────────────
|
|
|
|
|
|
class TestTransitionConfigDefaults:
|
|
def test_default_config(self):
|
|
cfg = TransitionConfig()
|
|
assert cfg.effect == CUT_TRANSITION
|
|
assert cfg.duration == DEFAULT_TRANSITION_DURATION
|
|
assert cfg.is_cut is True
|
|
|
|
def test_is_cut_true(self):
|
|
cfg = TransitionConfig(effect="cut")
|
|
assert cfg.is_cut is True
|
|
|
|
def test_is_cut_false(self):
|
|
cfg = TransitionConfig(effect="fade")
|
|
assert cfg.is_cut is False
|
|
|
|
|
|
# ── TransitionConfig.parse ───────────────────────────────────────────────────
|
|
|
|
|
|
class TestTransitionConfigParse:
|
|
def test_none_params_default(self):
|
|
cfg = TransitionConfig.parse()
|
|
assert cfg.effect == CUT_TRANSITION
|
|
assert cfg.duration == DEFAULT_TRANSITION_DURATION
|
|
|
|
def test_empty_effect_default(self):
|
|
cfg = TransitionConfig.parse(effect="")
|
|
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_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
|