"""模板编辑器工具函数测试 — _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)