"""视频调速引擎单元测试 - 配置解析 + 滤镜生成等纯逻辑.""" 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