Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9af146fd8b | |||
| 7d1bfe74cf |
Executable
+453
@@ -0,0 +1,453 @@
|
||||
"""speed_config 调速配置领域模型单测."""
|
||||
|
||||
import pytest
|
||||
from domain.speed_config import (
|
||||
DEFAULT_SPEED,
|
||||
MAX_SPEED,
|
||||
MIN_SPEED,
|
||||
SpeedConfig,
|
||||
adjust_duration,
|
||||
build_audio_filter,
|
||||
build_clip_speed_filter,
|
||||
build_video_filter,
|
||||
resolve_clip_speed,
|
||||
)
|
||||
|
||||
# ── 常量测试 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestConstants:
|
||||
"""模块常量"""
|
||||
|
||||
def test_speed_limits(self):
|
||||
assert MIN_SPEED == 0.25
|
||||
assert MAX_SPEED == 4.0
|
||||
assert DEFAULT_SPEED == 1.0
|
||||
|
||||
|
||||
# ── SpeedConfig 默认值与基础 ────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestSpeedConfigDefaults:
|
||||
"""SpeedConfig 默认值"""
|
||||
|
||||
def test_default_values(self):
|
||||
c = SpeedConfig()
|
||||
assert c.speed == 1.0
|
||||
assert c.pitch_correct is True
|
||||
|
||||
def test_custom_values(self):
|
||||
c = SpeedConfig(speed=2.0, pitch_correct=False)
|
||||
assert c.speed == 2.0
|
||||
assert c.pitch_correct is False
|
||||
|
||||
|
||||
# ── SpeedConfig.parse ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestSpeedConfigParse:
|
||||
"""SpeedConfig.parse 工厂方法"""
|
||||
|
||||
def test_none_returns_default(self):
|
||||
c = SpeedConfig.parse(None)
|
||||
assert c.speed == 1.0
|
||||
assert c.pitch_correct is True
|
||||
|
||||
def test_empty_dict_returns_default(self):
|
||||
c = SpeedConfig.parse({})
|
||||
assert c.speed == 1.0
|
||||
|
||||
def test_not_dict_returns_default(self):
|
||||
c = SpeedConfig.parse("not a dict")
|
||||
assert c.speed == 1.0
|
||||
|
||||
def test_valid_speed(self):
|
||||
c = SpeedConfig.parse({"speed": 2.0})
|
||||
assert c.speed == 2.0
|
||||
|
||||
def test_valid_speed_int(self):
|
||||
c = SpeedConfig.parse({"speed": 2})
|
||||
assert c.speed == 2.0
|
||||
assert isinstance(c.speed, float)
|
||||
|
||||
def test_pitch_correct_false(self):
|
||||
c = SpeedConfig.parse({"pitch_correct": False})
|
||||
assert c.pitch_correct is False
|
||||
|
||||
def test_pitch_correct_non_bool_falls_back(self):
|
||||
c = SpeedConfig.parse({"pitch_correct": "true"})
|
||||
assert c.pitch_correct is True
|
||||
|
||||
def test_invalid_speed_string_falls_back(self):
|
||||
c = SpeedConfig.parse({"speed": "fast"})
|
||||
assert c.speed == 1.0
|
||||
|
||||
def test_speed_below_min_clamped(self):
|
||||
c = SpeedConfig.parse({"speed": 0.1})
|
||||
assert c.speed == MIN_SPEED
|
||||
|
||||
def test_speed_above_max_clamped(self):
|
||||
c = SpeedConfig.parse({"speed": 10.0})
|
||||
assert c.speed == MAX_SPEED
|
||||
|
||||
def test_zero_speed_falls_back_to_default(self):
|
||||
c = SpeedConfig.parse({"speed": 0})
|
||||
assert c.speed == DEFAULT_SPEED
|
||||
|
||||
def test_negative_speed_falls_back(self):
|
||||
c = SpeedConfig.parse({"speed": -1.0})
|
||||
assert c.speed == DEFAULT_SPEED
|
||||
|
||||
def test_min_speed_boundary(self):
|
||||
c = SpeedConfig.parse({"speed": 0.25})
|
||||
assert c.speed == 0.25
|
||||
|
||||
def test_max_speed_boundary(self):
|
||||
c = SpeedConfig.parse({"speed": 4.0})
|
||||
assert c.speed == 4.0
|
||||
|
||||
|
||||
# ── SpeedConfig.clamp ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestSpeedConfigClamp:
|
||||
"""SpeedConfig.clamp 方法"""
|
||||
|
||||
def test_normal_speed_no_change(self):
|
||||
c = SpeedConfig(speed=1.5)
|
||||
c.clamp()
|
||||
assert c.speed == 1.5
|
||||
|
||||
def test_zero_speed_reset_default(self):
|
||||
c = SpeedConfig(speed=0.0)
|
||||
c.clamp()
|
||||
assert c.speed == DEFAULT_SPEED
|
||||
|
||||
def test_negative_speed_reset_default(self):
|
||||
c = SpeedConfig(speed=-0.5)
|
||||
c.clamp()
|
||||
assert c.speed == DEFAULT_SPEED
|
||||
|
||||
def test_below_min_clamped(self):
|
||||
c = SpeedConfig(speed=0.1)
|
||||
c.clamp()
|
||||
assert c.speed == MIN_SPEED
|
||||
|
||||
def test_above_max_clamped(self):
|
||||
c = SpeedConfig(speed=5.0)
|
||||
c.clamp()
|
||||
assert c.speed == MAX_SPEED
|
||||
|
||||
def test_exact_min_unchanged(self):
|
||||
c = SpeedConfig(speed=MIN_SPEED)
|
||||
c.clamp()
|
||||
assert c.speed == MIN_SPEED
|
||||
|
||||
def test_exact_max_unchanged(self):
|
||||
c = SpeedConfig(speed=MAX_SPEED)
|
||||
c.clamp()
|
||||
assert c.speed == MAX_SPEED
|
||||
|
||||
|
||||
# ── SpeedConfig 属性方法 ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestSpeedConfigProperties:
|
||||
"""SpeedConfig 属性方法"""
|
||||
|
||||
def test_is_original_true(self):
|
||||
c = SpeedConfig(speed=1.0)
|
||||
assert c.is_original is True
|
||||
|
||||
def test_is_original_very_close(self):
|
||||
c = SpeedConfig(speed=1.0 + 1e-7)
|
||||
assert c.is_original is True
|
||||
|
||||
def test_is_original_false_fast(self):
|
||||
c = SpeedConfig(speed=1.5)
|
||||
assert c.is_original is False
|
||||
|
||||
def test_is_original_false_slow(self):
|
||||
c = SpeedConfig(speed=0.8)
|
||||
assert c.is_original is False
|
||||
|
||||
def test_is_fast_true(self):
|
||||
c = SpeedConfig(speed=2.0)
|
||||
assert c.is_fast is True
|
||||
|
||||
def test_is_fast_false(self):
|
||||
c = SpeedConfig(speed=0.5)
|
||||
assert c.is_fast is False
|
||||
|
||||
def test_is_fast_at_one(self):
|
||||
c = SpeedConfig(speed=1.0)
|
||||
assert c.is_fast is False
|
||||
|
||||
def test_is_slow_true(self):
|
||||
c = SpeedConfig(speed=0.5)
|
||||
assert c.is_slow is True
|
||||
|
||||
def test_is_slow_false(self):
|
||||
c = SpeedConfig(speed=2.0)
|
||||
assert c.is_slow is False
|
||||
|
||||
def test_is_slow_at_one(self):
|
||||
c = SpeedConfig(speed=1.0)
|
||||
assert c.is_slow is False
|
||||
|
||||
|
||||
# ── build_video_filter ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestBuildVideoFilter:
|
||||
"""build_video_filter 视频滤镜构建"""
|
||||
|
||||
def test_original_speed_empty(self):
|
||||
c = SpeedConfig(speed=1.0)
|
||||
assert build_video_filter(c) == ""
|
||||
|
||||
def test_double_speed(self):
|
||||
c = SpeedConfig(speed=2.0)
|
||||
result = build_video_filter(c)
|
||||
assert "setpts=PTS/2.0" in result
|
||||
|
||||
def test_half_speed(self):
|
||||
c = SpeedConfig(speed=0.5)
|
||||
result = build_video_filter(c)
|
||||
assert "setpts=PTS/0.5" in result
|
||||
|
||||
def test_format_precision(self):
|
||||
c = SpeedConfig(speed=1.5)
|
||||
result = build_video_filter(c)
|
||||
# 应该是 4 位小数
|
||||
assert "1.5000" in result
|
||||
|
||||
def test_min_speed(self):
|
||||
c = SpeedConfig(speed=0.25)
|
||||
result = build_video_filter(c)
|
||||
assert result.startswith("setpts=PTS/")
|
||||
|
||||
def test_max_speed(self):
|
||||
c = SpeedConfig(speed=4.0)
|
||||
result = build_video_filter(c)
|
||||
assert "4.0000" in result
|
||||
|
||||
|
||||
# ── build_audio_filter / atempo 拆分 ────────────────────────────────────────
|
||||
|
||||
|
||||
class TestBuildAudioFilter:
|
||||
"""build_audio_filter 音频滤镜构建"""
|
||||
|
||||
def test_original_speed_empty(self):
|
||||
c = SpeedConfig(speed=1.0)
|
||||
assert build_audio_filter(c) == ""
|
||||
|
||||
def test_within_range_single_stage(self):
|
||||
c = SpeedConfig(speed=1.5)
|
||||
result = build_audio_filter(c)
|
||||
assert result == "atempo=1.5000"
|
||||
|
||||
def test_05_speed_single_stage(self):
|
||||
c = SpeedConfig(speed=0.5)
|
||||
result = build_audio_filter(c)
|
||||
assert result == "atempo=0.5000"
|
||||
|
||||
def test_20_speed_single_stage(self):
|
||||
c = SpeedConfig(speed=2.0)
|
||||
result = build_audio_filter(c)
|
||||
assert result == "atempo=2.0000"
|
||||
|
||||
def test_4x_speed_two_stages(self):
|
||||
c = SpeedConfig(speed=4.0)
|
||||
result = build_audio_filter(c)
|
||||
# 2.0 * 2.0 = 4.0
|
||||
assert result == "atempo=2.0000,atempo=2.0000"
|
||||
|
||||
def test_025_speed_two_stages(self):
|
||||
c = SpeedConfig(speed=0.25)
|
||||
result = build_audio_filter(c)
|
||||
# 0.5 * 0.5 = 0.25
|
||||
assert result == "atempo=0.5000,atempo=0.5000"
|
||||
|
||||
def test_3x_speed_two_stages(self):
|
||||
c = SpeedConfig(speed=3.0)
|
||||
result = build_audio_filter(c)
|
||||
# 2.0 * 1.5 = 3.0
|
||||
stages = result.split(",")
|
||||
assert len(stages) == 2
|
||||
assert "atempo=2.0000" in stages[0]
|
||||
assert "atempo=1.5000" in stages[1]
|
||||
|
||||
def test_03_speed_two_stages(self):
|
||||
c = SpeedConfig(speed=0.3)
|
||||
result = build_audio_filter(c)
|
||||
stages = result.split(",")
|
||||
assert len(stages) == 2
|
||||
# 0.5 * 0.6 = 0.3
|
||||
assert "atempo=0.5000" in stages[0]
|
||||
|
||||
def test_format_each_stage(self):
|
||||
c = SpeedConfig(speed=1.2345)
|
||||
result = build_audio_filter(c)
|
||||
assert "atempo=1.2345" in result
|
||||
|
||||
|
||||
class TestAtempoStages:
|
||||
"""atempo 多级拆分逻辑验证"""
|
||||
|
||||
def _extract_speeds(self, filter_str: str) -> list[float]:
|
||||
"""从 atempo 滤镜字符串中提取速度值."""
|
||||
import re
|
||||
|
||||
return [float(m) for m in re.findall(r"atempo=([\d.]+)", filter_str)]
|
||||
|
||||
def test_product_equals_speed_fast_3x(self):
|
||||
c = SpeedConfig(speed=3.0)
|
||||
speeds = self._extract_speeds(build_audio_filter(c))
|
||||
product = 1.0
|
||||
for s in speeds:
|
||||
product *= s
|
||||
assert abs(product - 3.0) < 1e-4
|
||||
|
||||
def test_product_equals_speed_4x(self):
|
||||
c = SpeedConfig(speed=4.0)
|
||||
speeds = self._extract_speeds(build_audio_filter(c))
|
||||
product = 1.0
|
||||
for s in speeds:
|
||||
product *= s
|
||||
assert abs(product - 4.0) < 1e-4
|
||||
|
||||
def test_product_equals_speed_slow_025(self):
|
||||
c = SpeedConfig(speed=0.25)
|
||||
speeds = self._extract_speeds(build_audio_filter(c))
|
||||
product = 1.0
|
||||
for s in speeds:
|
||||
product *= s
|
||||
assert abs(product - 0.25) < 1e-4
|
||||
|
||||
def test_product_equals_speed_slow_03(self):
|
||||
c = SpeedConfig(speed=0.3)
|
||||
speeds = self._extract_speeds(build_audio_filter(c))
|
||||
product = 1.0
|
||||
for s in speeds:
|
||||
product *= s
|
||||
assert abs(product - 0.3) < 1e-4
|
||||
|
||||
def test_each_stage_in_range_fast(self):
|
||||
c = SpeedConfig(speed=3.5)
|
||||
speeds = self._extract_speeds(build_audio_filter(c))
|
||||
for s in speeds:
|
||||
assert 0.5 <= s <= 2.0
|
||||
|
||||
def test_each_stage_in_range_slow(self):
|
||||
c = SpeedConfig(speed=0.35)
|
||||
speeds = self._extract_speeds(build_audio_filter(c))
|
||||
for s in speeds:
|
||||
assert 0.5 <= s <= 2.0
|
||||
|
||||
|
||||
# ── adjust_duration ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestAdjustDuration:
|
||||
"""adjust_duration 时长计算"""
|
||||
|
||||
def test_original_speed_no_change(self):
|
||||
assert adjust_duration(10.0, SpeedConfig(speed=1.0)) == 10.0
|
||||
|
||||
def test_double_speed_half_duration(self):
|
||||
assert adjust_duration(10.0, SpeedConfig(speed=2.0)) == 5.0
|
||||
|
||||
def test_half_speed_double_duration(self):
|
||||
assert adjust_duration(10.0, SpeedConfig(speed=0.5)) == 20.0
|
||||
|
||||
def test_zero_duration_unchanged(self):
|
||||
assert adjust_duration(0.0, SpeedConfig(speed=2.0)) == 0.0
|
||||
|
||||
def test_negative_duration_unchanged(self):
|
||||
assert adjust_duration(-1.0, SpeedConfig(speed=2.0)) == -1.0
|
||||
|
||||
def test_original_with_zero_duration(self):
|
||||
assert adjust_duration(0.0, SpeedConfig(speed=1.0)) == 0.0
|
||||
|
||||
def test_triple_speed(self):
|
||||
assert adjust_duration(30.0, SpeedConfig(speed=3.0)) == 10.0
|
||||
|
||||
def test_quarter_speed(self):
|
||||
assert adjust_duration(10.0, SpeedConfig(speed=0.25)) == 40.0
|
||||
|
||||
|
||||
# ── build_clip_speed_filter ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestBuildClipSpeedFilter:
|
||||
"""build_clip_speed_filter 便捷方法"""
|
||||
|
||||
def test_returns_tuple_of_three(self):
|
||||
result = build_clip_speed_filter(1.5)
|
||||
assert len(result) == 3
|
||||
video_filter, audio_filter, config = result
|
||||
assert isinstance(video_filter, str)
|
||||
assert isinstance(audio_filter, str)
|
||||
assert isinstance(config, SpeedConfig)
|
||||
|
||||
def test_normal_speed(self):
|
||||
video_filter, audio_filter, config = build_clip_speed_filter(1.0)
|
||||
assert video_filter == ""
|
||||
assert audio_filter == ""
|
||||
assert config.speed == 1.0
|
||||
|
||||
def test_double_speed(self):
|
||||
video_filter, audio_filter, config = build_clip_speed_filter(2.0)
|
||||
assert "setpts" in video_filter
|
||||
assert "atempo" in audio_filter
|
||||
assert config.speed == 2.0
|
||||
|
||||
def test_clamps_speed(self):
|
||||
_, _, config = build_clip_speed_filter(10.0)
|
||||
assert config.speed == MAX_SPEED
|
||||
|
||||
def test_pitch_correct_false(self):
|
||||
# pitch_correct=False 时仍然生成滤镜(实际使用中可能换其他算法,但接口返回不变)
|
||||
video_filter, audio_filter, config = build_clip_speed_filter(2.0, pitch_correct=False)
|
||||
assert config.pitch_correct is False
|
||||
assert "setpts" in video_filter
|
||||
|
||||
|
||||
# ── resolve_clip_speed ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestResolveClipSpeed:
|
||||
"""resolve_clip_speed 片段速度解析"""
|
||||
|
||||
def test_none_config_uses_global(self):
|
||||
assert resolve_clip_speed(None, 1.5) == 1.5
|
||||
|
||||
def test_zero_speed_uses_global(self):
|
||||
assert resolve_clip_speed({"playback_speed": 0}, 1.5) == 1.5
|
||||
|
||||
def test_missing_key_uses_global(self):
|
||||
assert resolve_clip_speed({}, 2.0) == 2.0
|
||||
|
||||
def test_valid_speed(self):
|
||||
assert resolve_clip_speed({"playback_speed": 1.5}, 1.0) == 1.5
|
||||
|
||||
def test_negative_speed_uses_global(self):
|
||||
assert resolve_clip_speed({"playback_speed": -1.0}, 1.0) == 1.0
|
||||
|
||||
def test_invalid_type_uses_global(self):
|
||||
assert resolve_clip_speed({"playback_speed": "fast"}, 1.0) == 1.0
|
||||
|
||||
def test_default_global_is_one(self):
|
||||
assert resolve_clip_speed({"playback_speed": 0}) == 1.0
|
||||
|
||||
def test_int_speed(self):
|
||||
result = resolve_clip_speed({"playback_speed": 2})
|
||||
assert result == 2.0
|
||||
assert isinstance(result, float)
|
||||
|
||||
def test_very_small_positive_uses_it(self):
|
||||
# 只要 > 0 就用
|
||||
result = resolve_clip_speed({"playback_speed": 0.1})
|
||||
assert result == 0.1
|
||||
Reference in New Issue
Block a user