5fa915cdad
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
228 lines
8.7 KiB
Python
Executable File
228 lines
8.7 KiB
Python
Executable File
"""ReverseEngine 纯逻辑单测 — 配置解析 + 滤镜构建 + 安全限制.
|
|
|
|
全纯函数测试,不依赖 FFmpeg。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from video_processing.reverse_engine import ReverseConfig, ReverseEngine
|
|
|
|
# ── ReverseConfig 解析 ────────────────────────────────────────────────────
|
|
|
|
|
|
class TestReverseConfigFromDict:
|
|
"""ReverseConfig.from_dict 配置解析测试."""
|
|
|
|
def test_none_returns_disabled(self):
|
|
"""None 输入返回 disabled 默认配置."""
|
|
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):
|
|
"""空 dict 返回 disabled."""
|
|
config = ReverseConfig.from_dict({})
|
|
assert config.enabled is False
|
|
|
|
def test_enabled_false_returns_disabled(self):
|
|
"""显式 enabled=False."""
|
|
config = ReverseConfig.from_dict({"enabled": False})
|
|
assert config.enabled is False
|
|
|
|
def test_enabled_default_flags_default_video_audio(self):
|
|
"""只启用时默认视频音频都倒放."""
|
|
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_video_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_audio_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_both_disabled_but_enabled_flag_true(self):
|
|
"""enabled=True 但两个子选项都关了(边缘情况)."""
|
|
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(self):
|
|
"""非 dict 类型回退到默认 disabled."""
|
|
config = ReverseConfig.from_dict("reverse=true")
|
|
assert config.enabled is False
|
|
|
|
def test_attribute_error_falls_back(self):
|
|
"""属性错误时回退到默认."""
|
|
|
|
class WeirdObj:
|
|
def get(self, key, default=None):
|
|
raise AttributeError("nope")
|
|
|
|
config = ReverseConfig.from_dict(WeirdObj())
|
|
assert config.enabled is False
|
|
|
|
def test_type_error_falls_back(self):
|
|
"""类型错误时回退到默认."""
|
|
config = ReverseConfig.from_dict([1, 2, 3])
|
|
assert config.enabled is False
|
|
|
|
|
|
# ── ReverseEngine 视频滤镜 ──────────────────────────────────────────────────
|
|
|
|
|
|
class TestReverseEngineBuildVideoFilter:
|
|
"""ReverseEngine.build_video_filter 视频滤镜测试."""
|
|
|
|
def test_disabled_returns_empty(self):
|
|
"""disabled 返回空字符串."""
|
|
config = ReverseConfig(enabled=False)
|
|
assert ReverseEngine.build_video_filter(config) == ""
|
|
|
|
def test_enabled_returns_reverse(self):
|
|
"""启用返回 reverse 滤镜."""
|
|
config = ReverseConfig(enabled=True, reverse_video=True)
|
|
assert ReverseEngine.build_video_filter(config) == "reverse"
|
|
|
|
def test_video_disabled_returns_empty(self):
|
|
"""reverse_video=False 返回空."""
|
|
config = ReverseConfig(enabled=True, reverse_video=False, reverse_audio=True)
|
|
assert ReverseEngine.build_video_filter(config) == ""
|
|
|
|
def test_short_duration_ok(self):
|
|
"""短时长正常返回 reverse."""
|
|
config = ReverseConfig(enabled=True)
|
|
result = ReverseEngine.build_video_filter(config, duration=10.0)
|
|
assert result == "reverse"
|
|
|
|
def test_exactly_max_duration_ok(self):
|
|
"""恰好等于安全上限正常."""
|
|
config = ReverseConfig(enabled=True)
|
|
result = ReverseEngine.build_video_filter(config, duration=ReverseEngine.MAX_SAFE_DURATION)
|
|
assert result == "reverse"
|
|
|
|
def test_over_max_duration_skipped(self):
|
|
"""超过安全时长跳过倒放."""
|
|
config = ReverseConfig(enabled=True)
|
|
result = ReverseEngine.build_video_filter(config, duration=200.0)
|
|
assert result == ""
|
|
|
|
def test_zero_duration_ok(self):
|
|
"""零时长正常倒放."""
|
|
config = ReverseConfig(enabled=True)
|
|
result = ReverseEngine.build_video_filter(config, duration=0.0)
|
|
assert result == "reverse"
|
|
|
|
|
|
# ── ReverseEngine 音频滤镜 ──────────────────────────────────────────────
|
|
|
|
|
|
class TestReverseEngineBuildAudioFilter:
|
|
"""ReverseEngine.build_audio_filter 音频滤镜测试."""
|
|
|
|
def test_disabled_returns_empty(self):
|
|
"""disabled 返回空字符串."""
|
|
config = ReverseConfig(enabled=False)
|
|
assert ReverseEngine.build_audio_filter(config) == ""
|
|
|
|
def test_enabled_returns_areverse(self):
|
|
"""启用返回 areverse 滤镜."""
|
|
config = ReverseConfig(enabled=True, reverse_audio=True)
|
|
assert ReverseEngine.build_audio_filter(config) == "areverse"
|
|
|
|
def test_audio_disabled_returns_empty(self):
|
|
"""reverse_audio=False 返回空."""
|
|
config = ReverseConfig(enabled=True, reverse_video=True, reverse_audio=False)
|
|
assert ReverseEngine.build_audio_filter(config) == ""
|
|
|
|
def test_short_duration_ok(self):
|
|
"""短时长正常返回 areverse."""
|
|
config = ReverseConfig(enabled=True)
|
|
result = ReverseEngine.build_audio_filter(config, duration=30.0)
|
|
assert result == "areverse"
|
|
|
|
def test_over_max_duration_skipped(self):
|
|
"""超过安全时长跳过音频倒放."""
|
|
config = ReverseConfig(enabled=True)
|
|
result = ReverseEngine.build_audio_filter(config, duration=150.0)
|
|
assert result == ""
|
|
|
|
def test_exactly_max_duration_ok(self):
|
|
"""恰好等于安全上限正常."""
|
|
config = ReverseConfig(enabled=True)
|
|
result = ReverseEngine.build_audio_filter(config, duration=ReverseEngine.MAX_SAFE_DURATION)
|
|
assert result == "areverse"
|
|
|
|
|
|
# ── 组合场景 ────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestReverseEngineCombined:
|
|
"""组合场景测试."""
|
|
|
|
def test_both_video_audio_reverse(self):
|
|
"""视频音频都倒放."""
|
|
config = ReverseConfig(enabled=True)
|
|
vf = ReverseEngine.build_video_filter(config)
|
|
af = ReverseEngine.build_audio_filter(config)
|
|
assert vf == "reverse"
|
|
assert af == "areverse"
|
|
|
|
def test_neither_video_nor_audio(self):
|
|
"""都不倒放."""
|
|
config = ReverseConfig(enabled=True, reverse_video=False, reverse_audio=False)
|
|
assert ReverseEngine.build_video_filter(config) == ""
|
|
assert ReverseEngine.build_audio_filter(config) == ""
|
|
|
|
def test_long_video_both_skipped(self):
|
|
"""超长视频两个都跳过."""
|
|
config = ReverseConfig(enabled=True)
|
|
duration = ReverseEngine.MAX_SAFE_DURATION + 1
|
|
assert ReverseEngine.build_video_filter(config, duration=duration) == ""
|
|
assert ReverseEngine.build_audio_filter(config, duration=duration) == ""
|
|
|
|
def test_from_dict_full_config_flow(self):
|
|
"""从 dict 解析到滤镜构建的完整流程."""
|
|
data = {"enabled": True, "reverse_video": True, "reverse_audio": False}
|
|
config = ReverseConfig.from_dict(data)
|
|
assert ReverseEngine.build_video_filter(config, duration=10) == "reverse"
|
|
assert ReverseEngine.build_audio_filter(config, duration=10) == ""
|
|
|
|
def test_from_dict_disabled_flow(self):
|
|
"""disabled 配置完整流程."""
|
|
config = ReverseConfig.from_dict({"enabled": False})
|
|
assert ReverseEngine.build_video_filter(config) == ""
|
|
assert ReverseEngine.build_audio_filter(config) == ""
|