Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c368373d05 | |||
| aa3d017b13 |
Executable
+521
@@ -0,0 +1,521 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user