"""裁剪引擎单元测试 - 配置解析+推导等纯逻辑.""" from __future__ import annotations import pytest from video_processing.trim_engine import MIN_TRIM_DURATION, TrimConfig, TrimSegment class TestTrimConfigFromDict: """TrimConfig.from_dict 解析测试.""" def test_none_returns_none(self): """None返回None(不裁剪).""" assert TrimConfig.from_dict(None) is None def test_empty_dict_returns_none(self): """空dict返回None.""" assert TrimConfig.from_dict({}) is None def test_all_zero_returns_none(self): """全零返回None.""" assert ( TrimConfig.from_dict( { "start_time": 0, "end_time": 0, "duration": 0, } ) is None ) def test_start_only(self): """只有start_time有效.""" config = TrimConfig.from_dict({"start_time": 5.0}) assert config is not None assert config.start_time == 5.0 assert config.end_time == 0 assert config.duration == 0 def test_duration_only(self): """只有duration有效.""" config = TrimConfig.from_dict({"duration": 10.0}) assert config is not None assert config.start_time == 0 assert config.duration == 10.0 def test_start_and_duration(self): """start + duration.""" config = TrimConfig.from_dict({"start_time": 2.0, "duration": 5.0}) assert config is not None assert config.start_time == 2.0 assert config.duration == 5.0 def test_start_and_end(self): """start + end.""" config = TrimConfig.from_dict({"start_time": 1.0, "end_time": 4.0}) assert config is not None assert config.start_time == 1.0 assert config.end_time == 4.0 def test_end_and_duration(self): """end + duration.""" config = TrimConfig.from_dict({"end_time": 10.0, "duration": 3.0}) assert config is not None assert config.end_time == 10.0 assert config.duration == 3.0 def test_string_values_converted(self): """字符串值会被转换.""" config = TrimConfig.from_dict( { "start_time": "5.0", "duration": "10.0", } ) assert config is not None assert config.start_time == 5.0 assert config.duration == 10.0 def test_falsy_start_with_duration(self): """start=0 + duration>0有效.""" config = TrimConfig.from_dict({"start_time": 0, "duration": 5.0}) assert config is not None assert config.start_time == 0.0 assert config.duration == 5.0 class TestValidateAndResolve: """validate_and_resolve 推导测试.""" def test_start_plus_end(self): """start + end → 推导duration.""" config = TrimConfig(start_time=2.0, end_time=7.0) resolved = config.validate_and_resolve(60.0) assert resolved.start_time == 2.0 assert resolved.end_time == 7.0 assert resolved.duration == 5.0 def test_start_plus_duration(self): """start + duration → 推导end.""" config = TrimConfig(start_time=3.0, duration=10.0) resolved = config.validate_and_resolve(60.0) assert resolved.start_time == 3.0 assert resolved.duration == 10.0 assert resolved.end_time == 13.0 def test_end_plus_duration(self): """end + duration → 推导start.""" config = TrimConfig(end_time=15.0, duration=5.0) resolved = config.validate_and_resolve(60.0) assert resolved.end_time == 15.0 assert resolved.duration == 5.0 assert resolved.start_time == 10.0 def test_start_only_takes_to_end(self): """只有start → 取到素材末尾.""" config = TrimConfig(start_time=50.0) resolved = config.validate_and_resolve(60.0) assert resolved.start_time == 50.0 assert resolved.end_time == 60.0 assert resolved.duration == 10.0 def test_end_only_takes_from_start(self): """只有end → 从开头取.""" config = TrimConfig(end_time=20.0) resolved = config.validate_and_resolve(60.0) assert resolved.start_time == 0.0 assert resolved.end_time == 20.0 assert resolved.duration == 20.0 def test_end_before_start_invalid(self): """end < start → 无效(0时长).""" config = TrimConfig(start_time=10.0, end_time=5.0) resolved = config.validate_and_resolve(60.0) assert resolved.duration == 0.0 assert resolved.is_valid is False def test_negative_start_clamped(self): """负start钳制到0.""" config = TrimConfig(start_time=-5.0, duration=10.0) resolved = config.validate_and_resolve(60.0) assert resolved.start_time == 0.0 assert resolved.duration == 10.0 def test_end_beyond_asset_clamped(self): """end超过素材时长钳制.""" config = TrimConfig(start_time=50.0, duration=20.0) resolved = config.validate_and_resolve(60.0) assert resolved.end_time == 60.0 assert resolved.duration == 10.0 def test_start_beyond_asset_clamped(self): """start超过素材时长 → 钳制到末尾保留MIN_TRIM.""" config = TrimConfig(start_time=100.0, duration=5.0) resolved = config.validate_and_resolve(60.0) assert resolved.start_time == 60.0 - MIN_TRIM_DURATION assert resolved.end_time == 60.0 def test_zero_asset_duration(self): """素材时长为0 → 不裁剪.""" config = TrimConfig(start_time=1.0, duration=5.0) resolved = config.validate_and_resolve(0.0) assert resolved.start_time == 0.0 assert resolved.duration == 0.0 def test_end_and_duration_with_negative_start(self): """end + duration推导出来负start → 钳制+重算.""" config = TrimConfig(end_time=3.0, duration=10.0) resolved = config.validate_and_resolve(60.0) assert resolved.start_time == 0.0 assert resolved.end_time == 3.0 assert resolved.duration == 3.0 def test_all_three_params_uses_start_duration(self): """三个都给了,以start+duration为准.""" # 实际代码是先判断 start+end(情况1),如果都>0就用 # 所以这里测试 start+end 都给了且都>0的情况 config = TrimConfig(start_time=2.0, end_time=8.0, duration=10.0) resolved = config.validate_and_resolve(60.0) # 走情况1(start+end都有) assert resolved.start_time == 2.0 assert resolved.end_time == 8.0 assert resolved.duration == 6.0 class TestIsValid: """is_valid 属性测试.""" def test_valid_duration(self): """时长足够有效.""" config = TrimConfig(start_time=0.0, end_time=0.0, duration=5.0) assert config.is_valid is True def test_zero_duration_invalid(self): """零时长无效.""" config = TrimConfig(duration=0.0) assert config.is_valid is False def test_min_duration_valid(self): """刚好等于最小值有效.""" config = TrimConfig(duration=MIN_TRIM_DURATION) assert config.is_valid is True class TestIsNoop: """is_noop 属性测试.""" def test_zero_is_noop(self): """全零是noop.""" config = TrimConfig() assert config.is_noop is True def test_with_duration_not_noop(self): """有时长不是noop.""" config = TrimConfig(duration=10.0) assert config.is_noop is False def test_with_start_not_noop(self): """有start不是noop.""" config = TrimConfig(start_time=5.0) assert config.is_noop is False class TestTrimFromStart: """trim_from_start 属性测试.""" def test_zero_start_is_from_start(self): """start=0是从开头裁.""" config = TrimConfig(start_time=0.0) assert config.trim_from_start is True def test_positive_start_not_from_start(self): """有start不是从开头裁.""" config = TrimConfig(start_time=5.0) assert config.trim_from_start is False class TestTrimSegment: """TrimSegment 测试.""" def test_from_dict_basic(self): """基本解析.""" seg = TrimSegment.from_dict( { "start_time": 5.0, "duration": 10.0, "segment_id": "seg1", }, default_order=0, ) assert seg.segment_id == "seg1" assert seg.trim.start_time == 5.0 assert seg.trim.duration == 10.0 assert seg.order == 0 def test_from_dict_with_order(self): """带order的解析.""" seg = TrimSegment.from_dict( { "start_time": 1.0, "end_time": 4.0, "order": 2, } ) assert seg.order == 2 assert seg.trim.start_time == 1.0 assert seg.trim.end_time == 4.0 def test_from_dict_default_segment_id(self): """缺省segment_id时用默认值.""" seg = TrimSegment.from_dict({"duration": 5.0}, default_order=3) assert seg.segment_id == "seg_3" assert seg.order == 3 def test_from_dict_empty_string_segment_id(self): """空字符串segment_id走默认.""" seg = TrimSegment.from_dict( { "segment_id": "", "duration": 5.0, }, default_order=5, ) assert seg.segment_id == "seg_5"