"""trim_config 领域模型单测.""" from __future__ import annotations import pytest from packages.domain.trim_config import ( MIN_TRIM_DURATION, TrimConfig, TrimSegment, build_audio_trim_filter, build_video_trim_filter, extract_trim_from_clip_config, parse_segments_from_config, resolve_segments, ) # ── TrimConfig.from_dict 测试 ───────────────────────────────────────────── class TestTrimConfigFromDict: def test_none_returns_none(self): assert TrimConfig.from_dict(None) is None def test_empty_dict_returns_none(self): assert TrimConfig.from_dict({}) is None def test_all_zero_returns_none(self): 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_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 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 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_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_falsy_values_treated_as_zero(self): cfg = TrimConfig.from_dict({"start_time": None, "duration": None}) assert cfg is None def test_default_values(self): cfg = TrimConfig() assert cfg.start_time == 0.0 assert cfg.end_time == 0.0 assert cfg.duration == 0.0 # ── validate_and_resolve 测试 ───────────────────────────────────────────── 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_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_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_zero_asset_duration(self): cfg = TrimConfig(start_time=1.0, duration=5.0) resolved = cfg.validate_and_resolve(0.0) assert resolved.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 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 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_empty_config_returns_noop(self): cfg = TrimConfig() resolved = cfg.validate_and_resolve(30.0) assert resolved.is_noop # ── is_valid / is_noop / trim_from_start 测试 ───────────────────────────── class TestProperties: def test_is_valid_true_for_normal(self): cfg = TrimConfig(start_time=0, end_time=0, duration=5.0) assert cfg.is_valid is True def test_is_valid_false_for_zero(self): cfg = TrimConfig(duration=0.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): cfg = TrimConfig(duration=MIN_TRIM_DURATION) assert cfg.is_valid is True def test_is_noop_true_for_default(self): cfg = TrimConfig() assert cfg.is_noop is True def test_is_noop_false_with_start(self): cfg = TrimConfig(start_time=1.0) assert cfg.is_noop is False def test_is_noop_false_with_duration(self): cfg = TrimConfig(duration=1.0) assert cfg.is_noop is False def test_trim_from_start_true(self): cfg = TrimConfig(start_time=0.0, duration=5.0) assert cfg.trim_from_start is True def test_trim_from_start_false(self): cfg = TrimConfig(start_time=2.0, duration=5.0) assert cfg.trim_from_start is False # ── 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 def test_from_dict_with_order(self): seg = TrimSegment.from_dict({"segment_id": "s2", "start_time": 0, "end_time": 5.0, "order": 2}) assert seg.order == 2 def test_from_dict_default_order(self): seg = TrimSegment.from_dict({"start_time": 1.0}, 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) assert seg.segment_id == "seg_3" # ── build_video_trim_filter 测试 ─────────────────────────────────────────── class TestBuildVideoTrimFilter: def test_noop_returns_setpts(self): cfg = TrimConfig() result = build_video_trim_filter("[0:v]", cfg, "[v]") assert "setpts=PTS-STARTPTS" in result 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 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_duration_only(self): cfg = TrimConfig(duration=3.5) result = build_video_trim_filter("[0:v]", cfg, "[v]") assert "duration=3.500" in result assert "start=" not in result # ── build_audio_trim_filter 测试 ─────────────────────────────────────────── class TestBuildAudioTrimFilter: def test_noop_returns_asetpts(self): cfg = TrimConfig() result = build_audio_trim_filter("[0:a]", cfg, "[a]") assert "asetpts=PTS-STARTPTS" in result assert "atrim=" not in result 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 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 # ── resolve_segments 测试 ────────────────────────────────────────────────── class TestResolveSegments: def test_empty_list_returns_empty(self): result = resolve_segments([], 30.0) 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) assert len(result) == 1 assert result[0].segment_id == "s1" assert result[0].trim.duration == 5.0 def test_invalid_segment_filters_out(self): 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, ), ] result = resolve_segments(segs, 30.0) assert len(result) == 1 assert result[0].segment_id == "good" def test_sorted_by_order(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), ] result = resolve_segments(segs, 30.0) assert [s.segment_id for s in result] == ["s0", "s1", "s2"] def test_negative_order_uses_index(self): segs = [ TrimSegment(segment_id="s0", trim=TrimConfig(duration=3.0), order=-1), ] result = resolve_segments(segs, 30.0) assert len(result) == 1 assert result[0].order == 0 # ── parse_segments_from_config 测试 ──────────────────────────────────────── class TestParseSegmentsFromConfig: def test_none_returns_empty(self): assert parse_segments_from_config(None) == [] def test_empty_dict_returns_empty(self): 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}, ] } result = parse_segments_from_config(config) assert len(result) == 2 assert result[0].segment_id == "s1" assert result[1].segment_id == "s2" 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_single_trim_compat(self): config = {"trim_start": 1.0, "trim_duration": 5.0} 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.duration == 5.0 def test_no_trim_fields_returns_empty(self): config = {"other_field": "value"} assert parse_segments_from_config(config) == [] # ── extract_trim_from_clip_config 测试 ──────────────────────────────────── class TestExtractTrimFromClipConfig: def test_none_returns_none(self): assert extract_trim_from_clip_config(None) is None def test_empty_dict_returns_none(self): 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 def test_trim_subdict_empty(self): 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_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