18bd0de3fa
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m42s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m44s
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m26s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m13s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m22s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 4m29s
CI/CD Pipeline / Integration Tests (push) Successful in 2m16s
CI/CD Pipeline / Unit Tests (push) Failing after 5m46s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m13s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m58s
194 lines
7.2 KiB
Python
Executable File
194 lines
7.2 KiB
Python
Executable File
"""ReverseEngine 单测 — 倒放引擎配置解析 + 滤镜构建."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
from video_processing.reverse_engine import ReverseConfig, ReverseEngine
|
||
|
||
# ── ReverseConfig.from_dict ────────────────────────────────────────────────
|
||
|
||
|
||
class TestReverseConfigFromDict:
|
||
"""ReverseConfig.from_dict 配置解析."""
|
||
|
||
def test_none_returns_disabled(self):
|
||
config = ReverseConfig.from_dict(None)
|
||
assert config.enabled is False
|
||
assert config.reverse_video is True
|
||
assert config.reverse_audio is True
|
||
|
||
def test_empty_dict_returns_disabled(self):
|
||
config = ReverseConfig.from_dict({})
|
||
assert config.enabled is False
|
||
|
||
def test_enabled_false(self):
|
||
config = ReverseConfig.from_dict({"enabled": False})
|
||
assert config.enabled is False
|
||
|
||
def test_enabled_true_defaults(self):
|
||
"""只传 enabled=True,video/audio 默认都开."""
|
||
config = ReverseConfig.from_dict({"enabled": True})
|
||
assert config.enabled is True
|
||
assert config.reverse_video is True
|
||
assert config.reverse_audio is True
|
||
|
||
def test_disable_video_only(self):
|
||
config = ReverseConfig.from_dict(
|
||
{
|
||
"enabled": True,
|
||
"reverse_video": False,
|
||
"reverse_audio": True,
|
||
}
|
||
)
|
||
assert config.enabled is True
|
||
assert config.reverse_video is False
|
||
assert config.reverse_audio is True
|
||
|
||
def test_disable_audio_only(self):
|
||
config = ReverseConfig.from_dict(
|
||
{
|
||
"enabled": True,
|
||
"reverse_video": True,
|
||
"reverse_audio": False,
|
||
}
|
||
)
|
||
assert config.enabled is True
|
||
assert config.reverse_video is True
|
||
assert config.reverse_audio is False
|
||
|
||
def test_both_disabled(self):
|
||
config = ReverseConfig.from_dict(
|
||
{
|
||
"enabled": True,
|
||
"reverse_video": False,
|
||
"reverse_audio": False,
|
||
}
|
||
)
|
||
assert config.enabled is True
|
||
assert config.reverse_video is False
|
||
assert config.reverse_audio is False
|
||
|
||
def test_invalid_type_falls_back_default(self):
|
||
"""传入非字典类型(如列表),捕获 TypeError,返回默认配置."""
|
||
config = ReverseConfig.from_dict([1, 2, 3]) # type: ignore[arg-type]
|
||
assert config.enabled is False
|
||
|
||
def test_attribute_error_falls_back(self):
|
||
"""没有 .get() 方法的对象,捕获 AttributeError,返回默认配置."""
|
||
config = ReverseConfig.from_dict(123) # type: ignore[arg-type]
|
||
assert config.enabled is False
|
||
|
||
def test_truthy_values(self):
|
||
"""非布尔真值也能被 bool() 转换."""
|
||
config = ReverseConfig.from_dict(
|
||
{
|
||
"enabled": 1,
|
||
"reverse_video": 1,
|
||
"reverse_audio": "yes",
|
||
}
|
||
)
|
||
assert config.enabled is True
|
||
assert config.reverse_video is True
|
||
assert config.reverse_audio is True
|
||
|
||
def test_falsy_values(self):
|
||
"""非布尔假值也能被 bool() 转换."""
|
||
config = ReverseConfig.from_dict(
|
||
{
|
||
"enabled": True,
|
||
"reverse_video": 0,
|
||
"reverse_audio": "",
|
||
}
|
||
)
|
||
assert config.enabled is True
|
||
assert config.reverse_video is False
|
||
assert config.reverse_audio is False
|
||
|
||
|
||
# ── ReverseEngine.build_video_filter ───────────────────────────────────────
|
||
|
||
|
||
class TestBuildVideoFilter:
|
||
"""ReverseEngine.build_video_filter 视频倒放滤镜构建."""
|
||
|
||
def test_disabled_returns_empty(self):
|
||
config = ReverseConfig(enabled=False)
|
||
result = ReverseEngine.build_video_filter(config)
|
||
assert result == ""
|
||
|
||
def test_enabled_but_video_off_returns_empty(self):
|
||
config = ReverseConfig(enabled=True, reverse_video=False, reverse_audio=True)
|
||
result = ReverseEngine.build_video_filter(config)
|
||
assert result == ""
|
||
|
||
def test_enabled_normal_duration(self):
|
||
config = ReverseConfig(enabled=True)
|
||
result = ReverseEngine.build_video_filter(config, duration=10.0)
|
||
assert result == "reverse"
|
||
|
||
def test_zero_duration(self):
|
||
config = ReverseConfig(enabled=True)
|
||
result = ReverseEngine.build_video_filter(config, duration=0.0)
|
||
assert result == "reverse"
|
||
|
||
def test_exactly_max_safe_duration(self):
|
||
"""刚好等于上限,允许."""
|
||
config = ReverseConfig(enabled=True)
|
||
result = ReverseEngine.build_video_filter(config, duration=ReverseEngine.MAX_SAFE_DURATION)
|
||
assert result == "reverse"
|
||
|
||
def test_exceeds_max_safe_duration_skips(self):
|
||
"""超过上限,跳过倒放,返回空字符串."""
|
||
config = ReverseConfig(enabled=True)
|
||
result = ReverseEngine.build_video_filter(config, duration=ReverseEngine.MAX_SAFE_DURATION + 1)
|
||
assert result == ""
|
||
|
||
def test_negative_duration(self):
|
||
"""负时长应该不会触发上限,但仍会正常返回 reverse."""
|
||
config = ReverseConfig(enabled=True)
|
||
result = ReverseEngine.build_video_filter(config, duration=-5.0)
|
||
assert result == "reverse"
|
||
|
||
|
||
# ── ReverseEngine.build_audio_filter ───────────────────────────────────────
|
||
|
||
|
||
class TestBuildAudioFilter:
|
||
"""ReverseEngine.build_audio_filter 音频倒放滤镜构建."""
|
||
|
||
def test_disabled_returns_empty(self):
|
||
config = ReverseConfig(enabled=False)
|
||
result = ReverseEngine.build_audio_filter(config)
|
||
assert result == ""
|
||
|
||
def test_enabled_but_audio_off_returns_empty(self):
|
||
config = ReverseConfig(enabled=True, reverse_video=True, reverse_audio=False)
|
||
result = ReverseEngine.build_audio_filter(config)
|
||
assert result == ""
|
||
|
||
def test_enabled_normal_duration(self):
|
||
config = ReverseConfig(enabled=True)
|
||
result = ReverseEngine.build_audio_filter(config, duration=10.0)
|
||
assert result == "areverse"
|
||
|
||
def test_zero_duration(self):
|
||
config = ReverseConfig(enabled=True)
|
||
result = ReverseEngine.build_audio_filter(config, duration=0.0)
|
||
assert result == "areverse"
|
||
|
||
def test_exactly_max_safe_duration(self):
|
||
config = ReverseConfig(enabled=True)
|
||
result = ReverseEngine.build_audio_filter(config, duration=ReverseEngine.MAX_SAFE_DURATION)
|
||
assert result == "areverse"
|
||
|
||
def test_exceeds_max_safe_duration_skips(self):
|
||
config = ReverseConfig(enabled=True)
|
||
result = ReverseEngine.build_audio_filter(config, duration=ReverseEngine.MAX_SAFE_DURATION + 1)
|
||
assert result == ""
|
||
|
||
def test_both_video_and_audio_disabled(self):
|
||
"""两个都关,两个滤镜都为空."""
|
||
config = ReverseConfig(enabled=True, reverse_video=False, reverse_audio=False)
|
||
assert ReverseEngine.build_video_filter(config, 10) == ""
|
||
assert ReverseEngine.build_audio_filter(config, 10) == ""
|