Files
xiaoxia-saas/tests/unit/test_trim_engine.py
T
CI Bot 0bdc3924ec
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 31s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m13s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m42s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m12s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 37s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 22s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 24s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 36s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m17s
AI Code Review / AI Code Review (pull_request) Successful in 9m0s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m39s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m14s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m12s
test(unit): 第67波 - trim引擎 + concat引擎 + subtitle_render引擎配置解析
- TrimConfig: from_dict解析 + validate_and_resolve三选二推导 + 边界钳制 + is_valid/is_noop/trim_from_start属性
- TrimSegment: from_dict解析 + segment_id默认值 + order字段
- ConcatConfig: from_config_dict多段解析 + has_effect/total_segments属性
- ConcatSegment: 构造 + duration计算
- SubtitleRender: hex转ASS BGR颜色 + opacity转ASS alpha + 文本转义 + 时间格式化 + 自动换行 + SubtitleStyle配置
+112 tests 全绿
2026-07-25 00:38:41 +08:00

272 lines
9.2 KiB
Python
Executable File
Raw Blame History

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