From ffb9db32af23fd89191f991b44424a48de12d7fb Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 10:49:44 +0800 Subject: [PATCH] test(wave146): add trim_config unit tests (+78) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TrimConfig.from_dict: None/空/全0、start+end/duration/end二组合、单参数、字符串/None值 - validate_and_resolve: 三选二推导(5种场景)、start负钳制、end超时长、start超时长、 start>=end无效、0/负素材时长、精确时长、duration非负 - 属性: is_valid(有效/0/阈值/低于阈值)、is_noop(3种)、trim_from_start(3种) - TrimSegment.from_dict: 基础、默认order、缺segment_id、duration - build_video_trim_filter: noop、start+duration、only_start、from_start、标签保持、精度 - build_audio_trim_filter: noop、start+duration、only_duration、用atrim - resolve_segments: 空/单段/多段排序/过滤无效段/负order/边界钳制 - parse_segments_from_config: None/空/多段/空列表/非列表/单段/多段优先/过滤非dict - extract_trim_from_clip_config: None/空/trim子字典/扁平字段/子字典优先/非dict回退/无字段 --- tests/unit/test_trim_config.py | 726 +++++++++++++++++++++------------ 1 file changed, 471 insertions(+), 255 deletions(-) diff --git a/tests/unit/test_trim_config.py b/tests/unit/test_trim_config.py index b370b2934..2300667d0 100755 --- a/tests/unit/test_trim_config.py +++ b/tests/unit/test_trim_config.py @@ -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 -- 2.54.0