test(wave146): trim_config 单测补全 +78 #1069
+471
-255
@@ -1,4 +1,4 @@
|
||||
"""trim_config 领域模型单测."""
|
||||
"""trim_config 裁剪配置领域模型单元测试."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -15,409 +15,625 @@ from packages.domain.trim_config import (
|
||||
resolve_segments,
|
||||
)
|
||||
|
||||
# ── TrimConfig.from_dict 测试 ─────────────────────────────────────────────
|
||||
# ── TrimConfig.from_dict 测试 ────────────────────────────────────────────────
|
||||
|
||||
|
||||
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):
|
||||
"""全0返回None(不裁剪)."""
|
||||
assert TrimConfig.from_dict({"start_time": 0, "end_time": 0, "duration": 0}) is None
|
||||
|
||||
def test_start_only_valid(self):
|
||||
cfg = TrimConfig.from_dict({"start_time": 5.0})
|
||||
assert cfg is not None
|
||||
assert cfg.start_time == 5.0
|
||||
assert cfg.end_time == 0
|
||||
assert cfg.duration == 0
|
||||
|
||||
def test_duration_only_valid(self):
|
||||
cfg = TrimConfig.from_dict({"duration": 10.0})
|
||||
assert cfg is not None
|
||||
assert cfg.duration == 10.0
|
||||
assert cfg.start_time == 0
|
||||
def test_start_and_end(self):
|
||||
"""start + end."""
|
||||
result = TrimConfig.from_dict({"start_time": 5, "end_time": 10})
|
||||
assert result is not None
|
||||
assert result.start_time == 5.0
|
||||
assert result.end_time == 10.0
|
||||
|
||||
def test_start_and_duration(self):
|
||||
cfg = TrimConfig.from_dict({"start_time": 2.0, "duration": 5.0})
|
||||
assert cfg is not None
|
||||
assert cfg.start_time == 2.0
|
||||
assert cfg.duration == 5.0
|
||||
"""start + duration."""
|
||||
result = TrimConfig.from_dict({"start_time": 2, "duration": 5})
|
||||
assert result is not None
|
||||
assert result.start_time == 2.0
|
||||
assert result.duration == 5.0
|
||||
|
||||
def test_end_and_duration(self):
|
||||
"""end + duration."""
|
||||
result = TrimConfig.from_dict({"end_time": 10, "duration": 3})
|
||||
assert result is not None
|
||||
assert result.end_time == 10.0
|
||||
assert result.duration == 3.0
|
||||
|
||||
def test_only_start_returns_config(self):
|
||||
"""只有start_time也返回有效配置(从start取到末尾语义)."""
|
||||
result = TrimConfig.from_dict({"start_time": 3})
|
||||
assert result is not None
|
||||
assert result.start_time == 3.0
|
||||
|
||||
def test_only_duration_returns_config(self):
|
||||
"""只有duration也返回(从开头取duration)."""
|
||||
result = TrimConfig.from_dict({"duration": 5})
|
||||
assert result is not None
|
||||
assert result.duration == 5.0
|
||||
|
||||
def test_only_end_returns_config(self):
|
||||
"""只有end_time也返回."""
|
||||
result = TrimConfig.from_dict({"end_time": 8})
|
||||
assert result is not None
|
||||
assert result.end_time == 8.0
|
||||
|
||||
def test_string_values(self):
|
||||
"""字符串值能正确解析."""
|
||||
result = TrimConfig.from_dict({"start_time": "2.5", "duration": "3"})
|
||||
assert result is not None
|
||||
assert result.start_time == 2.5
|
||||
assert result.duration == 3.0
|
||||
|
||||
def test_none_values_treated_as_zero(self):
|
||||
"""None值当作0处理."""
|
||||
result = TrimConfig.from_dict({"start_time": None, "duration": 5})
|
||||
assert result is not None
|
||||
assert result.start_time == 0.0
|
||||
assert result.duration == 5.0
|
||||
|
||||
def test_false_values_treated_as_zero(self):
|
||||
"""0/false值当作0处理."""
|
||||
result = TrimConfig.from_dict({"start_time": 0, "duration": 0})
|
||||
assert result is None
|
||||
|
||||
def test_all_three_params(self):
|
||||
"""三个参数都给了."""
|
||||
result = TrimConfig.from_dict({"start_time": 1, "end_time": 6, "duration": 5})
|
||||
assert result is not None
|
||||
assert result.start_time == 1.0
|
||||
assert result.end_time == 6.0
|
||||
assert result.duration == 5.0
|
||||
|
||||
|
||||
# ── TrimConfig.validate_and_resolve 测试 ────────────────────────────────────
|
||||
|
||||
|
||||
class TestTrimConfigValidateAndResolve:
|
||||
"""validate_and_resolve 三选二推导 + 边界钳制测试."""
|
||||
|
||||
# 基础三选二推导
|
||||
|
||||
def test_start_and_end(self):
|
||||
cfg = TrimConfig.from_dict({"start_time": 1.0, "end_time": 5.0})
|
||||
assert cfg is not None
|
||||
assert cfg.start_time == 1.0
|
||||
assert cfg.end_time == 5.0
|
||||
"""start + end → 推导duration."""
|
||||
cfg = TrimConfig(start_time=5, end_time=15)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.start_time == 5.0
|
||||
assert result.end_time == 15.0
|
||||
assert result.duration == 10.0
|
||||
|
||||
def test_end_only(self):
|
||||
cfg = TrimConfig.from_dict({"end_time": 8.0})
|
||||
assert cfg is not None
|
||||
assert cfg.end_time == 8.0
|
||||
def test_start_and_duration(self):
|
||||
"""start + duration → 推导end."""
|
||||
cfg = TrimConfig(start_time=3, duration=7)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.start_time == 3.0
|
||||
assert result.duration == 7.0
|
||||
assert result.end_time == 10.0
|
||||
|
||||
def test_string_values_coerced(self):
|
||||
cfg = TrimConfig.from_dict({"start_time": "3.5", "duration": "2.0"})
|
||||
assert cfg is not None
|
||||
assert cfg.start_time == 3.5
|
||||
assert cfg.duration == 2.0
|
||||
def test_end_and_duration(self):
|
||||
"""end + duration → 推导start."""
|
||||
cfg = TrimConfig(end_time=20, duration=5)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.end_time == 20.0
|
||||
assert result.duration == 5.0
|
||||
assert result.start_time == 15.0
|
||||
|
||||
def test_falsy_values_treated_as_zero(self):
|
||||
cfg = TrimConfig.from_dict({"start_time": None, "duration": None})
|
||||
assert cfg is None
|
||||
def test_end_minus_duration_negative(self):
|
||||
"""end + duration 但算出start<0 → 钳制到0重新计算."""
|
||||
cfg = TrimConfig(end_time=3, duration=10)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.start_time == 0.0
|
||||
assert result.end_time == 3.0
|
||||
assert result.duration == 3.0
|
||||
|
||||
def test_default_values(self):
|
||||
def test_only_start_takes_to_end(self):
|
||||
"""只有start → 取到素材末尾."""
|
||||
cfg = TrimConfig(start_time=5)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.start_time == 5.0
|
||||
assert result.end_time == 30.0
|
||||
assert result.duration == 25.0
|
||||
|
||||
def test_only_end_takes_from_start(self):
|
||||
"""只有end → 从开头取到end."""
|
||||
cfg = TrimConfig(end_time=10)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.start_time == 0.0
|
||||
assert result.end_time == 10.0
|
||||
assert result.duration == 10.0
|
||||
|
||||
def test_only_duration(self):
|
||||
"""只有duration → 从开头取duration."""
|
||||
cfg = TrimConfig(duration=8)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.start_time == 0.0
|
||||
assert result.end_time == 8.0
|
||||
assert result.duration == 8.0
|
||||
|
||||
def test_all_zero_noop(self):
|
||||
"""全0 → noop不裁剪."""
|
||||
cfg = TrimConfig()
|
||||
assert cfg.start_time == 0.0
|
||||
assert cfg.end_time == 0.0
|
||||
assert cfg.duration == 0.0
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.start_time == 0.0
|
||||
assert result.duration == 0.0
|
||||
assert result.is_noop
|
||||
|
||||
# 边界钳制
|
||||
|
||||
# ── validate_and_resolve 测试 ─────────────────────────────────────────────
|
||||
def test_start_negative_clamped(self):
|
||||
"""start为负 → 钳制到0."""
|
||||
cfg = TrimConfig(start_time=-5, duration=10)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.start_time == 0.0
|
||||
assert result.duration == 10.0
|
||||
assert result.end_time == 10.0
|
||||
|
||||
def test_end_exceeds_asset_duration(self):
|
||||
"""end超过素材时长 → 钳制."""
|
||||
cfg = TrimConfig(start_time=5, end_time=50)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.end_time == 30.0
|
||||
assert result.duration == 25.0
|
||||
|
||||
class TestValidateAndResolve:
|
||||
def test_start_and_end_resolves_duration(self):
|
||||
cfg = TrimConfig(start_time=2.0, end_time=7.0)
|
||||
resolved = cfg.validate_and_resolve(100.0)
|
||||
assert resolved.start_time == 2.0
|
||||
assert resolved.end_time == 7.0
|
||||
assert resolved.duration == 5.0
|
||||
def test_start_exceeds_asset_duration(self):
|
||||
"""start超过素材时长 → 移到末尾取最小片段."""
|
||||
cfg = TrimConfig(start_time=40, duration=5)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.end_time == 30.0
|
||||
assert result.start_time >= 0
|
||||
assert result.duration >= 0
|
||||
|
||||
def test_start_and_duration_resolves_end(self):
|
||||
cfg = TrimConfig(start_time=3.0, duration=10.0)
|
||||
resolved = cfg.validate_and_resolve(100.0)
|
||||
assert resolved.start_time == 3.0
|
||||
assert resolved.duration == 10.0
|
||||
assert resolved.end_time == 13.0
|
||||
def test_start_equals_end_invalid(self):
|
||||
"""start >= end → 无效(duration=0)."""
|
||||
cfg = TrimConfig(start_time=10, end_time=10)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.duration == 0.0
|
||||
assert result.is_valid is False
|
||||
|
||||
def test_end_and_duration_resolves_start(self):
|
||||
cfg = TrimConfig(end_time=15.0, duration=5.0)
|
||||
resolved = cfg.validate_and_resolve(100.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):
|
||||
cfg = TrimConfig(start_time=5.0)
|
||||
resolved = cfg.validate_and_resolve(30.0)
|
||||
assert resolved.start_time == 5.0
|
||||
assert resolved.end_time == 30.0
|
||||
assert resolved.duration == 25.0
|
||||
|
||||
def test_end_only_takes_from_start(self):
|
||||
cfg = TrimConfig(end_time=8.0)
|
||||
resolved = cfg.validate_and_resolve(30.0)
|
||||
assert resolved.start_time == 0.0
|
||||
assert resolved.end_time == 8.0
|
||||
assert resolved.duration == 8.0
|
||||
|
||||
def test_duration_only_from_zero(self):
|
||||
cfg = TrimConfig(duration=10.0)
|
||||
resolved = cfg.validate_and_resolve(30.0)
|
||||
assert resolved.start_time == 0.0
|
||||
assert resolved.duration == 10.0
|
||||
assert resolved.end_time == 10.0
|
||||
|
||||
def test_negative_start_clamped(self):
|
||||
cfg = TrimConfig(start_time=-5.0, duration=10.0)
|
||||
resolved = cfg.validate_and_resolve(30.0)
|
||||
assert resolved.start_time == 0.0
|
||||
|
||||
def test_end_exceeds_asset_clamped(self):
|
||||
cfg = TrimConfig(start_time=5.0, duration=50.0)
|
||||
resolved = cfg.validate_and_resolve(30.0)
|
||||
assert resolved.end_time == 30.0
|
||||
assert resolved.duration == 25.0
|
||||
|
||||
def test_start_exceeds_asset_clamped(self):
|
||||
cfg = TrimConfig(start_time=50.0, duration=10.0)
|
||||
resolved = cfg.validate_and_resolve(30.0)
|
||||
assert resolved.start_time < 30.0
|
||||
assert resolved.end_time == 30.0
|
||||
|
||||
def test_end_before_start_invalid(self):
|
||||
cfg = TrimConfig(start_time=10.0, end_time=5.0)
|
||||
resolved = cfg.validate_and_resolve(30.0)
|
||||
assert resolved.duration == 0.0
|
||||
assert resolved.is_valid is False
|
||||
def test_start_greater_than_end(self):
|
||||
"""start > end → 无效."""
|
||||
cfg = TrimConfig(start_time=15, end_time=10)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.duration == 0.0
|
||||
assert result.is_valid is False
|
||||
|
||||
def test_zero_asset_duration(self):
|
||||
cfg = TrimConfig(start_time=1.0, duration=5.0)
|
||||
resolved = cfg.validate_and_resolve(0.0)
|
||||
assert resolved.is_noop
|
||||
"""素材时长为0 → 返回noop."""
|
||||
cfg = TrimConfig(start_time=5, duration=10)
|
||||
result = cfg.validate_and_resolve(asset_duration=0)
|
||||
assert result.start_time == 0.0
|
||||
assert result.duration == 0.0
|
||||
assert result.is_noop
|
||||
|
||||
def test_negative_asset_duration(self):
|
||||
cfg = TrimConfig(start_time=1.0, duration=5.0)
|
||||
resolved = cfg.validate_and_resolve(-1.0)
|
||||
assert resolved.is_noop
|
||||
"""素材时长为负 → 返回noop."""
|
||||
cfg = TrimConfig(start_time=1, duration=2)
|
||||
result = cfg.validate_and_resolve(asset_duration=-5)
|
||||
assert result.is_noop
|
||||
|
||||
def test_end_and_duration_with_negative_start(self):
|
||||
cfg = TrimConfig(end_time=3.0, duration=10.0)
|
||||
resolved = cfg.validate_and_resolve(30.0)
|
||||
assert resolved.start_time == 0.0
|
||||
assert resolved.end_time == 3.0
|
||||
assert resolved.duration == 3.0
|
||||
# duration 边界
|
||||
|
||||
def test_all_three_params_uses_start_duration(self):
|
||||
cfg = TrimConfig(start_time=2.0, end_time=8.0, duration=3.0)
|
||||
resolved = cfg.validate_and_resolve(30.0)
|
||||
# 有 start + end 时应该用 start+end 推导 duration
|
||||
assert resolved.start_time == 2.0
|
||||
assert resolved.end_time == 8.0
|
||||
assert resolved.duration == 6.0
|
||||
def test_duration_preserved_exactly(self):
|
||||
"""精确时长保持."""
|
||||
cfg = TrimConfig(start_time=1.234, duration=2.567)
|
||||
result = cfg.validate_and_resolve(asset_duration=10)
|
||||
assert abs(result.duration - 2.567) < 0.001
|
||||
assert abs(result.start_time - 1.234) < 0.001
|
||||
|
||||
def test_empty_config_returns_noop(self):
|
||||
cfg = TrimConfig()
|
||||
resolved = cfg.validate_and_resolve(30.0)
|
||||
assert resolved.is_noop
|
||||
def test_duration_never_negative(self):
|
||||
"""duration永远不为负."""
|
||||
cfg = TrimConfig(start_time=10, end_time=5)
|
||||
result = cfg.validate_and_resolve(asset_duration=30)
|
||||
assert result.duration >= 0
|
||||
|
||||
|
||||
# ── is_valid / is_noop / trim_from_start 测试 ─────────────────────────────
|
||||
# ── TrimConfig 属性测试 ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestProperties:
|
||||
def test_is_valid_true_for_normal(self):
|
||||
cfg = TrimConfig(start_time=0, end_time=0, duration=5.0)
|
||||
class TestTrimConfigProperties:
|
||||
"""TrimConfig 属性测试."""
|
||||
|
||||
def test_is_valid_valid_trim(self):
|
||||
"""有效裁剪."""
|
||||
cfg = TrimConfig(start_time=0, end_time=0, duration=5)
|
||||
assert cfg.is_valid is True
|
||||
|
||||
def test_is_valid_false_for_zero(self):
|
||||
cfg = TrimConfig(duration=0.0)
|
||||
def test_is_valid_zero_duration(self):
|
||||
"""duration=0无效."""
|
||||
cfg = TrimConfig(duration=0)
|
||||
assert cfg.is_valid is False
|
||||
|
||||
def test_is_valid_false_for_very_small(self):
|
||||
cfg = TrimConfig(duration=0.01)
|
||||
assert cfg.is_valid is False
|
||||
|
||||
def test_is_valid_true_at_boundary(self):
|
||||
def test_is_valid_min_threshold(self):
|
||||
"""刚好等于最小阈值也算有效."""
|
||||
cfg = TrimConfig(duration=MIN_TRIM_DURATION)
|
||||
assert cfg.is_valid is True
|
||||
|
||||
def test_is_noop_true_for_default(self):
|
||||
cfg = TrimConfig()
|
||||
def test_is_valid_below_min(self):
|
||||
"""低于最小阈值无效."""
|
||||
cfg = TrimConfig(duration=MIN_TRIM_DURATION / 2)
|
||||
assert cfg.is_valid is False
|
||||
|
||||
def test_is_noop_true(self):
|
||||
"""从0开始且duration=0是noop."""
|
||||
cfg = TrimConfig(start_time=0, duration=0)
|
||||
assert cfg.is_noop is True
|
||||
|
||||
def test_is_noop_false_with_start(self):
|
||||
cfg = TrimConfig(start_time=1.0)
|
||||
def test_is_noop_false_has_start(self):
|
||||
"""有start不是noop."""
|
||||
cfg = TrimConfig(start_time=5, duration=0)
|
||||
assert cfg.is_noop is False
|
||||
|
||||
def test_is_noop_false_with_duration(self):
|
||||
cfg = TrimConfig(duration=1.0)
|
||||
def test_is_noop_false_has_duration(self):
|
||||
"""有duration不是noop."""
|
||||
cfg = TrimConfig(start_time=0, duration=1)
|
||||
assert cfg.is_noop is False
|
||||
|
||||
def test_trim_from_start_true(self):
|
||||
cfg = TrimConfig(start_time=0.0, duration=5.0)
|
||||
"""start=0是从开头裁剪."""
|
||||
cfg = TrimConfig(start_time=0, duration=5)
|
||||
assert cfg.trim_from_start is True
|
||||
|
||||
def test_trim_from_start_false(self):
|
||||
cfg = TrimConfig(start_time=2.0, duration=5.0)
|
||||
"""start>0不是从开头裁剪."""
|
||||
cfg = TrimConfig(start_time=2, duration=5)
|
||||
assert cfg.trim_from_start is False
|
||||
|
||||
def test_trim_from_start_negative_treated_as_zero(self):
|
||||
"""start<0也认为从开头."""
|
||||
cfg = TrimConfig(start_time=-1, duration=5)
|
||||
assert cfg.trim_from_start is True
|
||||
|
||||
# ── TrimSegment 测试 ───────────────────────────────────────────────────────
|
||||
|
||||
# ── TrimSegment 测试 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestTrimSegment:
|
||||
def test_from_dict_basic(self):
|
||||
seg = TrimSegment.from_dict({"segment_id": "s1", "start_time": 1.0, "duration": 3.0})
|
||||
assert seg.segment_id == "s1"
|
||||
assert seg.trim.start_time == 1.0
|
||||
assert seg.trim.duration == 3.0
|
||||
assert seg.order == 0
|
||||
"""TrimSegment 测试."""
|
||||
|
||||
def test_from_dict_with_order(self):
|
||||
seg = TrimSegment.from_dict({"segment_id": "s2", "start_time": 0, "end_time": 5.0, "order": 2})
|
||||
def test_from_dict_basic(self):
|
||||
"""基础构造."""
|
||||
data = {"segment_id": "seg1", "start_time": 1, "end_time": 5, "order": 2}
|
||||
seg = TrimSegment.from_dict(data)
|
||||
assert seg.segment_id == "seg1"
|
||||
assert seg.order == 2
|
||||
assert seg.trim.start_time == 1.0
|
||||
assert seg.trim.end_time == 5.0
|
||||
|
||||
def test_from_dict_default_order(self):
|
||||
seg = TrimSegment.from_dict({"start_time": 1.0}, default_order=5)
|
||||
"""缺order使用默认值."""
|
||||
data = {"segment_id": "s1", "start_time": 0, "duration": 3}
|
||||
seg = TrimSegment.from_dict(data, default_order=5)
|
||||
assert seg.order == 5
|
||||
|
||||
def test_from_dict_default_segment_id(self):
|
||||
seg = TrimSegment.from_dict({"start_time": 1.0}, default_order=3)
|
||||
def test_from_dict_missing_segment_id(self):
|
||||
"""缺segment_id用默认名."""
|
||||
data = {"start_time": 0, "duration": 2}
|
||||
seg = TrimSegment.from_dict(data, default_order=3)
|
||||
assert seg.segment_id == "seg_3"
|
||||
|
||||
def test_from_dict_duration(self):
|
||||
"""duration正确传递."""
|
||||
data = {"segment_id": "s1", "duration": 10}
|
||||
seg = TrimSegment.from_dict(data)
|
||||
assert seg.trim.duration == 10.0
|
||||
|
||||
# ── build_video_trim_filter 测试 ───────────────────────────────────────────
|
||||
|
||||
# ── build_video_trim_filter 测试 ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestBuildVideoTrimFilter:
|
||||
def test_noop_returns_setpts(self):
|
||||
"""build_video_trim_filter 视频滤镜构建测试."""
|
||||
|
||||
def test_noop_filter(self):
|
||||
"""noop时只有setpts."""
|
||||
cfg = TrimConfig()
|
||||
result = build_video_trim_filter("[0:v]", cfg, "[v]")
|
||||
assert "setpts=PTS-STARTPTS" in result
|
||||
result = build_video_trim_filter("[0:v]", cfg, "[vout]")
|
||||
assert "trim=" not in result
|
||||
assert "[0:v]" in result
|
||||
assert "[v]" in result
|
||||
|
||||
def test_with_start_and_duration(self):
|
||||
cfg = TrimConfig(start_time=5.0, end_time=10.0, duration=5.0)
|
||||
result = build_video_trim_filter("[0:v]", cfg, "[out]")
|
||||
assert "trim=" in result
|
||||
assert "start=5.000" in result
|
||||
assert "duration=5.000" in result
|
||||
assert "setpts=PTS-STARTPTS" in result
|
||||
assert result.startswith("[0:v]")
|
||||
assert result.endswith("[vout]")
|
||||
|
||||
def test_contains_input_and_output_labels(self):
|
||||
cfg = TrimConfig(start_time=1.0, duration=2.0)
|
||||
result = build_video_trim_filter("[in_v]", cfg, "[out_v]")
|
||||
assert "[in_v]" in result
|
||||
assert "[out_v]" in result
|
||||
def test_start_and_duration(self):
|
||||
"""start + duration 完整滤镜."""
|
||||
cfg = TrimConfig(start_time=10, end_time=15, duration=5)
|
||||
result = build_video_trim_filter("[0:v]", cfg, "[v0]")
|
||||
assert "trim=start=10.000:duration=5.000" in result
|
||||
assert "setpts=PTS-STARTPTS" in result
|
||||
assert result.startswith("[0:v]")
|
||||
assert result.endswith("[v0]")
|
||||
|
||||
def test_duration_only(self):
|
||||
cfg = TrimConfig(duration=3.5)
|
||||
def test_only_start(self):
|
||||
"""只有start(取到末尾的情况resolve后也有duration)."""
|
||||
cfg = TrimConfig(start_time=5, end_time=30, duration=25)
|
||||
result = build_video_trim_filter("[1:v]", cfg, "[v1]")
|
||||
assert "start=5.000" in result
|
||||
assert "duration=25.000" in result
|
||||
|
||||
def test_only_duration_from_start(self):
|
||||
"""从开头裁剪duration."""
|
||||
cfg = TrimConfig(start_time=0, end_time=3, duration=3)
|
||||
result = build_video_trim_filter("[0:v]", cfg, "[out]")
|
||||
assert "trim=duration=3.000" in result or "trim=start=0" in result
|
||||
# start=0 不写,只有duration
|
||||
assert "start=0" not in result
|
||||
|
||||
def test_preserves_input_output_labels(self):
|
||||
"""保持输入输出标签."""
|
||||
cfg = TrimConfig(start_time=1, duration=2)
|
||||
result = build_video_trim_filter("[in_label]", cfg, "[out_label]")
|
||||
assert result.startswith("[in_label]")
|
||||
assert result.endswith("[out_label]")
|
||||
|
||||
def test_three_decimal_precision(self):
|
||||
"""三位小数精度."""
|
||||
cfg = TrimConfig(start_time=1.234, duration=2.678)
|
||||
result = build_video_trim_filter("[0:v]", cfg, "[v]")
|
||||
assert "duration=3.500" in result
|
||||
assert "start=" not in result
|
||||
assert "start=1.234" in result
|
||||
assert "duration=2.678" in result
|
||||
|
||||
|
||||
# ── build_audio_trim_filter 测试 ───────────────────────────────────────────
|
||||
# ── build_audio_trim_filter 测试 ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestBuildAudioTrimFilter:
|
||||
def test_noop_returns_asetpts(self):
|
||||
"""build_audio_trim_filter 音频滤镜构建测试."""
|
||||
|
||||
def test_noop_filter(self):
|
||||
"""noop时只有asetpts."""
|
||||
cfg = TrimConfig()
|
||||
result = build_audio_trim_filter("[0:a]", cfg, "[a]")
|
||||
assert "asetpts=PTS-STARTPTS" in result
|
||||
result = build_audio_trim_filter("[0:a]", cfg, "[aout]")
|
||||
assert "atrim=" not in result
|
||||
assert "asetpts=PTS-STARTPTS" in result
|
||||
assert result.startswith("[0:a]")
|
||||
assert result.endswith("[aout]")
|
||||
|
||||
def test_with_start_and_duration(self):
|
||||
cfg = TrimConfig(start_time=2.0, end_time=7.0, duration=5.0)
|
||||
result = build_audio_trim_filter("[0:a]", cfg, "[out]")
|
||||
assert "atrim=" in result
|
||||
assert "start=2.000" in result
|
||||
assert "duration=5.000" in result
|
||||
def test_start_and_duration(self):
|
||||
"""start + duration 完整滤镜."""
|
||||
cfg = TrimConfig(start_time=5, duration=3)
|
||||
result = build_audio_trim_filter("[0:a]", cfg, "[a0]")
|
||||
assert "atrim=start=5.000:duration=3.000" in result
|
||||
assert "asetpts=PTS-STARTPTS" in result
|
||||
|
||||
def test_contains_input_and_output_labels(self):
|
||||
cfg = TrimConfig(start_time=1.0, duration=2.0)
|
||||
result = build_audio_trim_filter("[in_a]", cfg, "[out_a]")
|
||||
assert "[in_a]" in result
|
||||
assert "[out_a]" in result
|
||||
def test_only_duration(self):
|
||||
"""只有duration(start=0时不写start参数)."""
|
||||
cfg = TrimConfig(start_time=0, duration=4)
|
||||
result = build_audio_trim_filter("[0:a]", cfg, "[a0]")
|
||||
assert "atrim=duration=4.000" in result
|
||||
|
||||
def test_uses_atrim_not_trim(self):
|
||||
"""用atrim不是trim."""
|
||||
cfg = TrimConfig(start_time=1, duration=2)
|
||||
result = build_audio_trim_filter("[0:a]", cfg, "[a]")
|
||||
assert "atrim=" in result
|
||||
assert ",trim=" not in result
|
||||
|
||||
|
||||
# ── resolve_segments 测试 ──────────────────────────────────────────────────
|
||||
# ── resolve_segments 测试 ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestResolveSegments:
|
||||
def test_empty_list_returns_empty(self):
|
||||
result = resolve_segments([], 30.0)
|
||||
"""resolve_segments 多段裁剪解析测试."""
|
||||
|
||||
def test_empty_list(self):
|
||||
"""空列表返回空."""
|
||||
result = resolve_segments([], asset_duration=30)
|
||||
assert result == []
|
||||
|
||||
def test_single_segment(self):
|
||||
segs = [TrimSegment(segment_id="s1", trim=TrimConfig(start_time=1.0, duration=5.0), order=0)]
|
||||
result = resolve_segments(segs, 30.0)
|
||||
"""单段解析."""
|
||||
seg = TrimSegment(
|
||||
segment_id="s1",
|
||||
trim=TrimConfig(start_time=0, duration=5),
|
||||
order=0,
|
||||
)
|
||||
result = resolve_segments([seg], asset_duration=30)
|
||||
assert len(result) == 1
|
||||
assert result[0].segment_id == "s1"
|
||||
assert result[0].trim.duration == 5.0
|
||||
|
||||
def test_invalid_segment_filters_out(self):
|
||||
def test_multiple_segments_sorted(self):
|
||||
"""多段按order排序."""
|
||||
segs = [
|
||||
TrimSegment(segment_id="good", trim=TrimConfig(start_time=0, duration=5.0), order=0),
|
||||
TrimSegment(
|
||||
segment_id="bad",
|
||||
trim=TrimConfig(start_time=5.0, end_time=5.0), # end == start → duration 0
|
||||
order=1,
|
||||
),
|
||||
TrimSegment(segment_id="s1", trim=TrimConfig(duration=2), order=2),
|
||||
TrimSegment(segment_id="s2", trim=TrimConfig(duration=3), order=0),
|
||||
TrimSegment(segment_id="s3", trim=TrimConfig(duration=1), order=1),
|
||||
]
|
||||
result = resolve_segments(segs, 30.0)
|
||||
assert len(result) == 1
|
||||
assert result[0].segment_id == "good"
|
||||
result = resolve_segments(segs, asset_duration=30)
|
||||
assert len(result) == 3
|
||||
assert result[0].segment_id == "s2"
|
||||
assert result[1].segment_id == "s3"
|
||||
assert result[2].segment_id == "s1"
|
||||
|
||||
def test_sorted_by_order(self):
|
||||
def test_filter_invalid_segments(self):
|
||||
"""过滤无效段."""
|
||||
segs = [
|
||||
TrimSegment(segment_id="s2", trim=TrimConfig(start_time=5.0, duration=3.0), order=2),
|
||||
TrimSegment(segment_id="s1", trim=TrimConfig(start_time=0, duration=3.0), order=1),
|
||||
TrimSegment(segment_id="s0", trim=TrimConfig(start_time=10.0, duration=3.0), order=0),
|
||||
TrimSegment(segment_id="valid", trim=TrimConfig(duration=5), order=0),
|
||||
TrimSegment(segment_id="invalid", trim=TrimConfig(duration=0), order=1),
|
||||
]
|
||||
result = resolve_segments(segs, 30.0)
|
||||
assert [s.segment_id for s in result] == ["s0", "s1", "s2"]
|
||||
result = resolve_segments(segs, asset_duration=30)
|
||||
assert len(result) == 1
|
||||
assert result[0].segment_id == "valid"
|
||||
|
||||
def test_negative_order_uses_index(self):
|
||||
"""order为负时使用索引."""
|
||||
segs = [
|
||||
TrimSegment(segment_id="s0", trim=TrimConfig(duration=3.0), order=-1),
|
||||
TrimSegment(segment_id="s1", trim=TrimConfig(duration=2), order=-1),
|
||||
TrimSegment(segment_id="s2", trim=TrimConfig(duration=3), order=-1),
|
||||
]
|
||||
result = resolve_segments(segs, 30.0)
|
||||
result = resolve_segments(segs, asset_duration=30)
|
||||
assert len(result) == 2
|
||||
# order用各自的index值(0, 1)
|
||||
|
||||
def test_resolves_with_asset_duration(self):
|
||||
"""用素材时长做边界钳制."""
|
||||
seg = TrimSegment(
|
||||
segment_id="s1",
|
||||
trim=TrimConfig(start_time=0, duration=50), # 超过素材时长
|
||||
order=0,
|
||||
)
|
||||
result = resolve_segments([seg], asset_duration=30)
|
||||
assert len(result) == 1
|
||||
assert result[0].order == 0
|
||||
assert result[0].trim.end_time == 30.0
|
||||
assert result[0].trim.duration == 30.0
|
||||
|
||||
|
||||
# ── parse_segments_from_config 测试 ────────────────────────────────────────
|
||||
# ── parse_segments_from_config 测试 ─────────────────────────────────────────
|
||||
|
||||
|
||||
class TestParseSegmentsFromConfig:
|
||||
def test_none_returns_empty(self):
|
||||
"""parse_segments_from_config 测试."""
|
||||
|
||||
def test_none_config(self):
|
||||
"""None返回空."""
|
||||
assert parse_segments_from_config(None) == []
|
||||
|
||||
def test_empty_dict_returns_empty(self):
|
||||
def test_empty_config(self):
|
||||
"""空dict返回空."""
|
||||
assert parse_segments_from_config({}) == []
|
||||
|
||||
def test_trim_segments_list(self):
|
||||
"""多段配置解析."""
|
||||
config = {
|
||||
"trim_segments": [
|
||||
{"segment_id": "s1", "start_time": 0, "duration": 3.0, "order": 0},
|
||||
{"segment_id": "s2", "start_time": 5.0, "duration": 2.0, "order": 1},
|
||||
{"segment_id": "s1", "start_time": 0, "duration": 3, "order": 0},
|
||||
{"segment_id": "s2", "start_time": 5, "duration": 4, "order": 1},
|
||||
]
|
||||
}
|
||||
result = parse_segments_from_config(config)
|
||||
assert len(result) == 2
|
||||
assert result[0].segment_id == "s1"
|
||||
assert result[0].trim.duration == 3.0
|
||||
assert result[1].segment_id == "s2"
|
||||
assert result[1].trim.start_time == 5.0
|
||||
|
||||
def test_trim_segments_skips_non_dict(self):
|
||||
config = {"trim_segments": [{"segment_id": "s1", "duration": 3.0}, "invalid", None]}
|
||||
result = parse_segments_from_config(config)
|
||||
assert len(result) == 1
|
||||
def test_trim_segments_empty_list(self):
|
||||
"""空segments列表 + 无单段 → 空."""
|
||||
config = {"trim_segments": []}
|
||||
assert parse_segments_from_config(config) == []
|
||||
|
||||
def test_single_trim_compat(self):
|
||||
config = {"trim_start": 1.0, "trim_duration": 5.0}
|
||||
def test_trim_segments_not_list(self):
|
||||
"""segments不是list → 回退到单段(如果有)."""
|
||||
config = {"trim_segments": "not_a_list"}
|
||||
assert parse_segments_from_config(config) == []
|
||||
|
||||
def test_single_trim_start(self):
|
||||
"""单段:trim_start."""
|
||||
config = {"trim_start": 2, "trim_duration": 5}
|
||||
result = parse_segments_from_config(config)
|
||||
assert len(result) == 1
|
||||
assert result[0].segment_id == "main"
|
||||
assert result[0].trim.start_time == 1.0
|
||||
assert result[0].trim.start_time == 2.0
|
||||
assert result[0].trim.duration == 5.0
|
||||
|
||||
def test_no_trim_fields_returns_empty(self):
|
||||
config = {"other_field": "value"}
|
||||
assert parse_segments_from_config(config) == []
|
||||
def test_single_trim_end(self):
|
||||
"""单段:trim_end."""
|
||||
config = {"trim_end": 10}
|
||||
result = parse_segments_from_config(config)
|
||||
assert len(result) == 1
|
||||
assert result[0].trim.end_time == 10.0
|
||||
|
||||
def test_segments_take_priority_over_single(self):
|
||||
"""多段配置优先于单段."""
|
||||
config = {
|
||||
"trim_segments": [{"segment_id": "s1", "start_time": 0, "duration": 2}],
|
||||
"trim_start": 5,
|
||||
"trim_duration": 3,
|
||||
}
|
||||
result = parse_segments_from_config(config)
|
||||
assert len(result) == 1
|
||||
assert result[0].segment_id == "s1" # 多段优先
|
||||
|
||||
def test_segments_filter_non_dict(self):
|
||||
"""过滤非dict元素."""
|
||||
config = {
|
||||
"trim_segments": [
|
||||
{"segment_id": "s1", "duration": 2},
|
||||
"not_a_dict",
|
||||
None,
|
||||
123,
|
||||
]
|
||||
}
|
||||
result = parse_segments_from_config(config)
|
||||
assert len(result) == 1
|
||||
assert result[0].segment_id == "s1"
|
||||
|
||||
|
||||
# ── extract_trim_from_clip_config 测试 ────────────────────────────────────
|
||||
# ── extract_trim_from_clip_config 测试 ──────────────────────────────────────
|
||||
|
||||
|
||||
class TestExtractTrimFromClipConfig:
|
||||
def test_none_returns_none(self):
|
||||
"""extract_trim_from_clip_config 测试."""
|
||||
|
||||
def test_none_config(self):
|
||||
"""None返回None."""
|
||||
assert extract_trim_from_clip_config(None) is None
|
||||
|
||||
def test_empty_dict_returns_none(self):
|
||||
def test_empty_config(self):
|
||||
"""空dict返回None."""
|
||||
assert extract_trim_from_clip_config({}) is None
|
||||
|
||||
def test_trim_subdict(self):
|
||||
config = {"trim": {"start_time": 2.0, "duration": 5.0}}
|
||||
cfg = extract_trim_from_clip_config(config)
|
||||
assert cfg is not None
|
||||
assert cfg.start_time == 2.0
|
||||
assert cfg.duration == 5.0
|
||||
|
||||
def test_flat_trim_fields(self):
|
||||
config = {"trim_start": 1.0, "trim_end": 6.0}
|
||||
cfg = extract_trim_from_clip_config(config)
|
||||
assert cfg is not None
|
||||
assert cfg.start_time == 1.0
|
||||
assert cfg.end_time == 6.0
|
||||
"""trim子字典提取."""
|
||||
config = {"trim": {"start_time": 2, "duration": 5}}
|
||||
result = extract_trim_from_clip_config(config)
|
||||
assert result is not None
|
||||
assert result.start_time == 2.0
|
||||
assert result.duration == 5.0
|
||||
|
||||
def test_trim_subdict_empty(self):
|
||||
"""trim子字典为空 → None."""
|
||||
config = {"trim": {}}
|
||||
assert extract_trim_from_clip_config(config) is None
|
||||
|
||||
def test_no_trim_fields(self):
|
||||
config = {"foo": "bar"}
|
||||
assert extract_trim_from_clip_config(config) is None
|
||||
def test_flat_trim_fields(self):
|
||||
"""扁平trim_字段."""
|
||||
config = {"trim_start": 1, "trim_end": 6}
|
||||
result = extract_trim_from_clip_config(config)
|
||||
assert result is not None
|
||||
assert result.start_time == 1.0
|
||||
assert result.end_time == 6.0
|
||||
|
||||
def test_flat_trim_duration_only(self):
|
||||
config = {"trim_duration": 10.0}
|
||||
cfg = extract_trim_from_clip_config(config)
|
||||
assert cfg is not None
|
||||
assert cfg.duration == 10.0
|
||||
def test_flat_trim_duration(self):
|
||||
"""扁平trim_duration."""
|
||||
config = {"trim_duration": 10}
|
||||
result = extract_trim_from_clip_config(config)
|
||||
assert result is not None
|
||||
assert result.duration == 10.0
|
||||
|
||||
def test_trim_subdict_priority(self):
|
||||
"""trim子字典优先于扁平字段."""
|
||||
config = {
|
||||
"trim": {"start_time": 1, "duration": 2},
|
||||
"trim_start": 10,
|
||||
"trim_duration": 20,
|
||||
}
|
||||
result = extract_trim_from_clip_config(config)
|
||||
assert result is not None
|
||||
assert result.start_time == 1.0
|
||||
assert result.duration == 2.0
|
||||
|
||||
def test_trim_not_dict_ignored(self):
|
||||
"""trim不是dict时忽略(回退到扁平字段)."""
|
||||
config = {"trim": "not_a_dict", "trim_duration": 5}
|
||||
result = extract_trim_from_clip_config(config)
|
||||
assert result is not None
|
||||
assert result.duration == 5.0
|
||||
|
||||
def test_no_trim_fields(self):
|
||||
"""无裁剪字段返回None."""
|
||||
config = {"other_field": "value", "font_size": 12}
|
||||
assert extract_trim_from_clip_config(config) is None
|
||||
|
||||
Reference in New Issue
Block a user