diff --git a/tests/unit/test_video_concat.py b/tests/unit/test_video_concat.py index 7d78551d7..94656d7e3 100755 --- a/tests/unit/test_video_concat.py +++ b/tests/unit/test_video_concat.py @@ -1,4 +1,4 @@ -"""video_concat 领域模型单测 — 纯逻辑,48个测试用例.""" +"""video_concat 视频拼接领域模型单元测试.""" from __future__ import annotations @@ -12,125 +12,192 @@ from packages.domain.video_concat import ( ConcatSegment, ) +# ── 常量测试 ────────────────────────────────────────────────────────────────── + + +class TestConstants: + """常量测试.""" + + def test_max_concat_segments(self): + """最大拼接段数.""" + assert MAX_CONCAT_SEGMENTS == 50 + + def test_allowed_extensions_not_empty(self): + """支持的视频格式不为空.""" + assert len(ALLOWED_VIDEO_EXTENSIONS) > 0 + + def test_common_formats_supported(self): + """常见格式都支持.""" + assert ".mp4" in ALLOWED_VIDEO_EXTENSIONS + assert ".mov" in ALLOWED_VIDEO_EXTENSIONS + assert ".avi" in ALLOWED_VIDEO_EXTENSIONS + assert ".mkv" in ALLOWED_VIDEO_EXTENSIONS + assert ".webm" in ALLOWED_VIDEO_EXTENSIONS + + def test_demuxer_params_not_empty(self): + """demuxer必需参数不为空.""" + assert len(CONCAT_DEMUXER_REQUIRED_PARAMS) > 0 + + def test_demuxer_params_include_codec(self): + """包含编解码相关参数.""" + assert "codec_name" in CONCAT_DEMUXER_REQUIRED_PARAMS + assert "width" in CONCAT_DEMUXER_REQUIRED_PARAMS + assert "height" in CONCAT_DEMUXER_REQUIRED_PARAMS + assert "r_frame_rate" in CONCAT_DEMUXER_REQUIRED_PARAMS + + # ── ConcatSegment 测试 ─────────────────────────────────────────────────────── -class TestConcatSegmentBasics: - def test_default_values(self): - seg = ConcatSegment(video_path="test.mp4") - assert seg.video_path == "test.mp4" +class TestConcatSegment: + """ConcatSegment 测试.""" + + def test_basic_creation(self): + """基础创建.""" + seg = ConcatSegment(video_path="/tmp/video.mp4") + assert seg.video_path == "/tmp/video.mp4" assert seg.start_time == 0.0 assert seg.duration == 0.0 assert seg.has_audio is True - def test_full_params(self): + def test_full_creation(self): + """完整字段创建.""" seg = ConcatSegment( - video_path="video.mp4", + video_path="/tmp/v.mov", start_time=5.5, duration=10.0, has_audio=False, ) - assert seg.video_path == "video.mp4" + assert seg.video_path == "/tmp/v.mov" assert seg.start_time == 5.5 assert seg.duration == 10.0 assert seg.has_audio is False + # is_valid 属性 -class TestConcatSegmentFromDict: - def test_normal_dict(self): - seg = ConcatSegment.from_dict( - { - "video_path": "test.mp4", - "start_time": 2.0, - "duration": 5.0, - "has_audio": False, - } - ) - assert seg.video_path == "test.mp4" - assert seg.start_time == 2.0 - assert seg.duration == 5.0 - assert seg.has_audio is False - - def test_empty_dict(self): - seg = ConcatSegment.from_dict({}) - assert seg.video_path == "" - assert seg.start_time == 0.0 - assert seg.duration == 0.0 - assert seg.has_audio is True - - def test_none_input(self): - seg = ConcatSegment.from_dict(None) - assert seg.video_path == "" - assert seg.is_valid is False - - def test_non_dict_input(self): - seg = ConcatSegment.from_dict("not a dict") - assert seg.video_path == "" - - def test_start_time_negative_clamped(self): - seg = ConcatSegment.from_dict({"video_path": "a.mp4", "start_time": -5}) - assert seg.start_time == 0.0 - - def test_duration_negative_clamped(self): - seg = ConcatSegment.from_dict({"video_path": "a.mp4", "duration": -10}) - assert seg.duration == 0.0 - - def test_start_time_invalid_string(self): - seg = ConcatSegment.from_dict({"video_path": "a.mp4", "start_time": "abc"}) - assert seg.start_time == 0.0 - - def test_duration_invalid_string(self): - seg = ConcatSegment.from_dict({"video_path": "a.mp4", "duration": "xyz"}) - assert seg.duration == 0.0 - - def test_start_time_int_casted(self): - seg = ConcatSegment.from_dict({"video_path": "a.mp4", "start_time": 3}) - assert seg.start_time == 3.0 - - def test_duration_int_casted(self): - seg = ConcatSegment.from_dict({"video_path": "a.mp4", "duration": 7}) - assert seg.duration == 7.0 - - def test_video_path_casted_to_string(self): - seg = ConcatSegment.from_dict({"video_path": 12345}) - assert seg.video_path == "12345" - - def test_has_audio_false(self): - seg = ConcatSegment.from_dict({"video_path": "a.mp4", "has_audio": False}) - assert seg.has_audio is False - - def test_has_audio_truthy_value(self): - seg = ConcatSegment.from_dict({"video_path": "a.mp4", "has_audio": 1}) - assert seg.has_audio is True - - -class TestConcatSegmentProperties: def test_is_valid_with_path(self): - seg = ConcatSegment(video_path="test.mp4") + """有视频路径是有效的.""" + seg = ConcatSegment(video_path="/tmp/v.mp4") assert seg.is_valid is True def test_is_valid_empty_path(self): + """空路径无效.""" seg = ConcatSegment(video_path="") assert seg.is_valid is False def test_effective_duration_positive(self): - seg = ConcatSegment(video_path="a.mp4", duration=10.5) + """正的effective_duration.""" + seg = ConcatSegment(video_path="v.mp4", duration=10.5) assert seg.effective_duration == 10.5 def test_effective_duration_zero(self): - seg = ConcatSegment(video_path="a.mp4", duration=0.0) + """duration为0时effective_duration为0.""" + seg = ConcatSegment(video_path="v.mp4", duration=0) assert seg.effective_duration == 0.0 def test_effective_duration_negative(self): - seg = ConcatSegment(video_path="a.mp4", duration=-5.0) + """负的duration被钳制到0.""" + seg = ConcatSegment(video_path="v.mp4", duration=-5) assert seg.effective_duration == 0.0 -# ── ConcatConfig 测试 ──────────────────────────────────────────────────────── +# ── ConcatSegment.from_dict 测试 ───────────────────────────────────────────── -class TestConcatConfigBasics: - def test_default_values(self): +class TestConcatSegmentFromDict: + """ConcatSegment.from_dict 工厂方法测试.""" + + def test_none_input(self): + """None输入返回默认片段(空路径).""" + seg = ConcatSegment.from_dict(None) + assert seg.video_path == "" + assert seg.is_valid is False + + def test_empty_dict(self): + """空dict返回默认.""" + seg = ConcatSegment.from_dict({}) + assert seg.video_path == "" + + def test_not_dict_input(self): + """非dict输入安全处理.""" + seg = ConcatSegment.from_dict("not_a_dict") + assert seg.video_path == "" + + def test_list_input(self): + """list输入安全处理.""" + seg = ConcatSegment.from_dict([1, 2, 3]) + assert seg.video_path == "" + + def test_with_video_path(self): + """带视频路径.""" + seg = ConcatSegment.from_dict({"video_path": "/tmp/v.mp4"}) + assert seg.video_path == "/tmp/v.mp4" + assert seg.is_valid is True + + def test_with_start_time(self): + """带start_time.""" + seg = ConcatSegment.from_dict({"video_path": "v.mp4", "start_time": 3.5}) + assert seg.start_time == 3.5 + + def test_start_time_negative_clamped(self): + """负的start_time钳制到0.""" + seg = ConcatSegment.from_dict({"video_path": "v.mp4", "start_time": -5}) + assert seg.start_time == 0.0 + + def test_start_time_invalid_string(self): + """无效start_time字符串回退到0.""" + seg = ConcatSegment.from_dict({"video_path": "v.mp4", "start_time": "abc"}) + assert seg.start_time == 0.0 + + def test_with_duration(self): + """带duration.""" + seg = ConcatSegment.from_dict({"video_path": "v.mp4", "duration": 8.5}) + assert seg.duration == 8.5 + + def test_duration_negative_clamped(self): + """负的duration钳制到0.""" + seg = ConcatSegment.from_dict({"video_path": "v.mp4", "duration": -10}) + assert seg.duration == 0.0 + + def test_duration_invalid_string(self): + """无效duration回退到0.""" + seg = ConcatSegment.from_dict({"video_path": "v.mp4", "duration": "xyz"}) + assert seg.duration == 0.0 + + def test_has_audio_true(self): + """has_audio为True.""" + seg = ConcatSegment.from_dict({"video_path": "v.mp4", "has_audio": True}) + assert seg.has_audio is True + + def test_has_audio_false(self): + """has_audio为False.""" + seg = ConcatSegment.from_dict({"video_path": "v.mp4", "has_audio": False}) + assert seg.has_audio is False + + def test_has_audio_default_true(self): + """has_audio默认True.""" + seg = ConcatSegment.from_dict({"video_path": "v.mp4"}) + assert seg.has_audio is True + + def test_string_start_time(self): + """字符串形式的start_time.""" + seg = ConcatSegment.from_dict({"video_path": "v.mp4", "start_time": "2.5"}) + assert seg.start_time == 2.5 + + def test_int_duration(self): + """整数duration.""" + seg = ConcatSegment.from_dict({"video_path": "v.mp4", "duration": 10}) + assert seg.duration == 10.0 + + +# ── ConcatConfig 基础测试 ──────────────────────────────────────────────────── + + +class TestConcatConfig: + """ConcatConfig 基础测试.""" + + def test_default_creation(self): + """默认创建.""" cfg = ConcatConfig() assert cfg.segments == [] assert cfg.output_width == 0 @@ -141,224 +208,294 @@ class TestConcatConfigBasics: assert cfg.transition_duration == 0.3 def test_with_segments(self): - segs = [ConcatSegment(video_path="a.mp4")] - cfg = ConcatConfig(segments=segs) - assert len(cfg.segments) == 1 - assert cfg.segments[0].video_path == "a.mp4" - - -class TestConcatConfigFromDict: - def test_none_config(self): - cfg = ConcatConfig.from_config_dict(None) - assert cfg.segments == [] - assert cfg.output_width == 0 - - def test_empty_dict(self): - cfg = ConcatConfig.from_config_dict({}) - assert cfg.segments == [] - - def test_non_dict_input(self): - cfg = ConcatConfig.from_config_dict("config") - assert cfg.segments == [] - - def test_with_valid_segments(self): - cfg = ConcatConfig.from_config_dict( - { - "segments": [ - {"video_path": "a.mp4", "duration": 10}, - {"video_path": "b.mp4", "duration": 20}, - ], - } - ) + """带片段创建.""" + segs = [ConcatSegment(video_path="v1.mp4"), ConcatSegment(video_path="v2.mp4")] + cfg = ConcatConfig(segments=segs, output_width=1920, output_height=1080) assert len(cfg.segments) == 2 - assert cfg.segments[0].video_path == "a.mp4" - assert cfg.segments[1].video_path == "b.mp4" - - def test_skips_empty_video_path(self): - cfg = ConcatConfig.from_config_dict( - { - "segments": [ - {"video_path": "a.mp4"}, - {"video_path": ""}, - {"video_path": "b.mp4"}, - ], - } - ) - assert len(cfg.segments) == 2 - - def test_skips_invalid_segment_dict(self): - cfg = ConcatConfig.from_config_dict( - { - "segments": [ - {"video_path": "a.mp4"}, - "not a dict", - {"video_path": "b.mp4"}, - ], - } - ) - assert len(cfg.segments) == 2 - - def test_segments_not_a_list(self): - cfg = ConcatConfig.from_config_dict({"segments": "not a list"}) - assert cfg.segments == [] - - def test_output_params(self): - cfg = ConcatConfig.from_config_dict( - { - "output_width": 1920, - "output_height": 1080, - "output_fps": 30.0, - "force_reencode": True, - } - ) assert cfg.output_width == 1920 assert cfg.output_height == 1080 - assert cfg.output_fps == 30.0 - assert cfg.force_reencode is True - def test_output_width_negative_clamped(self): - cfg = ConcatConfig.from_config_dict({"output_width": -100}) - assert cfg.output_width == 0 + # 属性测试 - def test_output_height_invalid_string(self): - cfg = ConcatConfig.from_config_dict({"output_height": "abc"}) - assert cfg.output_height == 0 - - def test_output_fps_invalid_string(self): - cfg = ConcatConfig.from_config_dict({"output_fps": "xyz"}) - assert cfg.output_fps == 0.0 - - def test_transition_params(self): - cfg = ConcatConfig.from_config_dict( - { - "transition": "crossfade", - "transition_duration": 1.0, - } - ) - assert cfg.transition == "crossfade" - assert cfg.transition_duration == 1.0 - - def test_transition_duration_minimum(self): - cfg = ConcatConfig.from_config_dict({"transition_duration": 0.01}) - assert cfg.transition_duration == 0.1 - - def test_transition_duration_negative(self): - cfg = ConcatConfig.from_config_dict({"transition_duration": -1}) - assert cfg.transition_duration == 0.1 - - def test_force_reencode_false_by_default(self): - cfg = ConcatConfig.from_config_dict({}) - assert cfg.force_reencode is False - - -class TestConcatConfigProperties: - def test_has_effect_two_segments(self): + def test_has_effect_with_two_segments(self): + """2个以上有效片段has_effect为True.""" cfg = ConcatConfig( segments=[ - ConcatSegment(video_path="a.mp4"), - ConcatSegment(video_path="b.mp4"), + ConcatSegment(video_path="v1.mp4"), + ConcatSegment(video_path="v2.mp4"), ] ) assert cfg.has_effect is True - def test_has_effect_one_segment(self): - cfg = ConcatConfig(segments=[ConcatSegment(video_path="a.mp4")]) + def test_has_effect_with_one_segment(self): + """只有1个有效片段has_effect为False.""" + cfg = ConcatConfig(segments=[ConcatSegment(video_path="v1.mp4")]) assert cfg.has_effect is False - def test_has_effect_empty(self): + def test_has_effect_with_no_segments(self): + """空片段has_effect为False.""" cfg = ConcatConfig() assert cfg.has_effect is False - def test_has_effect_skips_invalid(self): - cfg = ConcatConfig( - segments=[ - ConcatSegment(video_path="a.mp4"), - ConcatSegment(video_path=""), - ConcatSegment(video_path="b.mp4"), - ] - ) - assert cfg.has_effect is True - def test_valid_segment_count(self): + """有效片段计数.""" cfg = ConcatConfig( segments=[ - ConcatSegment(video_path="a.mp4"), - ConcatSegment(video_path=""), - ConcatSegment(video_path="b.mp4"), + ConcatSegment(video_path="v1.mp4"), + ConcatSegment(video_path=""), # 无效 + ConcatSegment(video_path="v2.mp4"), ] ) assert cfg.valid_segment_count == 2 - def test_first_valid_segment(self): + def test_total_segments_alias(self): + """total_segments是valid_segment_count的别名.""" cfg = ConcatConfig( segments=[ - ConcatSegment(video_path=""), - ConcatSegment(video_path="first.mp4"), + ConcatSegment(video_path="v1.mp4"), + ConcatSegment(video_path="v2.mp4"), + ] + ) + assert cfg.total_segments == cfg.valid_segment_count + assert cfg.total_segments == 2 + + def test_first_valid_segment(self): + """第一个有效片段.""" + cfg = ConcatConfig( + segments=[ + ConcatSegment(video_path=""), # 无效 + ConcatSegment(video_path="first_valid.mp4"), ConcatSegment(video_path="second.mp4"), ] ) - assert cfg.first_valid_segment is not None - assert cfg.first_valid_segment.video_path == "first.mp4" + first = cfg.first_valid_segment + assert first is not None + assert first.video_path == "first_valid.mp4" - def test_first_valid_segment_none_when_all_empty(self): - cfg = ConcatConfig( - segments=[ - ConcatSegment(video_path=""), - ConcatSegment(video_path=""), - ] - ) + def test_first_valid_segment_none(self): + """无有效片段时first_valid_segment为None.""" + cfg = ConcatConfig(segments=[ConcatSegment(video_path="")]) assert cfg.first_valid_segment is None - def test_first_valid_segment_empty_list(self): + def test_first_valid_segment_empty(self): + """空列表时为None.""" cfg = ConcatConfig() assert cfg.first_valid_segment is None def test_estimated_total_duration(self): + """估算总时长.""" cfg = ConcatConfig( segments=[ - ConcatSegment(video_path="a.mp4", duration=10.0), - ConcatSegment(video_path="b.mp4", duration=20.0), - ConcatSegment(video_path="c.mp4", duration=0.0), + ConcatSegment(video_path="v1.mp4", duration=10.0), + ConcatSegment(video_path="v2.mp4", duration=5.5), + ConcatSegment(video_path="v3.mp4", duration=0), # 不计入 ] ) - assert cfg.estimated_total_duration == 30.0 + assert cfg.estimated_total_duration == pytest.approx(15.5) + + def test_estimated_total_duration_empty(self): + """空片段时长为0.""" + cfg = ConcatConfig() + assert cfg.estimated_total_duration == 0.0 def test_estimated_total_duration_skips_invalid(self): + """跳过无效片段.""" cfg = ConcatConfig( segments=[ - ConcatSegment(video_path="", duration=10.0), - ConcatSegment(video_path="a.mp4", duration=5.0), + ConcatSegment(video_path="", duration=100), # 无效,跳过 + ConcatSegment(video_path="v1.mp4", duration=5.0), ] ) assert cfg.estimated_total_duration == 5.0 -class TestConcatConfigClampSegments: - def test_clamp_when_over_max(self): - segs = [ConcatSegment(video_path=f"s{i}.mp4") for i in range(100)] - cfg = ConcatConfig(segments=segs) - cfg.clamp_segments(50) - assert len(cfg.segments) == 50 - assert cfg.segments[0].video_path == "s0.mp4" - assert cfg.segments[-1].video_path == "s49.mp4" +# ── ConcatConfig.from_config_dict 测试 ─────────────────────────────────────── - def test_no_clamp_when_under_max(self): - segs = [ConcatSegment(video_path=f"s{i}.mp4") for i in range(10)] + +class TestConcatConfigFromConfigDict: + """ConcatConfig.from_config_dict 工厂方法测试.""" + + def test_none_config(self): + """None返回默认配置.""" + cfg = ConcatConfig.from_config_dict(None) + assert cfg.segments == [] + assert cfg.output_width == 0 + + def test_empty_config(self): + """空dict返回默认.""" + cfg = ConcatConfig.from_config_dict({}) + assert cfg.segments == [] + + def test_not_dict(self): + """非dict安全处理.""" + cfg = ConcatConfig.from_config_dict("not_dict") + assert cfg.segments == [] + + def test_with_segments(self): + """带片段列表.""" + cfg = ConcatConfig.from_config_dict( + { + "segments": [ + {"video_path": "v1.mp4", "duration": 10}, + {"video_path": "v2.mp4", "start_time": 2}, + ] + } + ) + assert len(cfg.segments) == 2 + assert cfg.segments[0].video_path == "v1.mp4" + assert cfg.segments[0].duration == 10.0 + assert cfg.segments[1].start_time == 2.0 + + def test_segments_not_list(self): + """segments不是list时忽略.""" + cfg = ConcatConfig.from_config_dict({"segments": "not_a_list"}) + assert cfg.segments == [] + + def test_skips_segments_without_path(self): + """跳过没有video_path的片段.""" + cfg = ConcatConfig.from_config_dict( + { + "segments": [ + {"video_path": "v1.mp4"}, + {"duration": 5}, # 没有video_path + {"video_path": ""}, # 空path + ] + } + ) + assert len(cfg.segments) == 1 + assert cfg.segments[0].video_path == "v1.mp4" + + def test_skips_non_dict_segments(self): + """跳过非dict片段.""" + cfg = ConcatConfig.from_config_dict( + { + "segments": [ + {"video_path": "v1.mp4"}, + "not_a_dict", + None, + 123, + ] + } + ) + assert len(cfg.segments) == 1 + + def test_output_dimensions(self): + """输出尺寸.""" + cfg = ConcatConfig.from_config_dict( + { + "output_width": 1920, + "output_height": 1080, + } + ) + assert cfg.output_width == 1920 + assert cfg.output_height == 1080 + + def test_output_dimensions_negative_clamped(self): + """负的尺寸钳制到0.""" + cfg = ConcatConfig.from_config_dict( + { + "output_width": -100, + "output_height": -50, + } + ) + assert cfg.output_width == 0 + assert cfg.output_height == 0 + + def test_output_dimensions_invalid(self): + """无效尺寸回退到0.""" + cfg = ConcatConfig.from_config_dict( + { + "output_width": "abc", + "output_height": "xyz", + } + ) + assert cfg.output_width == 0 + assert cfg.output_height == 0 + + def test_output_fps(self): + """输出帧率.""" + cfg = ConcatConfig.from_config_dict({"output_fps": 30.0}) + assert cfg.output_fps == 30.0 + + def test_output_fps_negative_clamped(self): + """负帧率钳制到0.""" + cfg = ConcatConfig.from_config_dict({"output_fps": -5}) + assert cfg.output_fps == 0.0 + + def test_force_reencode(self): + """强制重新编码.""" + cfg = ConcatConfig.from_config_dict({"force_reencode": True}) + assert cfg.force_reencode is True + + def test_force_reencode_default_false(self): + """默认不强制重编码.""" + cfg = ConcatConfig.from_config_dict({}) + assert cfg.force_reencode is False + + def test_transition(self): + """转场效果.""" + cfg = ConcatConfig.from_config_dict({"transition": "crossfade"}) + assert cfg.transition == "crossfade" + + def test_transition_default_none(self): + """默认转场none.""" + cfg = ConcatConfig.from_config_dict({}) + assert cfg.transition == "none" + + def test_transition_duration(self): + """转场时长.""" + cfg = ConcatConfig.from_config_dict({"transition_duration": 1.0}) + assert cfg.transition_duration == 1.0 + + def test_transition_duration_min(self): + """转场时长最小值0.1.""" + cfg = ConcatConfig.from_config_dict({"transition_duration": 0.01}) + assert cfg.transition_duration == 0.1 + + def test_transition_duration_invalid(self): + """无效转场时长回退到默认.""" + cfg = ConcatConfig.from_config_dict({"transition_duration": "invalid"}) + assert cfg.transition_duration == 0.3 + + +# ── clamp_segments 测试 ────────────────────────────────────────────────────── + + +class TestClampSegments: + """clamp_segments 截断测试.""" + + def test_under_limit_no_change(self): + """低于上限时不截断.""" + segs = [ConcatSegment(video_path=f"v{i}.mp4") for i in range(10)] cfg = ConcatConfig(segments=segs) - cfg.clamp_segments(50) + cfg.clamp_segments(max_segments=50) assert len(cfg.segments) == 10 - def test_default_max_constant(self): - assert MAX_CONCAT_SEGMENTS == 50 + def test_over_limit_truncated(self): + """超过上限时截断.""" + segs = [ConcatSegment(video_path=f"v{i}.mp4") for i in range(100)] + cfg = ConcatConfig(segments=segs) + cfg.clamp_segments(max_segments=30) + assert len(cfg.segments) == 30 + assert cfg.segments[0].video_path == "v0.mp4" + assert cfg.segments[-1].video_path == "v29.mp4" + def test_default_max_uses_constant(self): + """默认max_segments使用常量.""" + segs = [ConcatSegment(video_path=f"v{i}.mp4") for i in range(100)] + cfg = ConcatConfig(segments=segs) + cfg.clamp_segments() # 默认MAX_CONCAT_SEGMENTS + assert len(cfg.segments) == MAX_CONCAT_SEGMENTS -class TestConstants: - def test_allowed_extensions(self): - assert ".mp4" in ALLOWED_VIDEO_EXTENSIONS - assert ".mov" in ALLOWED_VIDEO_EXTENSIONS - assert ".webm" in ALLOWED_VIDEO_EXTENSIONS + def test_empty_segments(self): + """空列表不报错.""" + cfg = ConcatConfig() + cfg.clamp_segments() + assert cfg.segments == [] - def test_demuxer_params(self): - assert "codec_name" in CONCAT_DEMUXER_REQUIRED_PARAMS - assert "width" in CONCAT_DEMUXER_REQUIRED_PARAMS - assert "r_frame_rate" in CONCAT_DEMUXER_REQUIRED_PARAMS + def test_at_limit_stays(self): + """刚好在上限时不变.""" + segs = [ConcatSegment(video_path=f"v{i}.mp4") for i in range(50)] + cfg = ConcatConfig(segments=segs) + cfg.clamp_segments(max_segments=50) + assert len(cfg.segments) == 50