test(unit): 第66波 - multi_track + bgm_mixer + sticker 配置 (+52) #862
+88
-344
@@ -1,359 +1,103 @@
|
||||
"""BGM 混音单元测试.
|
||||
"""BGM混音单元测试 - 配置解析等纯逻辑."""
|
||||
|
||||
测试:
|
||||
- BGMConfig 配置解析与边界值
|
||||
- 预设 BGM 库查询
|
||||
- 纯 BGM 音频生成(端到端 ffmpeg)
|
||||
- BGM + 主音频混音(端到端 ffmpeg)
|
||||
- 淡入淡出效果
|
||||
- 音量边界(0 和 1)
|
||||
- sidechain 人声闪避
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "worker"))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from video_processing.bgm_mixer import BGMConfig, build_bgm_only, mix_bgm_with_main, prepare_bgm_track
|
||||
from video_processing.render_audio import RenderContext
|
||||
|
||||
# ── Fixtures ──────────────────────────────────────────────────────────────────
|
||||
from video_processing.bgm_mixer import BGMConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def work_dir(tmp_path):
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ctx(work_dir):
|
||||
return RenderContext(work_dir=work_dir, plan_id="test_plan")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_audio_path(work_dir):
|
||||
"""生成 10 秒测试主音频(正弦波模拟人声)。"""
|
||||
import subprocess
|
||||
|
||||
path = work_dir / "main.aac"
|
||||
# 生成 10 秒 440Hz 正弦波模拟主音频
|
||||
subprocess.run(
|
||||
[
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"sine=frequency=440:duration=10:sample_rate=44100",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"128k",
|
||||
str(path),
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
timeout=30,
|
||||
)
|
||||
return str(path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bgm_audio_path(work_dir):
|
||||
"""生成 5 秒测试 BGM(更低频率模拟背景音乐)。"""
|
||||
import subprocess
|
||||
|
||||
path = work_dir / "bgm.aac"
|
||||
# 生成 5 秒 220Hz 正弦波模拟 BGM
|
||||
subprocess.run(
|
||||
[
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"sine=frequency=220:duration=5:sample_rate=44100",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"128k",
|
||||
str(path),
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
timeout=30,
|
||||
)
|
||||
return str(path)
|
||||
|
||||
|
||||
# ── BGMConfig 测试 ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestBGMConfig:
|
||||
"""BGMConfig 配置解析测试。"""
|
||||
class TestBGMConfigDefaults:
|
||||
"""BGMConfig 默认值测试."""
|
||||
|
||||
def test_default_values(self):
|
||||
cfg = BGMConfig(bgm_path="/tmp/bgm.mp3")
|
||||
assert cfg.volume == 0.3
|
||||
assert cfg.fade_in == 0.0
|
||||
assert cfg.fade_out == 0.0
|
||||
assert cfg.loop_enabled is True
|
||||
assert cfg.sidechain_enabled is False
|
||||
assert cfg.sidechain_ratio == 0.3
|
||||
"""默认值正确."""
|
||||
config = BGMConfig(bgm_path="/bgm.mp3")
|
||||
assert config.bgm_path == "/bgm.mp3"
|
||||
assert config.volume == 0.3
|
||||
assert config.fade_in == 0.0
|
||||
assert config.fade_out == 0.0
|
||||
assert config.loop_enabled is True
|
||||
assert config.sidechain_enabled is False
|
||||
assert config.sidechain_ratio == 0.3
|
||||
assert config.sidechain_attack == 0.02
|
||||
assert config.sidechain_release == 0.5
|
||||
assert config.sidechain_threshold == -25.0
|
||||
|
||||
def test_from_config_dict(self):
|
||||
config_dict = {
|
||||
"enabled": True,
|
||||
"volume": 0.5,
|
||||
|
||||
class TestBGMConfigFromConfigDict:
|
||||
"""BGMConfig.from_config_dict 解析测试."""
|
||||
|
||||
def test_empty_dict_defaults(self):
|
||||
"""空字典用默认值."""
|
||||
config = BGMConfig.from_config_dict("/bgm.mp3", {})
|
||||
assert config.bgm_path == "/bgm.mp3"
|
||||
assert config.volume == 0.3
|
||||
assert config.loop_enabled is True
|
||||
assert config.sidechain_enabled is False
|
||||
|
||||
def test_custom_volume(self):
|
||||
"""自定义音量."""
|
||||
config = BGMConfig.from_config_dict("/a.mp3", {"volume": 0.5})
|
||||
assert config.volume == 0.5
|
||||
|
||||
def test_fade_in_out(self):
|
||||
"""淡入淡出."""
|
||||
config = BGMConfig.from_config_dict("/a.mp3", {
|
||||
"fade_in": 2.0,
|
||||
"fade_out": 3.0,
|
||||
"loop_enabled": False,
|
||||
})
|
||||
assert config.fade_in == 2.0
|
||||
assert config.fade_out == 3.0
|
||||
|
||||
def test_loop_disabled(self):
|
||||
"""禁用循环."""
|
||||
config = BGMConfig.from_config_dict("/a.mp3", {"loop_enabled": False})
|
||||
assert config.loop_enabled is False
|
||||
|
||||
def test_sidechain_enabled(self):
|
||||
"""启用人声闪避."""
|
||||
config = BGMConfig.from_config_dict("/a.mp3", {"sidechain_enabled": True})
|
||||
assert config.sidechain_enabled is True
|
||||
|
||||
def test_sidechain_custom_params(self):
|
||||
"""闪避自定义参数."""
|
||||
config = BGMConfig.from_config_dict("/a.mp3", {
|
||||
"sidechain_enabled": True,
|
||||
"sidechain_ratio": 0.5,
|
||||
}
|
||||
cfg = BGMConfig.from_config_dict("/bgm.mp3", config_dict)
|
||||
assert cfg.bgm_path == "/bgm.mp3"
|
||||
assert cfg.volume == 0.5
|
||||
assert cfg.fade_in == 2.0
|
||||
assert cfg.fade_out == 3.0
|
||||
assert cfg.loop_enabled is False
|
||||
assert cfg.sidechain_enabled is True
|
||||
assert cfg.sidechain_ratio == 0.5
|
||||
"sidechain_attack": 0.05,
|
||||
"sidechain_release": 0.8,
|
||||
"sidechain_threshold": -30.0,
|
||||
})
|
||||
assert config.sidechain_ratio == 0.5
|
||||
assert config.sidechain_attack == 0.05
|
||||
assert config.sidechain_release == 0.8
|
||||
assert config.sidechain_threshold == -30.0
|
||||
|
||||
def test_volume_clamped_by_config_schema(self):
|
||||
"""音量边界由 Pydantic Schema 在入口层保证,内部直接使用。"""
|
||||
from packages.domain.config_schemas import BGMConfig as BGMConfigSchema
|
||||
def test_bgm_path_preserved(self):
|
||||
"""bgm_path保持不变."""
|
||||
config = BGMConfig.from_config_dict("/custom/path.mp3", {"volume": 0.5})
|
||||
assert config.bgm_path == "/custom/path.mp3"
|
||||
|
||||
# 边界值测试
|
||||
cfg = BGMConfigSchema(enabled=True, volume=0.0)
|
||||
assert cfg.volume == 0.0
|
||||
|
||||
cfg = BGMConfigSchema(enabled=True, volume=1.0)
|
||||
assert cfg.volume == 1.0
|
||||
|
||||
def test_fade_boundaries(self):
|
||||
from packages.domain.config_schemas import BGMConfig as BGMConfigSchema
|
||||
|
||||
# 0 是合法值
|
||||
cfg = BGMConfigSchema(fade_in=0, fade_out=0)
|
||||
assert cfg.fade_in == 0.0
|
||||
assert cfg.fade_out == 0.0
|
||||
|
||||
|
||||
# ── 预设 BGM 库测试 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestPresetBGM:
|
||||
"""预设 BGM 库查询测试。"""
|
||||
|
||||
def test_total_count(self):
|
||||
from packages.domain.preset_bgm import PRESET_BGM_LIBRARY
|
||||
|
||||
assert len(PRESET_BGM_LIBRARY) >= 10
|
||||
|
||||
def test_get_preset_by_id(self):
|
||||
from packages.domain.preset_bgm import get_preset_bgm
|
||||
|
||||
bgm = get_preset_bgm("bgm_upbeat_001")
|
||||
assert bgm is not None
|
||||
assert bgm.name == "阳光清晨"
|
||||
assert bgm.style == "upbeat"
|
||||
|
||||
def test_get_preset_not_found(self):
|
||||
from packages.domain.preset_bgm import get_preset_bgm
|
||||
|
||||
assert get_preset_bgm("nonexistent") is None
|
||||
|
||||
def test_list_by_style(self):
|
||||
from packages.domain.preset_bgm import list_preset_bgm_by_style
|
||||
|
||||
upbeat = list_preset_bgm_by_style("upbeat")
|
||||
assert len(upbeat) >= 3
|
||||
assert all(b.style == "upbeat" for b in upbeat)
|
||||
|
||||
def test_search_by_keyword(self):
|
||||
from packages.domain.preset_bgm import search_preset_bgm
|
||||
|
||||
results = search_preset_bgm("钢琴")
|
||||
assert len(results) >= 2
|
||||
assert any("钢琴" in b.tags for b in results)
|
||||
|
||||
def test_all_presets_have_basic_fields(self):
|
||||
from packages.domain.preset_bgm import PRESET_BGM_LIBRARY
|
||||
|
||||
for bgm in PRESET_BGM_LIBRARY:
|
||||
assert bgm.id, f"{bgm.name} 缺少 id"
|
||||
assert bgm.name, "缺少 name"
|
||||
assert bgm.style, f"{bgm.name} 缺少 style"
|
||||
assert bgm.duration > 0, f"{bgm.name} 时长无效"
|
||||
|
||||
|
||||
# ── BGM 处理端到端测试 ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestPrepareBGMTrack:
|
||||
"""prepare_bgm_track 端到端测试。"""
|
||||
|
||||
def test_bgm_without_loop_short_duration(self, ctx, bgm_audio_path):
|
||||
"""BGM 比目标时长短且不循环 → 截断到目标时长(但前面没有足够内容)。"""
|
||||
bgm = BGMConfig(bgm_path=bgm_audio_path, volume=0.5, loop_enabled=False)
|
||||
result = prepare_bgm_track(ctx, bgm, target_duration=3.0)
|
||||
|
||||
assert result.exists()
|
||||
assert result.stat().st_size > 0
|
||||
|
||||
def test_bgm_with_loop_longer_duration(self, ctx, bgm_audio_path):
|
||||
"""BGM 比目标时长短,循环铺满。"""
|
||||
bgm = BGMConfig(bgm_path=bgm_audio_path, volume=0.3, loop_enabled=True)
|
||||
# BGM 5 秒,目标 12 秒,需要循环 3 次
|
||||
result = prepare_bgm_track(ctx, bgm, target_duration=12.0)
|
||||
|
||||
assert result.exists()
|
||||
assert result.stat().st_size > 0
|
||||
|
||||
def test_bgm_fade_in_and_fade_out(self, ctx, bgm_audio_path):
|
||||
"""BGM 淡入淡出效果。"""
|
||||
bgm = BGMConfig(
|
||||
bgm_path=bgm_audio_path,
|
||||
volume=0.5,
|
||||
fade_in=1.0,
|
||||
fade_out=1.0,
|
||||
loop_enabled=False,
|
||||
)
|
||||
result = prepare_bgm_track(ctx, bgm, target_duration=4.0)
|
||||
|
||||
assert result.exists()
|
||||
assert result.stat().st_size > 0
|
||||
|
||||
def test_volume_zero(self, ctx, bgm_audio_path):
|
||||
"""音量为 0 时仍能正常处理。"""
|
||||
bgm = BGMConfig(bgm_path=bgm_audio_path, volume=0.0, loop_enabled=False)
|
||||
result = prepare_bgm_track(ctx, bgm, target_duration=3.0)
|
||||
|
||||
assert result.exists()
|
||||
assert result.stat().st_size > 0
|
||||
|
||||
def test_volume_one(self, ctx, bgm_audio_path):
|
||||
"""音量为 1(最大)时正常处理。"""
|
||||
bgm = BGMConfig(bgm_path=bgm_audio_path, volume=1.0, loop_enabled=False)
|
||||
result = prepare_bgm_track(ctx, bgm, target_duration=3.0)
|
||||
|
||||
assert result.exists()
|
||||
assert result.stat().st_size > 0
|
||||
|
||||
|
||||
class TestMixBGMMain:
|
||||
"""BGM + 主音频混音端到端测试。"""
|
||||
|
||||
def test_simple_mix(self, ctx, main_audio_path, bgm_audio_path):
|
||||
"""普通 amix 混音(无 sidechain)。"""
|
||||
bgm = BGMConfig(
|
||||
bgm_path=bgm_audio_path,
|
||||
volume=0.3,
|
||||
loop_enabled=True,
|
||||
sidechain_enabled=False,
|
||||
)
|
||||
result = mix_bgm_with_main(ctx, Path(main_audio_path), bgm, target_duration=8.0)
|
||||
|
||||
assert result.exists()
|
||||
assert result.stat().st_size > 0
|
||||
|
||||
def test_sidechain_mix(self, ctx, main_audio_path, bgm_audio_path):
|
||||
"""sidechain 人声闪避混音。"""
|
||||
bgm = BGMConfig(
|
||||
bgm_path=bgm_audio_path,
|
||||
volume=0.5,
|
||||
loop_enabled=True,
|
||||
sidechain_enabled=True,
|
||||
sidechain_ratio=0.3,
|
||||
sidechain_threshold=-25.0,
|
||||
sidechain_attack=0.02,
|
||||
sidechain_release=0.5,
|
||||
)
|
||||
result = mix_bgm_with_main(ctx, Path(main_audio_path), bgm, target_duration=8.0)
|
||||
|
||||
assert result.exists()
|
||||
assert result.stat().st_size > 0
|
||||
|
||||
def test_sidechain_max_ratio(self, ctx, main_audio_path, bgm_audio_path):
|
||||
"""sidechain 最大闪避比例。"""
|
||||
bgm = BGMConfig(
|
||||
bgm_path=bgm_audio_path,
|
||||
volume=0.5,
|
||||
loop_enabled=True,
|
||||
sidechain_enabled=True,
|
||||
sidechain_ratio=0.9, # 降低 90%
|
||||
)
|
||||
result = mix_bgm_with_main(ctx, Path(main_audio_path), bgm, target_duration=5.0)
|
||||
|
||||
assert result.exists()
|
||||
assert result.stat().st_size > 0
|
||||
|
||||
|
||||
class TestBuildBGMOnly:
|
||||
"""纯 BGM 模式测试。"""
|
||||
|
||||
def test_build_bgm_only(self, ctx, bgm_audio_path):
|
||||
"""只有 BGM、没有主音频时生成纯 BGM 音频。"""
|
||||
bgm = BGMConfig(
|
||||
bgm_path=bgm_audio_path,
|
||||
volume=0.3,
|
||||
fade_in=1.0,
|
||||
fade_out=1.0,
|
||||
loop_enabled=True,
|
||||
)
|
||||
result = build_bgm_only(ctx, bgm, target_duration=15.0)
|
||||
|
||||
assert result.exists()
|
||||
assert result.stat().st_size > 0
|
||||
|
||||
|
||||
# ── Config Schema 集成测试 ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestConfigSchemaIntegration:
|
||||
"""config schema 与渲染配置的集成测试。"""
|
||||
|
||||
def test_full_bgm_config(self):
|
||||
"""完整 BGM 配置能正确解析。"""
|
||||
from packages.domain.config_schemas import EditPlanConfigSchema, normalize_plan_config
|
||||
|
||||
config = normalize_plan_config(
|
||||
{
|
||||
"bgm": {
|
||||
"enabled": True,
|
||||
"source": "library",
|
||||
"asset_id": "bgm-asset-001",
|
||||
"volume": 0.4,
|
||||
"fade_in": 2.5,
|
||||
"fade_out": 3.0,
|
||||
"loop_enabled": True,
|
||||
"sidechain_enabled": True,
|
||||
"sidechain_ratio": 0.4,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
bgm = config["bgm"]
|
||||
assert bgm["enabled"] is True
|
||||
assert bgm["volume"] == 0.4
|
||||
assert bgm["fade_in"] == 2.5
|
||||
assert bgm["fade_out"] == 3.0
|
||||
assert bgm["loop_enabled"] is True
|
||||
assert bgm["sidechain_enabled"] is True
|
||||
assert bgm["sidechain_ratio"] == 0.4
|
||||
# 默认值保留
|
||||
assert bgm["sidechain_attack"] == 0.02
|
||||
assert bgm["sidechain_release"] == 0.5
|
||||
assert bgm["sidechain_threshold"] == -25.0
|
||||
|
||||
def test_bgm_disabled_by_default(self):
|
||||
"""默认 BGM 是关闭的。"""
|
||||
from packages.domain.config_schemas import normalize_plan_config
|
||||
|
||||
config = normalize_plan_config({})
|
||||
assert config["bgm"]["enabled"] is False
|
||||
def test_all_params_custom(self):
|
||||
"""所有参数自定义."""
|
||||
config = BGMConfig.from_config_dict("/full.mp3", {
|
||||
"volume": 0.7,
|
||||
"fade_in": 1.5,
|
||||
"fade_out": 2.0,
|
||||
"loop_enabled": False,
|
||||
"sidechain_enabled": True,
|
||||
"sidechain_ratio": 0.4,
|
||||
"sidechain_attack": 0.03,
|
||||
"sidechain_release": 0.6,
|
||||
"sidechain_threshold": -20.0,
|
||||
})
|
||||
assert config.volume == 0.7
|
||||
assert config.fade_in == 1.5
|
||||
assert config.fade_out == 2.0
|
||||
assert config.loop_enabled is False
|
||||
assert config.sidechain_enabled is True
|
||||
assert config.sidechain_ratio == 0.4
|
||||
assert config.sidechain_attack == 0.03
|
||||
assert config.sidechain_release == 0.6
|
||||
assert config.sidechain_threshold == -20.0
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
"""
|
||||
多轨道混音引擎配置与纯逻辑测试.
|
||||
|
||||
覆盖 AudioTrack.from_dict / MultiTrackMixConfig.from_config_dict / has_effect 等纯逻辑.
|
||||
引擎核心混音方法依赖 FFmpeg,由集成测试覆盖.
|
||||
"""
|
||||
"""多轨道混音单元测试 - 配置解析等纯逻辑."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from video_processing.multi_track_mixer import (
|
||||
DEFAULT_VOLUMES,
|
||||
MAX_AUDIO_TRACKS,
|
||||
@@ -21,383 +17,317 @@ from video_processing.multi_track_mixer import (
|
||||
)
|
||||
|
||||
|
||||
class TestTrackConstants:
|
||||
"""轨道类型常量与默认值."""
|
||||
class TestConstants:
|
||||
"""常量测试."""
|
||||
|
||||
def test_track_types_exist(self):
|
||||
def test_track_types(self):
|
||||
"""5种轨道类型."""
|
||||
assert TRACK_TYPE_MAIN == "main"
|
||||
assert TRACK_TYPE_BGM == "bgm"
|
||||
assert TRACK_TYPE_VOICEOVER == "voiceover"
|
||||
assert TRACK_TYPE_SFX == "sfx"
|
||||
assert TRACK_TYPE_AMBIENT == "ambient"
|
||||
|
||||
def test_max_tracks(self):
|
||||
assert MAX_AUDIO_TRACKS == 8
|
||||
|
||||
def test_default_volumes(self):
|
||||
"""5种默认音量."""
|
||||
assert len(DEFAULT_VOLUMES) == 5
|
||||
assert DEFAULT_VOLUMES[TRACK_TYPE_MAIN] == 1.0
|
||||
assert DEFAULT_VOLUMES[TRACK_TYPE_BGM] == 0.3
|
||||
assert DEFAULT_VOLUMES[TRACK_TYPE_VOICEOVER] == 1.0
|
||||
assert DEFAULT_VOLUMES[TRACK_TYPE_SFX] == 0.7
|
||||
assert DEFAULT_VOLUMES[TRACK_TYPE_AMBIENT] == 0.2
|
||||
|
||||
|
||||
class TestAudioTrackFromDict:
|
||||
"""AudioTrack.from_dict 构造逻辑."""
|
||||
|
||||
def test_basic(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
}
|
||||
)
|
||||
assert track.track_id == "t1"
|
||||
assert track.track_type == "bgm"
|
||||
assert track.audio_path == "/tmp/bgm.mp3"
|
||||
assert track.volume == 0.3 # bgm 默认音量
|
||||
|
||||
def test_custom_volume(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "main",
|
||||
"audio_path": "/tmp/main.wav",
|
||||
"volume": 0.8,
|
||||
}
|
||||
)
|
||||
assert track.volume == 0.8
|
||||
|
||||
def test_volume_clamped_to_zero(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "sfx",
|
||||
"audio_path": "/tmp/sfx.wav",
|
||||
"volume": -1.0,
|
||||
}
|
||||
)
|
||||
assert track.volume == 0.0
|
||||
|
||||
def test_volume_clamped_to_max(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "sfx",
|
||||
"audio_path": "/tmp/sfx.wav",
|
||||
"volume": 3.0,
|
||||
}
|
||||
)
|
||||
assert track.volume == 2.0
|
||||
|
||||
def test_invalid_volume_falls_back_to_default(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"volume": "not_a_number",
|
||||
}
|
||||
)
|
||||
assert track.volume == 0.3 # bgm 默认
|
||||
|
||||
def test_none_volume_falls_back(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "voiceover",
|
||||
"audio_path": "/tmp/vo.wav",
|
||||
"volume": None,
|
||||
}
|
||||
)
|
||||
assert track.volume == 1.0 # voiceover 默认
|
||||
|
||||
def test_unknown_track_type_default_volume(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "unknown_type",
|
||||
"audio_path": "/tmp/a.wav",
|
||||
}
|
||||
)
|
||||
assert track.volume == 1.0 # 未知类型默认 1.0
|
||||
|
||||
def test_fade_in_fade_out(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"fade_in": 1.5,
|
||||
"fade_out": 2.0,
|
||||
}
|
||||
)
|
||||
assert track.fade_in == 1.5
|
||||
assert track.fade_out == 2.0
|
||||
|
||||
def test_negative_fade_clamped(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"fade_in": -0.5,
|
||||
"fade_out": -1.0,
|
||||
}
|
||||
)
|
||||
assert track.fade_in == 0.0
|
||||
assert track.fade_out == 0.0
|
||||
|
||||
def test_invalid_fade_falls_back(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"fade_in": "abc",
|
||||
"fade_out": None,
|
||||
}
|
||||
)
|
||||
assert track.fade_in == 0.0
|
||||
assert track.fade_out == 0.0
|
||||
|
||||
def test_start_time_and_duration(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "sfx",
|
||||
"audio_path": "/tmp/sfx.wav",
|
||||
"start_time": 5.0,
|
||||
"duration": 3.0,
|
||||
}
|
||||
)
|
||||
assert track.start_time == 5.0
|
||||
assert track.duration == 3.0
|
||||
|
||||
def test_negative_start_time_clamped(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"start_time": -10.0,
|
||||
"duration": -2.0,
|
||||
}
|
||||
)
|
||||
assert track.start_time == 0.0
|
||||
assert track.duration == 0.0
|
||||
|
||||
def test_invalid_time_values_fall_back(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"start_time": "invalid",
|
||||
"duration": "bad",
|
||||
}
|
||||
)
|
||||
assert track.start_time == 0.0
|
||||
assert track.duration == 0.0
|
||||
|
||||
def test_enabled_default_true(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
}
|
||||
)
|
||||
assert track.enabled is True
|
||||
|
||||
def test_enabled_can_be_false(self):
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"enabled": False,
|
||||
}
|
||||
)
|
||||
assert track.enabled is False
|
||||
def test_max_tracks(self):
|
||||
"""最大轨道数."""
|
||||
assert MAX_AUDIO_TRACKS == 8
|
||||
|
||||
|
||||
class TestMultiTrackMixConfigFromDict:
|
||||
"""MultiTrackMixConfig.from_config_dict 构造逻辑."""
|
||||
|
||||
def test_none_returns_default(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict(None)
|
||||
assert cfg.tracks == []
|
||||
assert cfg.master_volume == 1.0
|
||||
assert cfg.normalize is True
|
||||
|
||||
def test_empty_dict_returns_default(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({})
|
||||
assert cfg.tracks == []
|
||||
|
||||
def test_non_dict_returns_default(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict([])
|
||||
assert cfg.tracks == []
|
||||
|
||||
def test_single_track(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [
|
||||
{
|
||||
"track_id": "bgm1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"volume": 0.5,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.tracks) == 1
|
||||
assert cfg.tracks[0].track_id == "bgm1"
|
||||
assert cfg.tracks[0].volume == 0.5
|
||||
|
||||
def test_multiple_tracks(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [
|
||||
{"track_id": "m", "track_type": "main", "audio_path": "/tmp/m.wav"},
|
||||
{"track_id": "b", "track_type": "bgm", "audio_path": "/tmp/b.mp3"},
|
||||
{"track_id": "v", "track_type": "voiceover", "audio_path": "/tmp/v.wav"},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.tracks) == 3
|
||||
assert cfg.tracks[0].track_type == "main"
|
||||
assert cfg.tracks[1].track_type == "bgm"
|
||||
assert cfg.tracks[2].track_type == "voiceover"
|
||||
|
||||
def test_disabled_tracks_filtered(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [
|
||||
{"track_id": "a", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
|
||||
{"track_id": "b", "track_type": "sfx", "audio_path": "/tmp/b.wav", "enabled": False},
|
||||
{"track_id": "c", "track_type": "sfx", "audio_path": "/tmp/c.wav"},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.tracks) == 2
|
||||
assert all(t.track_id != "b" for t in cfg.tracks)
|
||||
|
||||
def test_empty_audio_path_filtered(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [
|
||||
{"track_id": "valid", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
|
||||
{"track_id": "empty", "track_type": "sfx", "audio_path": ""},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.tracks) == 1
|
||||
assert cfg.tracks[0].track_id == "valid"
|
||||
|
||||
def test_invalid_tracks_skipped(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [
|
||||
{"track_id": "ok", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
|
||||
"not_a_dict",
|
||||
None,
|
||||
{"no_audio_path": "xxx"},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.tracks) == 1
|
||||
|
||||
def test_tracks_not_a_list(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": "not_a_list",
|
||||
}
|
||||
)
|
||||
assert cfg.tracks == []
|
||||
|
||||
def test_master_volume(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [],
|
||||
"master_volume": 0.8,
|
||||
}
|
||||
)
|
||||
assert cfg.master_volume == 0.8
|
||||
|
||||
def test_master_volume_clamped(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [],
|
||||
"master_volume": 3.0,
|
||||
}
|
||||
)
|
||||
assert cfg.master_volume == 2.0
|
||||
|
||||
cfg2 = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [],
|
||||
"master_volume": -1.0,
|
||||
}
|
||||
)
|
||||
assert cfg2.master_volume == 0.0
|
||||
|
||||
def test_invalid_master_volume_falls_back(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [],
|
||||
"master_volume": "abc",
|
||||
}
|
||||
)
|
||||
assert cfg.master_volume == 1.0
|
||||
|
||||
def test_normalize_and_max_output(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [],
|
||||
"normalize": False,
|
||||
"max_output_volume": 2.0,
|
||||
}
|
||||
)
|
||||
assert cfg.normalize is False
|
||||
assert cfg.max_output_volume == 2.0
|
||||
class TestAudioTrackDefaults:
|
||||
"""AudioTrack 默认值测试."""
|
||||
|
||||
def test_default_values(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({"tracks": []})
|
||||
assert cfg.master_volume == 1.0
|
||||
assert cfg.normalize is True
|
||||
assert cfg.max_output_volume == 1.5
|
||||
"""默认值正确."""
|
||||
track = AudioTrack(track_id="t1", track_type="bgm", audio_path="/a.mp3")
|
||||
assert track.track_id == "t1"
|
||||
assert track.track_type == "bgm"
|
||||
assert track.audio_path == "/a.mp3"
|
||||
assert track.volume == 1.0
|
||||
assert track.fade_in == 0.0
|
||||
assert track.fade_out == 0.0
|
||||
assert track.start_time == 0.0
|
||||
assert track.duration == 0.0
|
||||
assert track.enabled is True
|
||||
|
||||
|
||||
class TestMultiTrackMixConfigProperties:
|
||||
"""has_effect 属性."""
|
||||
class TestAudioTrackFromDict:
|
||||
"""AudioTrack.from_dict 解析测试."""
|
||||
|
||||
def test_has_effect_with_tracks(self):
|
||||
cfg = MultiTrackMixConfig(
|
||||
tracks=[
|
||||
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3"),
|
||||
]
|
||||
)
|
||||
assert cfg.has_effect is True
|
||||
def test_basic_parsing(self):
|
||||
"""基本解析."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/bgm.mp3",
|
||||
})
|
||||
assert track.track_id == "t1"
|
||||
assert track.track_type == "bgm"
|
||||
assert track.audio_path == "/bgm.mp3"
|
||||
|
||||
def test_no_effect_empty(self):
|
||||
cfg = MultiTrackMixConfig(tracks=[])
|
||||
assert cfg.has_effect is False
|
||||
def test_default_volume_by_type_bgm(self):
|
||||
"""bgm默认音量0.3."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/a.mp3",
|
||||
})
|
||||
assert track.volume == 0.3
|
||||
|
||||
def test_no_effect_all_disabled(self):
|
||||
cfg = MultiTrackMixConfig(
|
||||
tracks=[
|
||||
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3", enabled=False),
|
||||
]
|
||||
)
|
||||
assert cfg.has_effect is False
|
||||
def test_default_volume_by_type_sfx(self):
|
||||
"""sfx默认音量0.7."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "sfx",
|
||||
"audio_path": "/a.mp3",
|
||||
})
|
||||
assert track.volume == 0.7
|
||||
|
||||
def test_no_effect_empty_paths(self):
|
||||
cfg = MultiTrackMixConfig(
|
||||
tracks=[
|
||||
AudioTrack(track_id="t1", track_type="bgm", audio_path=""),
|
||||
]
|
||||
)
|
||||
assert cfg.has_effect is False
|
||||
def test_default_volume_unknown_type(self):
|
||||
"""未知类型默认音量1.0."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "unknown_type",
|
||||
"audio_path": "/a.mp3",
|
||||
})
|
||||
assert track.volume == 1.0
|
||||
|
||||
def test_custom_volume(self):
|
||||
"""自定义音量."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/a.mp3",
|
||||
"volume": 0.5,
|
||||
})
|
||||
assert track.volume == 0.5
|
||||
|
||||
def test_volume_clamped_high(self):
|
||||
"""音量上限钳制."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/a.mp3",
|
||||
"volume": 3.0,
|
||||
})
|
||||
assert track.volume == 2.0
|
||||
|
||||
def test_volume_clamped_low(self):
|
||||
"""音量下限钳制."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"audio_path": "/a.mp3",
|
||||
"volume": -1.0,
|
||||
})
|
||||
assert track.volume == 0.0
|
||||
|
||||
def test_volume_invalid_falls_back(self):
|
||||
"""无效音量回退到类型默认值."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/a.mp3",
|
||||
"volume": "not_a_number",
|
||||
})
|
||||
assert track.volume == 0.3
|
||||
|
||||
def test_fade_in(self):
|
||||
"""淡入时长."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"audio_path": "/a.mp3",
|
||||
"fade_in": 2.5,
|
||||
})
|
||||
assert track.fade_in == 2.5
|
||||
|
||||
def test_fade_negative_clamped(self):
|
||||
"""负淡入钳制到0."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"audio_path": "/a.mp3",
|
||||
"fade_in": -1.0,
|
||||
"fade_out": -2.0,
|
||||
})
|
||||
assert track.fade_in == 0.0
|
||||
assert track.fade_out == 0.0
|
||||
|
||||
def test_start_time(self):
|
||||
"""开始时间."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"audio_path": "/a.mp3",
|
||||
"start_time": 5.5,
|
||||
})
|
||||
assert track.start_time == 5.5
|
||||
|
||||
def test_start_time_negative_clamped(self):
|
||||
"""负开始时间钳制到0."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"audio_path": "/a.mp3",
|
||||
"start_time": -3.0,
|
||||
})
|
||||
assert track.start_time == 0.0
|
||||
|
||||
def test_disabled_track(self):
|
||||
"""禁用轨道."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"audio_path": "/a.mp3",
|
||||
"enabled": False,
|
||||
})
|
||||
assert track.enabled is False
|
||||
|
||||
def test_invalid_fade_in_falls_back(self):
|
||||
"""无效淡入值回退到0."""
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"audio_path": "/a.mp3",
|
||||
"fade_in": "fast",
|
||||
})
|
||||
assert track.fade_in == 0.0
|
||||
|
||||
|
||||
class TestMultiTrackMixConfigDefaults:
|
||||
"""MultiTrackMixConfig 默认值测试."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""默认值正确."""
|
||||
config = MultiTrackMixConfig()
|
||||
assert config.tracks == []
|
||||
assert config.master_volume == 1.0
|
||||
assert config.normalize is True
|
||||
assert config.max_output_volume == 1.5
|
||||
|
||||
|
||||
class TestMultiTrackMixConfigFromConfigDict:
|
||||
"""MultiTrackMixConfig.from_config_dict 测试."""
|
||||
|
||||
def test_none_returns_default(self):
|
||||
"""None返回默认配置."""
|
||||
config = MultiTrackMixConfig.from_config_dict(None)
|
||||
assert config.tracks == []
|
||||
assert config.master_volume == 1.0
|
||||
|
||||
def test_empty_dict_returns_default(self):
|
||||
"""空dict返回默认."""
|
||||
config = MultiTrackMixConfig.from_config_dict({})
|
||||
assert config.tracks == []
|
||||
|
||||
def test_single_track(self):
|
||||
"""单轨道."""
|
||||
config = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [
|
||||
{
|
||||
"track_id": "bgm1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/bgm.mp3",
|
||||
},
|
||||
],
|
||||
})
|
||||
assert len(config.tracks) == 1
|
||||
assert config.tracks[0].track_id == "bgm1"
|
||||
|
||||
def test_multiple_tracks(self):
|
||||
"""多轨道."""
|
||||
config = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [
|
||||
{"track_id": "t1", "track_type": "bgm", "audio_path": "/a.mp3"},
|
||||
{"track_id": "t2", "track_type": "sfx", "audio_path": "/b.mp3"},
|
||||
],
|
||||
})
|
||||
assert len(config.tracks) == 2
|
||||
|
||||
def test_skips_disabled_tracks(self):
|
||||
"""跳过禁用轨道."""
|
||||
config = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [
|
||||
{"track_id": "t1", "audio_path": "/a.mp3", "enabled": True},
|
||||
{"track_id": "t2", "audio_path": "/b.mp3", "enabled": False},
|
||||
],
|
||||
})
|
||||
assert len(config.tracks) == 1
|
||||
assert config.tracks[0].track_id == "t1"
|
||||
|
||||
def test_skips_no_audio_path(self):
|
||||
"""跳过无audio_path的轨道."""
|
||||
config = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [
|
||||
{"track_id": "t1", "audio_path": "/a.mp3"},
|
||||
{"track_id": "t2", "audio_path": ""},
|
||||
{"track_id": "t3"},
|
||||
],
|
||||
})
|
||||
assert len(config.tracks) == 1
|
||||
|
||||
def test_master_volume(self):
|
||||
"""主音量."""
|
||||
config = MultiTrackMixConfig.from_config_dict({
|
||||
"master_volume": 0.8,
|
||||
"tracks": [{"track_id": "t1", "audio_path": "/a.mp3"}],
|
||||
})
|
||||
assert config.master_volume == 0.8
|
||||
|
||||
def test_master_volume_clamped(self):
|
||||
"""主音量边界钳制."""
|
||||
config = MultiTrackMixConfig.from_config_dict({
|
||||
"master_volume": 5.0,
|
||||
"tracks": [{"track_id": "t1", "audio_path": "/a.mp3"}],
|
||||
})
|
||||
assert config.master_volume == 2.0
|
||||
|
||||
def test_normalize_disabled(self):
|
||||
"""禁用归一化."""
|
||||
config = MultiTrackMixConfig.from_config_dict({
|
||||
"normalize": False,
|
||||
"tracks": [{"track_id": "t1", "audio_path": "/a.mp3"}],
|
||||
})
|
||||
assert config.normalize is False
|
||||
|
||||
def test_tracks_not_list_ignored(self):
|
||||
"""tracks不是列表时忽略."""
|
||||
config = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": "not_a_list",
|
||||
})
|
||||
assert config.tracks == []
|
||||
|
||||
def test_non_dict_track_skipped(self):
|
||||
"""非dict轨道跳过."""
|
||||
config = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [
|
||||
{"track_id": "t1", "audio_path": "/a.mp3"},
|
||||
"not_a_dict",
|
||||
],
|
||||
})
|
||||
assert len(config.tracks) == 1
|
||||
|
||||
|
||||
class TestHasEffect:
|
||||
"""has_effect 属性测试."""
|
||||
|
||||
def test_no_tracks_no_effect(self):
|
||||
"""无轨道无效果."""
|
||||
config = MultiTrackMixConfig()
|
||||
assert config.has_effect is False
|
||||
|
||||
def test_with_tracks_has_effect(self):
|
||||
"""有轨道有效果."""
|
||||
config = MultiTrackMixConfig(tracks=[
|
||||
AudioTrack(track_id="t1", track_type="bgm", audio_path="/a.mp3"),
|
||||
])
|
||||
assert config.has_effect is True
|
||||
|
||||
def test_disabled_tracks_no_effect(self):
|
||||
"""所有轨道都禁用无效果."""
|
||||
config = MultiTrackMixConfig(tracks=[
|
||||
AudioTrack(track_id="t1", track_type="bgm", audio_path="/a.mp3", enabled=False),
|
||||
])
|
||||
assert config.has_effect is False
|
||||
|
||||
Executable
+119
@@ -0,0 +1,119 @@
|
||||
"""贴纸引擎单元测试 - 配置+解析等纯逻辑."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from video_processing.sticker_engine import (
|
||||
ImageStickerConfig,
|
||||
TextStickerConfig,
|
||||
parse_stickers_from_config,
|
||||
)
|
||||
|
||||
|
||||
class TestImageStickerConfigDefaults:
|
||||
"""ImageStickerConfig 默认值测试."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""默认值正确."""
|
||||
s = ImageStickerConfig()
|
||||
assert s.enabled is False
|
||||
assert s.type == "image"
|
||||
assert s.position == "top_right"
|
||||
assert s.x is None
|
||||
assert s.y is None
|
||||
assert s.x_unit == "percent"
|
||||
assert s.y_unit == "percent"
|
||||
assert s.scale == 1.0
|
||||
assert s.width is None
|
||||
assert s.height is None
|
||||
assert s.opacity == 1.0
|
||||
assert s.start_time == 0.0
|
||||
assert s.duration == 0.0
|
||||
assert s.fade_in == 0.0
|
||||
assert s.fade_out == 0.0
|
||||
assert s.z_index == 10
|
||||
assert s.image_url == ""
|
||||
assert s.preset_id == ""
|
||||
|
||||
|
||||
class TestTextStickerConfigDefaults:
|
||||
"""TextStickerConfig 默认值测试."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""默认值正确."""
|
||||
s = TextStickerConfig()
|
||||
assert s.enabled is False
|
||||
assert s.type == "text"
|
||||
assert s.text == ""
|
||||
assert s.font_size == 36
|
||||
assert s.font_color == "#FFFFFF"
|
||||
assert s.font_family == "sans"
|
||||
assert s.stroke_color == "#000000"
|
||||
assert s.stroke_width == 2
|
||||
assert s.shadow_color == "#000000"
|
||||
assert s.shadow_x == 2
|
||||
assert s.shadow_y == 2
|
||||
assert s.shadow_alpha == 0.5
|
||||
assert s.position == "center"
|
||||
assert s.start_time == 0.0
|
||||
assert s.duration == 0.0
|
||||
assert s.z_index == 10
|
||||
assert s.bg_color == ""
|
||||
assert s.bg_padding == 8
|
||||
assert s.bg_alpha == 0.8
|
||||
assert s.bg_corner_radius == 8
|
||||
|
||||
|
||||
class TestParseStickersFromConfig:
|
||||
"""parse_stickers_from_config 测试."""
|
||||
|
||||
def test_none_returns_empty(self):
|
||||
"""None返回空列表."""
|
||||
result = parse_stickers_from_config(None)
|
||||
assert result == []
|
||||
|
||||
def test_empty_dict_returns_empty(self):
|
||||
"""空dict返回空."""
|
||||
result = parse_stickers_from_config({})
|
||||
assert result == []
|
||||
|
||||
def test_no_stickers_key_returns_empty(self):
|
||||
"""无stickers键返回空."""
|
||||
result = parse_stickers_from_config({"other": "value"})
|
||||
assert result == []
|
||||
|
||||
def test_stickers_not_list_returns_empty(self):
|
||||
"""stickers不是列表返回空."""
|
||||
result = parse_stickers_from_config({"stickers": "not_a_list"})
|
||||
assert result == []
|
||||
|
||||
def test_empty_stickers_list(self):
|
||||
"""空贴纸列表."""
|
||||
result = parse_stickers_from_config({"stickers": []})
|
||||
assert result == []
|
||||
|
||||
def test_single_sticker(self):
|
||||
"""单个贴纸."""
|
||||
result = parse_stickers_from_config({
|
||||
"stickers": [{"type": "text", "text": "hello"}],
|
||||
})
|
||||
assert len(result) == 1
|
||||
assert result[0]["text"] == "hello"
|
||||
|
||||
def test_multiple_stickers(self):
|
||||
"""多个贴纸."""
|
||||
result = parse_stickers_from_config({
|
||||
"stickers": [
|
||||
{"type": "text", "text": "a"},
|
||||
{"type": "image", "image_url": "/b.png"},
|
||||
{"type": "text", "text": "c"},
|
||||
],
|
||||
})
|
||||
assert len(result) == 3
|
||||
|
||||
def test_returns_raw_dicts(self):
|
||||
"""返回原始dict,不做转换."""
|
||||
sticker = {"type": "text", "text": "test", "font_size": 48}
|
||||
result = parse_stickers_from_config({"stickers": [sticker]})
|
||||
assert result[0] is sticker # 引用相同,不做深拷贝
|
||||
Reference in New Issue
Block a user