Files
xiaoxia-saas/tests/unit/test_audio_track_config.py
CI Bot 9a25eb6642
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
style: auto-format with black + isort + prettier
2026-07-26 23:21:17 +00:00

489 lines
17 KiB
Python
Executable File

"""audio_track_config 模块单测 — 纯逻辑,无外部依赖."""
from __future__ import annotations
import pytest
from packages.domain.audio_track_config import (
ALLOWED_AUDIO_EXTENSIONS,
DEFAULT_VOLUMES,
MAX_AUDIO_TRACKS,
TRACK_TYPE_AMBIENT,
TRACK_TYPE_BGM,
TRACK_TYPE_MAIN,
TRACK_TYPE_SFX,
TRACK_TYPE_VOICEOVER,
AudioTrack,
MultiTrackMixConfig,
clamp_volume,
is_valid_audio_extension,
)
# ── 常量 ──────────────────────────────────────────────────────────────────────
class TestConstants:
def test_track_type_constants(self):
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_default_volumes_keys(self):
assert set(DEFAULT_VOLUMES.keys()) == {
TRACK_TYPE_MAIN,
TRACK_TYPE_BGM,
TRACK_TYPE_VOICEOVER,
TRACK_TYPE_SFX,
TRACK_TYPE_AMBIENT,
}
def test_default_volumes_values(self):
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
def test_max_audio_tracks(self):
assert MAX_AUDIO_TRACKS == 8
def test_allowed_extensions(self):
assert ".mp3" in ALLOWED_AUDIO_EXTENSIONS
assert ".wav" in ALLOWED_AUDIO_EXTENSIONS
assert ".aac" in ALLOWED_AUDIO_EXTENSIONS
assert ".ogg" in ALLOWED_AUDIO_EXTENSIONS
assert ".flac" in ALLOWED_AUDIO_EXTENSIONS
assert ".m4a" in ALLOWED_AUDIO_EXTENSIONS
assert ".wma" in ALLOWED_AUDIO_EXTENSIONS
assert ".mp4" not in ALLOWED_AUDIO_EXTENSIONS
assert ".txt" not in ALLOWED_AUDIO_EXTENSIONS
# ── AudioTrack 默认值 ────────────────────────────────────────────────────────
class TestAudioTrackDefaults:
def test_default_values(self):
track = AudioTrack()
assert track.track_id == ""
assert track.track_type == TRACK_TYPE_SFX
assert track.audio_path == ""
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
# ── AudioTrack.from_dict ─────────────────────────────────────────────────────
class TestAudioTrackFromDict:
def test_full_fields(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/a.mp3",
"volume": 0.5,
"fade_in": 1.5,
"fade_out": 2.0,
"start_time": 3.0,
"duration": 10.0,
"enabled": True,
}
)
assert track.track_id == "t1"
assert track.track_type == "bgm"
assert track.audio_path == "/tmp/a.mp3"
assert track.volume == 0.5
assert track.fade_in == 1.5
assert track.fade_out == 2.0
assert track.start_time == 3.0
assert track.duration == 10.0
assert track.enabled is True
def test_empty_dict_defaults(self):
track = AudioTrack.from_dict({})
assert track.track_type == TRACK_TYPE_SFX
assert track.volume == DEFAULT_VOLUMES[TRACK_TYPE_SFX]
assert track.fade_in == 0.0
assert track.enabled is True
def test_volume_clamped_to_zero(self):
track = AudioTrack.from_dict({"volume": -0.5})
assert track.volume == 0.0
def test_volume_clamped_to_two(self):
track = AudioTrack.from_dict({"volume": 3.0})
assert track.volume == 2.0
def test_invalid_volume_fallback_to_default(self):
track = AudioTrack.from_dict({"track_type": "bgm", "volume": "invalid"})
assert track.volume == DEFAULT_VOLUMES[TRACK_TYPE_BGM]
def test_negative_fade_clamped_to_zero(self):
track = AudioTrack.from_dict({"fade_in": -1.0, "fade_out": -2.0})
assert track.fade_in == 0.0
assert track.fade_out == 0.0
def test_invalid_fade_fallback(self):
track = AudioTrack.from_dict({"fade_in": "bad", "fade_out": "bad"})
assert track.fade_in == 0.0
assert track.fade_out == 0.0
def test_negative_start_time_clamped(self):
track = AudioTrack.from_dict({"start_time": -5.0})
assert track.start_time == 0.0
def test_invalid_start_time_fallback(self):
track = AudioTrack.from_dict({"start_time": "bad"})
assert track.start_time == 0.0
def test_negative_duration_clamped(self):
track = AudioTrack.from_dict({"duration": -3.0})
assert track.duration == 0.0
def test_invalid_duration_fallback(self):
track = AudioTrack.from_dict({"duration": "bad"})
assert track.duration == 0.0
def test_enabled_false(self):
track = AudioTrack.from_dict({"enabled": False})
assert track.enabled is False
def test_bgm_default_volume(self):
track = AudioTrack.from_dict({"track_type": "bgm"})
assert track.volume == 0.3
def test_unknown_track_type_default_volume(self):
track = AudioTrack.from_dict({"track_type": "unknown_type"})
assert track.volume == 1.0
def test_string_numeric_values(self):
track = AudioTrack.from_dict(
{
"volume": "0.8",
"fade_in": "1.0",
"start_time": "2.5",
}
)
assert track.volume == 0.8
assert track.fade_in == 1.0
assert track.start_time == 2.5
# ── AudioTrack.validate ──────────────────────────────────────────────────────
class TestAudioTrackValidate:
def test_valid_track(self):
track = AudioTrack(audio_path="/tmp/a.mp3")
ok, err = track.validate()
assert ok is True
assert err == ""
def test_empty_audio_path_invalid(self):
track = AudioTrack(audio_path="")
ok, err = track.validate()
assert ok is False
assert "audio_path" in err
def test_volume_below_zero_invalid(self):
# from_dict 会 clamp,但直接构造可以测试
track = AudioTrack(audio_path="a.mp3")
object.__setattr__(track, "volume", -0.1)
ok, err = track.validate()
assert ok is False
assert "volume" in err
def test_volume_above_two_invalid(self):
track = AudioTrack(audio_path="a.mp3")
object.__setattr__(track, "volume", 2.1)
ok, err = track.validate()
assert ok is False
assert "volume" in err
def test_volume_boundary_zero_valid(self):
track = AudioTrack(audio_path="a.mp3")
object.__setattr__(track, "volume", 0.0)
ok, _ = track.validate()
assert ok is True
def test_volume_boundary_two_valid(self):
track = AudioTrack(audio_path="a.mp3")
object.__setattr__(track, "volume", 2.0)
ok, _ = track.validate()
assert ok is True
def test_negative_fade_in_invalid(self):
track = AudioTrack(audio_path="a.mp3")
object.__setattr__(track, "fade_in", -1.0)
ok, err = track.validate()
assert ok is False
assert "fade_in" in err
def test_negative_fade_out_invalid(self):
track = AudioTrack(audio_path="a.mp3")
object.__setattr__(track, "fade_out", -1.0)
ok, err = track.validate()
assert ok is False
assert "fade_out" in err
def test_negative_start_time_invalid(self):
track = AudioTrack(audio_path="a.mp3")
object.__setattr__(track, "start_time", -0.5)
ok, err = track.validate()
assert ok is False
assert "start_time" in err
def test_negative_duration_invalid(self):
track = AudioTrack(audio_path="a.mp3")
object.__setattr__(track, "duration", -1.0)
ok, err = track.validate()
assert ok is False
assert "duration" in err
# ── AudioTrack.is_effective ──────────────────────────────────────────────────
class TestAudioTrackIsEffective:
def test_enabled_with_path(self):
track = AudioTrack(audio_path="/tmp/a.mp3", enabled=True)
assert track.is_effective is True
def test_disabled_with_path(self):
track = AudioTrack(audio_path="/tmp/a.mp3", enabled=False)
assert track.is_effective is False
def test_enabled_empty_path(self):
track = AudioTrack(audio_path="", enabled=True)
assert track.is_effective is False
def test_disabled_empty_path(self):
track = AudioTrack(audio_path="", enabled=False)
assert track.is_effective is False
# ── MultiTrackMixConfig.from_config_dict ─────────────────────────────────────
class TestMultiTrackMixConfigFromDict:
def test_none_config_empty(self):
cfg = MultiTrackMixConfig.from_config_dict(None)
assert cfg.tracks == []
assert cfg.master_volume == 1.0
assert cfg.normalize is True
assert cfg.max_output_volume == 1.5
def test_empty_dict_defaults(self):
cfg = MultiTrackMixConfig.from_config_dict({})
assert cfg.tracks == []
assert cfg.master_volume == 1.0
assert cfg.normalize is True
def test_single_track(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [{"audio_path": "a.mp3", "track_type": "bgm", "volume": 0.5}],
}
)
assert len(cfg.tracks) == 1
assert cfg.tracks[0].audio_path == "a.mp3"
assert cfg.tracks[0].track_type == "bgm"
assert cfg.tracks[0].volume == 0.5
def test_multiple_tracks(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"audio_path": "main.wav", "track_type": "main"},
{"audio_path": "bgm.mp3", "track_type": "bgm"},
{"audio_path": "sfx.wav", "track_type": "sfx"},
],
}
)
assert len(cfg.tracks) == 3
types = [t.track_type for t in cfg.tracks]
assert "main" in types
assert "bgm" in types
assert "sfx" in types
def test_skip_disabled_track(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"audio_path": "a.mp3", "enabled": True},
{"audio_path": "b.mp3", "enabled": False},
],
}
)
assert len(cfg.tracks) == 1
assert cfg.tracks[0].audio_path == "a.mp3"
def test_skip_missing_audio_path(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"audio_path": "a.mp3"},
{"track_type": "bgm"}, # 无audio_path
],
}
)
assert len(cfg.tracks) == 1
def test_invalid_track_skipped(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"audio_path": "a.mp3"},
"not_a_dict",
{"audio_path": 123, "volume": "bad"}, # 类型不对
],
}
)
# 第二个不是dict跳过,第三个audio_path会被转成字符串"123"
# 但 track_type 非dict的话在 isinstance(t, dict) 判断就被跳过
assert len(cfg.tracks) >= 1
def test_master_volume_clamped(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [],
"master_volume": 3.0,
}
)
assert cfg.master_volume == 2.0
def test_master_volume_negative_clamped(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"master_volume": -1.0,
}
)
assert cfg.master_volume == 0.0
def test_invalid_master_volume_fallback(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"master_volume": "invalid",
}
)
assert cfg.master_volume == 1.0
def test_normalize_false(self):
cfg = MultiTrackMixConfig.from_config_dict({"normalize": False})
assert cfg.normalize is False
def test_custom_max_output_volume(self):
cfg = MultiTrackMixConfig.from_config_dict({"max_output_volume": 2.0})
assert cfg.max_output_volume == 2.0
def test_invalid_max_output_volume_fallback(self):
cfg = MultiTrackMixConfig.from_config_dict({"max_output_volume": "bad"})
assert cfg.max_output_volume == 1.5
def test_not_dict_config(self):
cfg = MultiTrackMixConfig.from_config_dict("not a dict")
assert cfg.tracks == []
assert cfg.master_volume == 1.0
# ── MultiTrackMixConfig 属性 ─────────────────────────────────────────────────
class TestMultiTrackMixConfigProperties:
def test_has_effect_empty(self):
cfg = MultiTrackMixConfig()
assert cfg.has_effect is False
def test_has_effect_with_tracks(self):
cfg = MultiTrackMixConfig(
tracks=[
AudioTrack(audio_path="a.mp3", enabled=True),
]
)
assert cfg.has_effect is True
def test_has_effect_all_disabled(self):
cfg = MultiTrackMixConfig(
tracks=[
AudioTrack(audio_path="a.mp3", enabled=False),
]
)
assert cfg.has_effect is False
def test_effective_track_count(self):
cfg = MultiTrackMixConfig(
tracks=[
AudioTrack(audio_path="a.mp3", enabled=True),
AudioTrack(audio_path="b.mp3", enabled=False),
AudioTrack(audio_path="c.mp3", enabled=True),
AudioTrack(audio_path="", enabled=True),
]
)
assert cfg.effective_track_count == 2
def test_main_tracks(self):
cfg = MultiTrackMixConfig(
tracks=[
AudioTrack(audio_path="m1.mp3", track_type=TRACK_TYPE_MAIN),
AudioTrack(audio_path="b1.mp3", track_type=TRACK_TYPE_BGM),
AudioTrack(audio_path="m2.mp3", track_type=TRACK_TYPE_MAIN, enabled=False),
]
)
mains = cfg.main_tracks
assert len(mains) == 1
assert mains[0].audio_path == "m1.mp3"
def test_bgm_tracks(self):
cfg = MultiTrackMixConfig(
tracks=[
AudioTrack(audio_path="b1.mp3", track_type=TRACK_TYPE_BGM),
AudioTrack(audio_path="b2.mp3", track_type=TRACK_TYPE_BGM),
AudioTrack(audio_path="v1.mp3", track_type=TRACK_TYPE_VOICEOVER),
]
)
assert len(cfg.bgm_tracks) == 2
# ── 工具函数 ─────────────────────────────────────────────────────────────────
class TestUtils:
@pytest.mark.parametrize(
"name,expected",
[
("song.mp3", True),
("audio.WAV", True),
("track.m4a", True),
("video.mp4", False),
("text.txt", False),
("", False),
("/path/to/music.flac", True),
("sound.OGG", True),
],
)
def test_is_valid_audio_extension(self, name, expected):
assert is_valid_audio_extension(name) is expected
def test_clamp_volume_within_range(self):
assert clamp_volume(1.0) == 1.0
assert clamp_volume(0.0) == 0.0
assert clamp_volume(2.0) == 2.0
def test_clamp_volume_below_min(self):
assert clamp_volume(-0.5) == 0.0
def test_clamp_volume_above_max(self):
assert clamp_volume(3.0) == 2.0
def test_clamp_volume_custom_range(self):
assert clamp_volume(0.5, 0.2, 0.8) == 0.5
assert clamp_volume(0.1, 0.2, 0.8) == 0.2
assert clamp_volume(1.0, 0.2, 0.8) == 0.8