7a72cfd709
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m9s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m19s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m7s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m7s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m17s
CI/CD Pipeline / Frontend Lint (push) Successful in 3m37s
CI/CD Pipeline / Integration Tests (push) Successful in 2m18s
CI/CD Pipeline / Unit Tests (push) Failing after 4m59s
CI/CD Pipeline / Build Staging API Image (push) Successful in 8m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m25s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 27m2s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 30m15s
243 lines
7.1 KiB
Python
Executable File
243 lines
7.1 KiB
Python
Executable File
"""模板编辑器工具函数测试 — _utils.py 纯函数."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
import pytest
|
|
from app.api.routes.templates_editor._utils import (
|
|
_clip_type_to_scene_label,
|
|
_clip_value,
|
|
_format_time,
|
|
_get_adjust_trim,
|
|
_get_adjust_volume,
|
|
_get_clip_config,
|
|
_validate_trim,
|
|
)
|
|
|
|
|
|
class TestFormatTime:
|
|
"""_format_time 秒数格式化测试."""
|
|
|
|
def test_zero(self):
|
|
"""0秒."""
|
|
assert _format_time(0) == "0:00"
|
|
|
|
def test_less_than_minute(self):
|
|
"""小于1分钟."""
|
|
assert _format_time(30) == "0:30"
|
|
assert _format_time(5) == "0:05"
|
|
assert _format_time(59) == "0:59"
|
|
|
|
def test_exact_minute(self):
|
|
"""整分钟."""
|
|
assert _format_time(60) == "1:00"
|
|
assert _format_time(120) == "2:00"
|
|
|
|
def test_minutes_and_seconds(self):
|
|
"""几分几秒."""
|
|
assert _format_time(65) == "1:05"
|
|
assert _format_time(125) == "2:05"
|
|
assert _format_time(600) == "10:00"
|
|
|
|
def test_float_seconds_truncated(self):
|
|
"""浮点秒数取整."""
|
|
assert _format_time(65.9) == "1:05"
|
|
assert _format_time(65.1) == "1:05"
|
|
|
|
|
|
class TestClipTypeToSceneLabel:
|
|
"""_clip_type_to_scene_label 片段类型转标签测试."""
|
|
|
|
def test_intro(self):
|
|
assert _clip_type_to_scene_label("intro", "") == "开场"
|
|
|
|
def test_title(self):
|
|
assert _clip_type_to_scene_label("title", "") == "标题"
|
|
|
|
def test_product(self):
|
|
assert _clip_type_to_scene_label("product", "") == "产品展示"
|
|
|
|
def test_showcase(self):
|
|
assert _clip_type_to_scene_label("showcase", "") == "场景展示"
|
|
|
|
def test_scene(self):
|
|
assert _clip_type_to_scene_label("scene", "") == "场景"
|
|
|
|
def test_subtitle(self):
|
|
assert _clip_type_to_scene_label("subtitle", "") == "字幕"
|
|
|
|
def test_text(self):
|
|
assert _clip_type_to_scene_label("text", "") == "文字"
|
|
|
|
def test_cta(self):
|
|
assert _clip_type_to_scene_label("cta", "") == "结尾 CTA"
|
|
|
|
def test_outro(self):
|
|
assert _clip_type_to_scene_label("outro", "") == "结尾"
|
|
|
|
def test_voiceover(self):
|
|
assert _clip_type_to_scene_label("voiceover", "") == "配音"
|
|
|
|
def test_transition(self):
|
|
assert _clip_type_to_scene_label("transition", "") == "转场"
|
|
|
|
def test_unknown_type_returns_itself(self):
|
|
"""未知类型返回类型名本身."""
|
|
assert _clip_type_to_scene_label("unknown_type", "") == "unknown_type"
|
|
|
|
def test_empty_type_fallback(self):
|
|
"""空类型fallback到片段."""
|
|
assert _clip_type_to_scene_label("", "") == "片段"
|
|
|
|
def test_with_text_content(self):
|
|
"""带文本内容时追加文本预览."""
|
|
result = _clip_type_to_scene_label("subtitle", "大家好今天")
|
|
assert "字幕 - 大家好今天" == result
|
|
|
|
def test_text_truncated_at_20_chars(self):
|
|
"""文本超过20字符截断."""
|
|
long_text = "一二三四五六七八九十一二三四五六七八九十"
|
|
result = _clip_type_to_scene_label("text", long_text + "extra")
|
|
# 前20个字符
|
|
assert long_text in result
|
|
assert "extra" not in result
|
|
|
|
def test_text_with_only_whitespace(self):
|
|
"""文本只有空白时不追加."""
|
|
result = _clip_type_to_scene_label("intro", " ")
|
|
assert result == "开场"
|
|
|
|
|
|
class TestValidateTrim:
|
|
"""_validate_trim 裁剪校验测试."""
|
|
|
|
def test_valid_trim(self):
|
|
"""合法裁剪."""
|
|
_validate_trim(1.0, 1.0, 5.0) # 不抛异常
|
|
|
|
def test_zero_trim(self):
|
|
"""不裁剪也合法."""
|
|
_validate_trim(0.0, 0.0, 5.0)
|
|
|
|
def test_trim_equals_total_raises(self):
|
|
"""裁剪总时长等于总时长→抛异常."""
|
|
with pytest.raises(ValueError, match="不能大于等于"):
|
|
_validate_trim(2.5, 2.5, 5.0)
|
|
|
|
def test_trim_exceeds_total_raises(self):
|
|
"""裁剪超过总时长→抛异常."""
|
|
with pytest.raises(ValueError):
|
|
_validate_trim(3.0, 3.0, 5.0)
|
|
|
|
def test_only_start_exceeds(self):
|
|
"""只有start就超过."""
|
|
with pytest.raises(ValueError):
|
|
_validate_trim(6.0, 0.0, 5.0)
|
|
|
|
def test_only_end_exceeds(self):
|
|
"""只有end就超过."""
|
|
with pytest.raises(ValueError):
|
|
_validate_trim(0.0, 6.0, 5.0)
|
|
|
|
|
|
class TestClipValue:
|
|
"""_clip_value 枚举/字符串值提取测试."""
|
|
|
|
def test_plain_string(self):
|
|
"""普通字符串返回自身."""
|
|
assert _clip_value("hello") == "hello"
|
|
|
|
def test_enum_value(self):
|
|
"""带value属性的对象返回value."""
|
|
|
|
class FakeEnum:
|
|
value = "enum_value"
|
|
|
|
assert _clip_value(FakeEnum()) == "enum_value"
|
|
|
|
def test_int_value(self):
|
|
"""整数转字符串."""
|
|
assert _clip_value(42) == "42"
|
|
|
|
|
|
@dataclass
|
|
class FakeClip:
|
|
"""测试用假Clip对象."""
|
|
|
|
id: str = "clip_1"
|
|
playback_speed: float = 1.0
|
|
duration: float = 10.0
|
|
config: dict | None = None
|
|
|
|
|
|
class TestGetClipConfig:
|
|
"""_get_clip_config 安全获取配置测试."""
|
|
|
|
def test_normal_config(self):
|
|
"""正常dict配置."""
|
|
clip = FakeClip(config={"volume": 0.5})
|
|
assert _get_clip_config(clip) == {"volume": 0.5}
|
|
|
|
def test_none_config(self):
|
|
"""config为None→返回空dict."""
|
|
clip = FakeClip(config=None)
|
|
assert _get_clip_config(clip) == {}
|
|
|
|
def test_non_dict_config(self):
|
|
"""config不是dict→返回空dict."""
|
|
clip = FakeClip(config="not_a_dict")
|
|
assert _get_clip_config(clip) == {}
|
|
|
|
def test_no_config_attribute(self):
|
|
"""没有config属性→返回空dict."""
|
|
|
|
class NoConfig:
|
|
pass
|
|
|
|
assert _get_clip_config(NoConfig()) == {}
|
|
|
|
|
|
class TestGetAdjustVolume:
|
|
"""_get_adjust_volume 获取音量测试."""
|
|
|
|
def test_default_volume(self):
|
|
"""无配置默认1.0."""
|
|
clip = FakeClip(config={})
|
|
assert _get_adjust_volume(clip) == pytest.approx(1.0)
|
|
|
|
def test_custom_volume(self):
|
|
"""自定义音量."""
|
|
clip = FakeClip(config={"volume": 0.7})
|
|
assert _get_adjust_volume(clip) == pytest.approx(0.7)
|
|
|
|
def test_none_config(self):
|
|
"""None config."""
|
|
clip = FakeClip(config=None)
|
|
assert _get_adjust_volume(clip) == pytest.approx(1.0)
|
|
|
|
|
|
class TestGetAdjustTrim:
|
|
"""_get_adjust_trim 获取裁剪测试."""
|
|
|
|
def test_default_trim(self):
|
|
"""无配置默认都是0."""
|
|
clip = FakeClip(config={})
|
|
start, end = _get_adjust_trim(clip)
|
|
assert start == pytest.approx(0.0)
|
|
assert end == pytest.approx(0.0)
|
|
|
|
def test_custom_trim(self):
|
|
"""自定义裁剪."""
|
|
clip = FakeClip(config={"trim_start": 1.5, "trim_end": 2.0})
|
|
start, end = _get_adjust_trim(clip)
|
|
assert start == pytest.approx(1.5)
|
|
assert end == pytest.approx(2.0)
|
|
|
|
def test_none_config(self):
|
|
"""None config."""
|
|
clip = FakeClip(config=None)
|
|
start, end = _get_adjust_trim(clip)
|
|
assert start == pytest.approx(0.0)
|
|
assert end == pytest.approx(0.0)
|