fd61800be9
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
CI/CD Pipeline / CI Gate (push) Has been cancelled
522 lines
17 KiB
Python
Executable File
522 lines
17 KiB
Python
Executable File
"""audio_track_config 多轨道音频配置单测."""
|
|
|
|
import pytest
|
|
from 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_max_tracks(self):
|
|
assert MAX_AUDIO_TRACKS == 8
|
|
|
|
def test_default_volumes(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_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
|
|
|
|
|
|
# ── AudioTrack ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestAudioTrackDefaults:
|
|
"""AudioTrack 默认值"""
|
|
|
|
def test_default_values(self):
|
|
t = AudioTrack()
|
|
assert t.track_id == ""
|
|
assert t.track_type == TRACK_TYPE_SFX
|
|
assert t.audio_path == ""
|
|
assert t.volume == 1.0
|
|
assert t.fade_in == 0.0
|
|
assert t.fade_out == 0.0
|
|
assert t.start_time == 0.0
|
|
assert t.duration == 0.0
|
|
assert t.enabled is True
|
|
|
|
def test_custom_track(self):
|
|
t = AudioTrack(
|
|
track_id="bgm_001",
|
|
track_type=TRACK_TYPE_BGM,
|
|
audio_path="/music/bgm.mp3",
|
|
volume=0.5,
|
|
fade_in=1.5,
|
|
fade_out=2.0,
|
|
start_time=3.0,
|
|
duration=30.0,
|
|
enabled=False,
|
|
)
|
|
assert t.track_id == "bgm_001"
|
|
assert t.track_type == TRACK_TYPE_BGM
|
|
assert t.audio_path == "/music/bgm.mp3"
|
|
assert t.volume == 0.5
|
|
assert t.fade_in == 1.5
|
|
assert t.start_time == 3.0
|
|
assert t.duration == 30.0
|
|
assert t.enabled is False
|
|
|
|
|
|
class TestAudioTrackFromDict:
|
|
"""AudioTrack.from_dict"""
|
|
|
|
def test_empty_dict(self):
|
|
t = AudioTrack.from_dict({})
|
|
assert t.track_type == TRACK_TYPE_SFX
|
|
assert t.audio_path == ""
|
|
assert t.volume == DEFAULT_VOLUMES[TRACK_TYPE_SFX]
|
|
assert t.enabled is True
|
|
|
|
def test_full_dict(self):
|
|
t = AudioTrack.from_dict(
|
|
{
|
|
"track_id": "t1",
|
|
"track_type": "bgm",
|
|
"audio_path": "/a.mp3",
|
|
"volume": 0.8,
|
|
"fade_in": 1.0,
|
|
"fade_out": 2.0,
|
|
"start_time": 5.0,
|
|
"duration": 60.0,
|
|
"enabled": True,
|
|
}
|
|
)
|
|
assert t.track_id == "t1"
|
|
assert t.track_type == "bgm"
|
|
assert t.volume == 0.8
|
|
assert t.fade_in == 1.0
|
|
assert t.duration == 60.0
|
|
|
|
def test_volume_clamped_to_zero(self):
|
|
t = AudioTrack.from_dict({"volume": -0.5})
|
|
assert t.volume == 0.0
|
|
|
|
def test_volume_clamped_to_two(self):
|
|
t = AudioTrack.from_dict({"volume": 3.0})
|
|
assert t.volume == 2.0
|
|
|
|
def test_invalid_volume_falls_back_to_default(self):
|
|
t = AudioTrack.from_dict({"track_type": "bgm", "volume": "abc"})
|
|
assert t.volume == DEFAULT_VOLUMES[TRACK_TYPE_BGM]
|
|
|
|
def test_invalid_fade_in_falls_back(self):
|
|
t = AudioTrack.from_dict({"fade_in": "bad"})
|
|
assert t.fade_in == 0.0
|
|
|
|
def test_negative_fade_in_clamped(self):
|
|
t = AudioTrack.from_dict({"fade_in": -1.0})
|
|
assert t.fade_in == 0.0
|
|
|
|
def test_invalid_fade_out_falls_back(self):
|
|
t = AudioTrack.from_dict({"fade_out": None})
|
|
assert t.fade_out == 0.0
|
|
|
|
def test_negative_start_time_clamped(self):
|
|
t = AudioTrack.from_dict({"start_time": -5.0})
|
|
assert t.start_time == 0.0
|
|
|
|
def test_invalid_duration_falls_back(self):
|
|
t = AudioTrack.from_dict({"duration": "long"})
|
|
assert t.duration == 0.0
|
|
|
|
def test_bgm_default_volume(self):
|
|
t = AudioTrack.from_dict({"track_type": "bgm"})
|
|
assert t.volume == 0.3
|
|
|
|
def test_main_default_volume(self):
|
|
t = AudioTrack.from_dict({"track_type": "main"})
|
|
assert t.volume == 1.0
|
|
|
|
def test_voiceover_default_volume(self):
|
|
t = AudioTrack.from_dict({"track_type": "voiceover"})
|
|
assert t.volume == 1.0
|
|
|
|
def test_ambient_default_volume(self):
|
|
t = AudioTrack.from_dict({"track_type": "ambient"})
|
|
assert t.volume == 0.2
|
|
|
|
def test_unknown_type_default_volume(self):
|
|
t = AudioTrack.from_dict({"track_type": "unknown_type"})
|
|
assert t.volume == 1.0
|
|
|
|
def test_enabled_false(self):
|
|
t = AudioTrack.from_dict({"enabled": False})
|
|
assert t.enabled is False
|
|
|
|
|
|
class TestAudioTrackValidate:
|
|
"""AudioTrack.validate"""
|
|
|
|
def test_empty_path_invalid(self):
|
|
t = AudioTrack(audio_path="")
|
|
valid, msg = t.validate()
|
|
assert valid is False
|
|
assert "audio_path" in msg
|
|
|
|
def test_valid_track(self):
|
|
t = AudioTrack(audio_path="/a.mp3", volume=0.5)
|
|
valid, msg = t.validate()
|
|
assert valid is True
|
|
assert msg == ""
|
|
|
|
def test_volume_below_zero_invalid(self):
|
|
t = AudioTrack(audio_path="/a.mp3", volume=-0.1)
|
|
valid, msg = t.validate()
|
|
assert valid is False
|
|
assert "volume" in msg
|
|
|
|
def test_volume_above_two_invalid(self):
|
|
t = AudioTrack(audio_path="/a.mp3", volume=2.1)
|
|
valid, msg = t.validate()
|
|
assert valid is False
|
|
assert "volume" in msg
|
|
|
|
def test_volume_zero_valid(self):
|
|
t = AudioTrack(audio_path="/a.mp3", volume=0.0)
|
|
valid, _ = t.validate()
|
|
assert valid is True
|
|
|
|
def test_volume_two_valid(self):
|
|
t = AudioTrack(audio_path="/a.mp3", volume=2.0)
|
|
valid, _ = t.validate()
|
|
assert valid is True
|
|
|
|
def test_negative_fade_in_invalid(self):
|
|
t = AudioTrack(audio_path="/a.mp3", fade_in=-1.0)
|
|
valid, msg = t.validate()
|
|
assert valid is False
|
|
assert "fade_in" in msg
|
|
|
|
def test_negative_fade_out_invalid(self):
|
|
t = AudioTrack(audio_path="/a.mp3", fade_out=-1.0)
|
|
valid, msg = t.validate()
|
|
assert valid is False
|
|
assert "fade_out" in msg
|
|
|
|
def test_negative_start_time_invalid(self):
|
|
t = AudioTrack(audio_path="/a.mp3", start_time=-0.5)
|
|
valid, msg = t.validate()
|
|
assert valid is False
|
|
assert "start_time" in msg
|
|
|
|
def test_negative_duration_invalid(self):
|
|
t = AudioTrack(audio_path="/a.mp3", duration=-1.0)
|
|
valid, msg = t.validate()
|
|
assert valid is False
|
|
assert "duration" in msg
|
|
|
|
|
|
class TestAudioTrackIsEffective:
|
|
"""AudioTrack.is_effective 属性"""
|
|
|
|
def test_enabled_with_path_effective(self):
|
|
t = AudioTrack(audio_path="/a.mp3", enabled=True)
|
|
assert t.is_effective is True
|
|
|
|
def test_disabled_not_effective(self):
|
|
t = AudioTrack(audio_path="/a.mp3", enabled=False)
|
|
assert t.is_effective is False
|
|
|
|
def test_no_path_not_effective(self):
|
|
t = AudioTrack(audio_path="", enabled=True)
|
|
assert t.is_effective is False
|
|
|
|
def test_disabled_no_path_not_effective(self):
|
|
t = AudioTrack(audio_path="", enabled=False)
|
|
assert t.is_effective is False
|
|
|
|
|
|
# ── MultiTrackMixConfig ──────────────────────────────────────────────────────
|
|
|
|
|
|
class TestMultiTrackMixConfigDefaults:
|
|
"""MultiTrackMixConfig 默认值"""
|
|
|
|
def test_default_values(self):
|
|
c = MultiTrackMixConfig()
|
|
assert c.tracks == []
|
|
assert c.master_volume == 1.0
|
|
assert c.normalize is True
|
|
assert c.max_output_volume == 1.5
|
|
|
|
def test_custom_config(self):
|
|
t1 = AudioTrack(track_id="t1", audio_path="/a.mp3")
|
|
c = MultiTrackMixConfig(
|
|
tracks=[t1],
|
|
master_volume=0.8,
|
|
normalize=False,
|
|
max_output_volume=2.0,
|
|
)
|
|
assert len(c.tracks) == 1
|
|
assert c.master_volume == 0.8
|
|
assert c.normalize is False
|
|
assert c.max_output_volume == 2.0
|
|
|
|
|
|
class TestMultiTrackFromConfigDict:
|
|
"""MultiTrackMixConfig.from_config_dict"""
|
|
|
|
def test_none_returns_default(self):
|
|
c = MultiTrackMixConfig.from_config_dict(None)
|
|
assert len(c.tracks) == 0
|
|
assert c.master_volume == 1.0
|
|
|
|
def test_empty_dict_returns_default(self):
|
|
c = MultiTrackMixConfig.from_config_dict({})
|
|
assert len(c.tracks) == 0
|
|
|
|
def test_non_dict_returns_default(self):
|
|
c = MultiTrackMixConfig.from_config_dict("not a dict")
|
|
assert len(c.tracks) == 0
|
|
|
|
def test_single_track(self):
|
|
c = MultiTrackMixConfig.from_config_dict(
|
|
{
|
|
"tracks": [
|
|
{"track_id": "t1", "track_type": "bgm", "audio_path": "/bgm.mp3", "volume": 0.5},
|
|
],
|
|
}
|
|
)
|
|
assert len(c.tracks) == 1
|
|
assert c.tracks[0].track_id == "t1"
|
|
assert c.tracks[0].volume == 0.5
|
|
|
|
def test_multiple_tracks(self):
|
|
c = MultiTrackMixConfig.from_config_dict(
|
|
{
|
|
"tracks": [
|
|
{"track_id": "t1", "track_type": "main", "audio_path": "/main.wav"},
|
|
{"track_id": "t2", "track_type": "bgm", "audio_path": "/bgm.mp3"},
|
|
{"track_id": "t3", "track_type": "sfx", "audio_path": "/sfx.wav"},
|
|
],
|
|
}
|
|
)
|
|
assert len(c.tracks) == 3
|
|
assert c.tracks[0].track_type == "main"
|
|
assert c.tracks[1].track_type == "bgm"
|
|
assert c.tracks[2].track_type == "sfx"
|
|
|
|
def test_skip_disabled_tracks(self):
|
|
c = MultiTrackMixConfig.from_config_dict(
|
|
{
|
|
"tracks": [
|
|
{"track_id": "t1", "audio_path": "/a.mp3", "enabled": True},
|
|
{"track_id": "t2", "audio_path": "/b.mp3", "enabled": False},
|
|
{"track_id": "t3", "audio_path": "/c.mp3"},
|
|
],
|
|
}
|
|
)
|
|
assert len(c.tracks) == 2
|
|
ids = [t.track_id for t in c.tracks]
|
|
assert "t1" in ids
|
|
assert "t2" not in ids
|
|
assert "t3" in ids
|
|
|
|
def test_skip_no_path_tracks(self):
|
|
c = MultiTrackMixConfig.from_config_dict(
|
|
{
|
|
"tracks": [
|
|
{"track_id": "t1", "audio_path": "/a.mp3"},
|
|
{"track_id": "t2", "audio_path": ""},
|
|
{"track_id": "t3"},
|
|
],
|
|
}
|
|
)
|
|
assert len(c.tracks) == 1
|
|
assert c.tracks[0].track_id == "t1"
|
|
|
|
def test_skip_non_dict_tracks(self):
|
|
c = MultiTrackMixConfig.from_config_dict(
|
|
{
|
|
"tracks": [
|
|
{"track_id": "t1", "audio_path": "/a.mp3"},
|
|
"not a dict",
|
|
123,
|
|
None,
|
|
],
|
|
}
|
|
)
|
|
assert len(c.tracks) == 1
|
|
|
|
def test_master_volume_clamped(self):
|
|
c = MultiTrackMixConfig.from_config_dict({"master_volume": 3.0})
|
|
assert c.master_volume == 2.0
|
|
|
|
def test_master_volume_negative_clamped(self):
|
|
c = MultiTrackMixConfig.from_config_dict({"master_volume": -1.0})
|
|
assert c.master_volume == 0.0
|
|
|
|
def test_invalid_master_volume_falls_back(self):
|
|
c = MultiTrackMixConfig.from_config_dict({"master_volume": "high"})
|
|
assert c.master_volume == 1.0
|
|
|
|
def test_normalize_false(self):
|
|
c = MultiTrackMixConfig.from_config_dict({"normalize": False})
|
|
assert c.normalize is False
|
|
|
|
def test_max_output_volume_custom(self):
|
|
c = MultiTrackMixConfig.from_config_dict({"max_output_volume": 2.0})
|
|
assert c.max_output_volume == 2.0
|
|
|
|
def test_invalid_max_output_volume_falls_back(self):
|
|
c = MultiTrackMixConfig.from_config_dict({"max_output_volume": "big"})
|
|
assert c.max_output_volume == 1.5
|
|
|
|
def test_tracks_not_list_ignored(self):
|
|
c = MultiTrackMixConfig.from_config_dict({"tracks": "not a list"})
|
|
assert len(c.tracks) == 0
|
|
|
|
|
|
class TestMultiTrackProperties:
|
|
"""MultiTrackMixConfig 属性方法"""
|
|
|
|
def _make_config(self):
|
|
return MultiTrackMixConfig.from_config_dict(
|
|
{
|
|
"tracks": [
|
|
{"track_id": "m1", "track_type": "main", "audio_path": "/m.wav"},
|
|
{"track_id": "b1", "track_type": "bgm", "audio_path": "/b1.mp3"},
|
|
{"track_id": "b2", "track_type": "bgm", "audio_path": "/b2.mp3", "enabled": False},
|
|
{"track_id": "s1", "track_type": "sfx", "audio_path": "/s.wav"},
|
|
{"track_id": "x", "track_type": "ambient", "audio_path": ""},
|
|
],
|
|
}
|
|
)
|
|
|
|
def test_has_effect_true(self):
|
|
c = self._make_config()
|
|
assert c.has_effect is True
|
|
|
|
def test_has_effect_false(self):
|
|
c = MultiTrackMixConfig()
|
|
assert c.has_effect is False
|
|
|
|
def test_effective_track_count(self):
|
|
c = self._make_config()
|
|
# m1 + b1 + s1 = 3个有效(b2禁用,x无路径)
|
|
assert c.effective_track_count == 3
|
|
|
|
def test_main_tracks(self):
|
|
c = self._make_config()
|
|
mains = c.main_tracks
|
|
assert len(mains) == 1
|
|
assert mains[0].track_id == "m1"
|
|
|
|
def test_bgm_tracks(self):
|
|
c = self._make_config()
|
|
bgms = c.bgm_tracks
|
|
assert len(bgms) == 1 # 只有b1有效
|
|
assert bgms[0].track_id == "b1"
|
|
|
|
def test_empty_tracks(self):
|
|
c = MultiTrackMixConfig()
|
|
assert c.effective_track_count == 0
|
|
assert c.main_tracks == []
|
|
assert c.bgm_tracks == []
|
|
|
|
|
|
# ── 工具函数 ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestIsValidAudioExtension:
|
|
"""is_valid_audio_extension 函数"""
|
|
|
|
def test_mp3(self):
|
|
assert is_valid_audio_extension("song.mp3") is True
|
|
|
|
def test_wav(self):
|
|
assert is_valid_audio_extension("sound.wav") is True
|
|
|
|
def test_aac(self):
|
|
assert is_valid_audio_extension("audio.aac") is True
|
|
|
|
def test_ogg(self):
|
|
assert is_valid_audio_extension("music.ogg") is True
|
|
|
|
def test_flac(self):
|
|
assert is_valid_audio_extension("lossless.flac") is True
|
|
|
|
def test_m4a(self):
|
|
assert is_valid_audio_extension("apple.m4a") is True
|
|
|
|
def test_wma(self):
|
|
assert is_valid_audio_extension("windows.wma") is True
|
|
|
|
def test_uppercase_extension(self):
|
|
assert is_valid_audio_extension("SONG.MP3") is True
|
|
|
|
def test_mixed_case_extension(self):
|
|
assert is_valid_audio_extension("song.Mp3") is True
|
|
|
|
def test_mp4_not_valid(self):
|
|
assert is_valid_audio_extension("video.mp4") is False
|
|
|
|
def test_txt_not_valid(self):
|
|
assert is_valid_audio_extension("notes.txt") is False
|
|
|
|
def test_no_extension(self):
|
|
assert is_valid_audio_extension("README") is False
|
|
|
|
def test_full_path(self):
|
|
assert is_valid_audio_extension("/home/user/music/song.mp3") is True
|
|
|
|
|
|
class TestClampVolume:
|
|
"""clamp_volume 函数"""
|
|
|
|
def test_within_range(self):
|
|
assert clamp_volume(0.5) == 0.5
|
|
|
|
def test_exact_min(self):
|
|
assert clamp_volume(0.0) == 0.0
|
|
|
|
def test_exact_max(self):
|
|
assert clamp_volume(2.0) == 2.0
|
|
|
|
def test_below_min(self):
|
|
assert clamp_volume(-1.0) == 0.0
|
|
|
|
def test_above_max(self):
|
|
assert clamp_volume(3.0) == 2.0
|
|
|
|
def test_custom_bounds(self):
|
|
assert clamp_volume(5.0, min_vol=1.0, max_vol=10.0) == 5.0
|
|
|
|
def test_custom_below_min(self):
|
|
assert clamp_volume(0.5, min_vol=1.0) == 1.0
|
|
|
|
def test_custom_above_max(self):
|
|
assert clamp_volume(15.0, max_vol=10.0) == 10.0
|