Files
xiaoxia-saas/tests/unit/test_speed_engine.py
CI Bot 7a72cfd709
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 2m9s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
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 / Validate - Type Check (mypy) (push) Successful in 2m19s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m7s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m7s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m17s
CI/CD Pipeline / Frontend Lint (push) Successful in 3m37s
CI/CD Pipeline / Integration Tests (push) Successful in 2m18s
CI/CD Pipeline / Unit Tests (push) Failing after 4m59s
CI/CD Pipeline / Build Staging API Image (push) Successful in 8m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m25s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 27m2s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 30m15s
style: auto-format with black + isort + prettier
2026-07-25 00:16:10 +00:00

306 lines
9.8 KiB
Python
Executable File

"""视频调速引擎单元测试 - 配置解析 + 滤镜生成等纯逻辑."""
from __future__ import annotations
import pytest
from video_processing.speed_engine import (
MAX_SPEED,
MIN_SPEED,
SpeedConfig,
SpeedEngine,
)
# ── 常量测试 ──────────────────────────────────────────────────
class TestConstants:
"""常量值测试."""
def test_speed_ranges(self):
"""速度范围合理."""
assert MIN_SPEED == 0.25
assert MAX_SPEED == 4.0
assert MIN_SPEED < MAX_SPEED
# ── SpeedConfig 测试 ────────────────────────────────────────
class TestSpeedConfigDefaults:
"""默认配置测试."""
def test_default_values(self):
"""默认值正确."""
config = SpeedConfig()
assert config.speed == 1.0
assert config.pitch_correct is True
def test_is_original_default(self):
"""默认配置是原速."""
config = SpeedConfig()
assert config.is_original is True
class TestSpeedConfigParse:
"""parse 配置解析测试."""
def test_none_returns_default(self):
"""None 返回默认配置."""
config = SpeedConfig.parse(None)
assert config.speed == 1.0
assert config.pitch_correct is True
def test_empty_dict_returns_default(self):
"""空 dict 返回默认配置."""
config = SpeedConfig.parse({})
assert config.is_original is True
def test_custom_speed(self):
"""自定义速度."""
config = SpeedConfig.parse({"speed": 2.0})
assert config.speed == 2.0
assert config.is_original is False
def test_pitch_correct_disabled(self):
"""禁用音调修正."""
config = SpeedConfig.parse({"speed": 1.5, "pitch_correct": False})
assert config.pitch_correct is False
def test_invalid_speed_type_falls_back(self):
"""无效速度类型回退到默认."""
config = SpeedConfig.parse({"speed": "fast"})
assert config.speed == 1.0
def test_invalid_pitch_correct_type_falls_back(self):
"""无效pitch_correct类型回退到默认."""
config = SpeedConfig.parse({"speed": 2.0, "pitch_correct": "yes"})
assert config.pitch_correct is True
def test_non_dict_input_returns_default(self):
"""非dict输入返回默认."""
config = SpeedConfig.parse("not_a_dict")
assert config.is_original is True
class TestSpeedConfigClamp:
"""clamp 边界钳制测试."""
def test_speed_below_min_clamped(self):
"""低于最小值钳制."""
config = SpeedConfig(speed=0.1)
config.clamp()
assert config.speed == MIN_SPEED
def test_speed_above_max_clamped(self):
"""高于最大值钳制."""
config = SpeedConfig(speed=10.0)
config.clamp()
assert config.speed == MAX_SPEED
def test_zero_speed_falls_back_to_default(self):
"""速度为0回退到默认."""
config = SpeedConfig(speed=0)
config.clamp()
assert config.speed == 1.0
def test_negative_speed_falls_back_to_default(self):
"""负速度回退到默认."""
config = SpeedConfig(speed=-2.0)
config.clamp()
assert config.speed == 1.0
def test_speed_at_min_ok(self):
"""最小值边界."""
config = SpeedConfig(speed=MIN_SPEED)
config.clamp()
assert config.speed == MIN_SPEED
def test_speed_at_max_ok(self):
"""最大值边界."""
config = SpeedConfig(speed=MAX_SPEED)
config.clamp()
assert config.speed == MAX_SPEED
def test_speed_in_range_unchanged(self):
"""合法范围内不修改."""
config = SpeedConfig(speed=1.5)
config.clamp()
assert config.speed == 1.5
def test_parse_auto_clamps(self):
"""parse 自动钳制."""
config = SpeedConfig.parse({"speed": 100.0})
assert config.speed == MAX_SPEED
class TestIsOriginal:
"""is_original 属性测试."""
def test_exactly_one(self):
"""速度恰好为1."""
assert SpeedConfig(speed=1.0).is_original is True
def test_very_close_to_one(self):
"""非常接近1也算原速."""
assert SpeedConfig(speed=1.0000001).is_original is True
def test_not_one(self):
"""不是1."""
assert SpeedConfig(speed=1.1).is_original is False
assert SpeedConfig(speed=0.9).is_original is False
# ── SpeedEngine 测试 ────────────────────────────────────────
class TestBuildVideoFilter:
"""build_video_filter 测试."""
def setup_method(self):
self.engine = SpeedEngine()
def test_original_speed_empty_filter(self):
"""原速返回空字符串(跳过滤镜)."""
config = SpeedConfig(speed=1.0)
assert self.engine.build_video_filter(config) == ""
def test_speed_up_2x(self):
"""2倍速."""
config = SpeedConfig(speed=2.0)
result = self.engine.build_video_filter(config)
assert "setpts=PTS/2.0000" == result
def test_slow_down_half(self):
"""0.5倍速."""
config = SpeedConfig(speed=0.5)
result = self.engine.build_video_filter(config)
assert "setpts=PTS/0.5000" == result
def test_contains_setpts(self):
"""包含setpts滤镜."""
config = SpeedConfig(speed=1.5)
result = self.engine.build_video_filter(config)
assert "setpts=PTS/" in result
class TestBuildAudioFilter:
"""build_audio_filter 测试."""
def setup_method(self):
self.engine = SpeedEngine()
def test_original_speed_empty_filter(self):
"""原速返回空字符串."""
config = SpeedConfig(speed=1.0)
assert self.engine.build_audio_filter(config) == ""
def test_single_stage_2x(self):
"""2倍速单级atempo."""
config = SpeedConfig(speed=2.0)
result = self.engine.build_audio_filter(config)
assert result == "atempo=2.0000"
def test_single_stage_half(self):
"""0.5倍速单级atempo."""
config = SpeedConfig(speed=0.5)
result = self.engine.build_audio_filter(config)
assert result == "atempo=0.5000"
def test_multi_stage_4x(self):
"""4倍速需要两级 atempo=2.0,atempo=2.0."""
config = SpeedConfig(speed=4.0)
result = self.engine.build_audio_filter(config)
assert result == "atempo=2.0000,atempo=2.0000"
def test_multi_stage_quarter(self):
"""0.25倍速需要两级 atempo=0.5,atempo=0.5."""
config = SpeedConfig(speed=0.25)
result = self.engine.build_audio_filter(config)
assert result == "atempo=0.5000,atempo=0.5000"
def test_multi_stage_3x(self):
"""3倍速: 2.0 * 1.5."""
config = SpeedConfig(speed=3.0)
result = self.engine.build_audio_filter(config)
stages = result.split(",")
assert len(stages) == 2
# 验证两级相乘等于3
values = [float(s.split("=")[1]) for s in stages]
assert abs(values[0] * values[1] - 3.0) < 0.01
class TestSplitAtempoStages:
"""_split_atempo_stages 测试."""
def test_single_stage_within_range(self):
"""范围内单级."""
stages = SpeedEngine._split_atempo_stages(1.5)
assert len(stages) == 1
assert stages[0] == 1.5
def test_single_stage_at_max(self):
"""最大值边界单级."""
stages = SpeedEngine._split_atempo_stages(2.0)
assert len(stages) == 1
def test_single_stage_at_min(self):
"""最小值边界单级."""
stages = SpeedEngine._split_atempo_stages(0.5)
assert len(stages) == 1
def test_multi_stage_double_speed(self):
"""4x 需要两级."""
stages = SpeedEngine._split_atempo_stages(4.0)
assert len(stages) == 2
assert abs(stages[0] * stages[1] - 4.0) < 0.01
def test_multi_stage_half_speed(self):
"""0.25x 需要两级."""
stages = SpeedEngine._split_atempo_stages(0.25)
assert len(stages) == 2
assert abs(stages[0] * stages[1] - 0.25) < 0.01
def test_all_stages_within_valid_range(self):
"""所有分级都在有效范围内."""
for speed in [0.25, 0.3, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0]:
stages = SpeedEngine._split_atempo_stages(speed)
for s in stages:
assert 0.5 <= s <= 2.0, f"speed={speed}, stage={s} out of range"
class TestAdjustDuration:
"""adjust_duration 时长计算测试."""
def setup_method(self):
self.engine = SpeedEngine()
def test_original_speed_unchanged(self):
"""原速时长不变."""
result = self.engine.adjust_duration(100.0, SpeedConfig(speed=1.0))
assert result == 100.0
def test_double_speed_half_duration(self):
"""2倍速时长减半."""
result = self.engine.adjust_duration(100.0, SpeedConfig(speed=2.0))
assert result == 50.0
def test_half_speed_double_duration(self):
"""0.5倍速时长翻倍."""
result = self.engine.adjust_duration(100.0, SpeedConfig(speed=0.5))
assert result == 200.0
def test_zero_duration_unchanged(self):
"""零时长不变."""
result = self.engine.adjust_duration(0.0, SpeedConfig(speed=2.0))
assert result == 0.0
def test_negative_duration_unchanged(self):
"""负时长不变(异常值保护)."""
result = self.engine.adjust_duration(-10.0, SpeedConfig(speed=2.0))
assert result == -10.0
def test_quarter_speed(self):
"""0.25倍速时长4倍."""
result = self.engine.adjust_duration(60.0, SpeedConfig(speed=0.25))
assert abs(result - 240.0) < 0.01