3921a657e8
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 52s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m56s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m27s
CI/CD Pipeline / Unit Tests (push) Failing after 8m24s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 16m1s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 21s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m34s
224 lines
8.3 KiB
Python
Executable File
224 lines
8.3 KiB
Python
Executable File
"""音频合并器单元测试."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from packages.application.tts_job.audio_merger import AudioMergeError, AudioMerger
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_audio_dir():
|
|
"""创建临时目录,放几个模拟音频文件"""
|
|
tmpdir = tempfile.mkdtemp()
|
|
files = []
|
|
for i in range(3):
|
|
fpath = os.path.join(tmpdir, f"part{i}.mp3")
|
|
with open(fpath, "wb") as f:
|
|
f.write(f"audio_data_{i}".encode() * 100)
|
|
files.append(fpath)
|
|
yield files
|
|
import shutil
|
|
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
|
|
|
|
class TestAudioMerger:
|
|
"""AudioMerger 测试"""
|
|
|
|
def test_empty_list_raises_error(self):
|
|
"""空列表抛出 AudioMergeError"""
|
|
merger = AudioMerger()
|
|
with pytest.raises(AudioMergeError, match="没有可合并的音频文件"):
|
|
merger.merge([])
|
|
|
|
def test_single_file_returns_content(self, sample_audio_dir):
|
|
"""单文件直接返回文件内容"""
|
|
merger = AudioMerger()
|
|
result = merger.merge([sample_audio_dir[0]])
|
|
|
|
with open(sample_audio_dir[0], "rb") as f:
|
|
expected = f.read()
|
|
|
|
assert result == expected
|
|
|
|
def test_single_file_no_ffmpeg_needed(self, sample_audio_dir):
|
|
"""单文件不需要调用 FFmpeg"""
|
|
with patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg:
|
|
merger = AudioMerger()
|
|
merger.merge([sample_audio_dir[0]])
|
|
mock_ffmpeg.assert_not_called()
|
|
|
|
def test_merge_multiple_files(self, sample_audio_dir):
|
|
"""多文件合并调用 FFmpeg"""
|
|
with (
|
|
patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
|
|
patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
|
|
):
|
|
# 模拟 FFmpeg 成功:在 output_path 写点数据
|
|
def fake_run_ffmpeg(cmd, timeout=120):
|
|
output_idx = cmd.index("-c") + 2 # -c copy 后面是 output_path
|
|
output_path = cmd[-1]
|
|
with open(output_path, "wb") as f:
|
|
f.write(b"merged_audio_data")
|
|
return MagicMock(stdout=b"", stderr=b"")
|
|
|
|
mock_ffmpeg.side_effect = fake_run_ffmpeg
|
|
|
|
merger = AudioMerger()
|
|
result = merger.merge(sample_audio_dir)
|
|
|
|
assert result == b"merged_audio_data"
|
|
mock_ffmpeg.assert_called_once()
|
|
|
|
def test_merge_concat_list_generated(self, sample_audio_dir):
|
|
"""生成正确的 concat demuxer 列表文件"""
|
|
import subprocess
|
|
|
|
with (
|
|
patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
|
|
patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
|
|
):
|
|
|
|
captured_list_content = []
|
|
|
|
def fake_run_ffmpeg(cmd, timeout=120):
|
|
# 找到 -i 参数后面的文件路径
|
|
# 命令结构: ffmpeg -y -f concat -safe 0 -i LIST_PATH -c copy OUTPUT
|
|
for i, arg in enumerate(cmd):
|
|
if arg == "-i" and i + 1 < len(cmd):
|
|
list_path = cmd[i + 1]
|
|
if list_path.endswith(".txt"):
|
|
with open(list_path, "r") as f:
|
|
captured_list_content.append(f.read())
|
|
break
|
|
# 写输出文件
|
|
output_path = cmd[-1]
|
|
with open(output_path, "wb") as f:
|
|
f.write(b"fake")
|
|
return MagicMock(stdout=b"", stderr=b"")
|
|
|
|
mock_ffmpeg.side_effect = fake_run_ffmpeg
|
|
|
|
merger = AudioMerger()
|
|
merger.merge(sample_audio_dir, output_format="mp3")
|
|
|
|
# 检查列表文件包含所有输入文件
|
|
assert len(captured_list_content) == 1
|
|
list_content = captured_list_content[0]
|
|
for fpath in sample_audio_dir:
|
|
assert fpath in list_content.replace("'\\''", "'")
|
|
|
|
def test_merge_ffmpeg_failure_raises(self, sample_audio_dir):
|
|
"""FFmpeg 失败抛出 AudioMergeError"""
|
|
from subprocess import CalledProcessError
|
|
|
|
with (
|
|
patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
|
|
patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
|
|
):
|
|
mock_ffmpeg.side_effect = CalledProcessError(returncode=1, cmd=["ffmpeg"], stderr=b"error message")
|
|
|
|
merger = AudioMerger()
|
|
with pytest.raises(AudioMergeError, match="FFmpeg 合并失败"):
|
|
merger.merge(sample_audio_dir)
|
|
|
|
def test_merge_timeout_raises(self, sample_audio_dir):
|
|
"""合并超时抛出 AudioMergeError"""
|
|
from subprocess import TimeoutExpired
|
|
|
|
with (
|
|
patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
|
|
patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
|
|
):
|
|
mock_ffmpeg.side_effect = TimeoutExpired(cmd=["ffmpeg"], timeout=120)
|
|
|
|
merger = AudioMerger()
|
|
with pytest.raises(AudioMergeError, match="超时"):
|
|
merger.merge(sample_audio_dir)
|
|
|
|
def test_merge_cleanup_temp_dir(self, sample_audio_dir):
|
|
"""合并完成后清理临时目录"""
|
|
with (
|
|
patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
|
|
patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
|
|
patch("packages.application.tts_job.audio_merger.shutil.rmtree") as mock_rmtree,
|
|
):
|
|
|
|
def fake_run_ffmpeg(cmd, timeout=120):
|
|
output_path = cmd[-1]
|
|
with open(output_path, "wb") as f:
|
|
f.write(b"data")
|
|
return MagicMock()
|
|
|
|
mock_ffmpeg.side_effect = fake_run_ffmpeg
|
|
|
|
merger = AudioMerger()
|
|
merger.merge(sample_audio_dir)
|
|
|
|
mock_rmtree.assert_called_once()
|
|
# 第一个参数是临时目录路径
|
|
temp_dir_path = mock_rmtree.call_args[0][0]
|
|
assert "tts_merge_" in temp_dir_path
|
|
|
|
def test_merge_cleanup_on_error(self, sample_audio_dir):
|
|
"""合并失败也清理临时目录"""
|
|
with (
|
|
patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
|
|
patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
|
|
patch("packages.application.tts_job.audio_merger.shutil.rmtree") as mock_rmtree,
|
|
):
|
|
from subprocess import CalledProcessError
|
|
|
|
mock_ffmpeg.side_effect = CalledProcessError(1, ["ffmpeg"])
|
|
|
|
merger = AudioMerger()
|
|
try:
|
|
merger.merge(sample_audio_dir)
|
|
except AudioMergeError:
|
|
pass
|
|
|
|
mock_rmtree.assert_called_once()
|
|
|
|
def test_merge_custom_output_format(self, sample_audio_dir):
|
|
"""自定义输出格式"""
|
|
with (
|
|
patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
|
|
patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
|
|
):
|
|
|
|
def fake_run_ffmpeg(cmd, timeout=120):
|
|
output_path = cmd[-1]
|
|
assert output_path.endswith(".wav")
|
|
with open(output_path, "wb") as f:
|
|
f.write(b"data")
|
|
return MagicMock()
|
|
|
|
mock_ffmpeg.side_effect = fake_run_ffmpeg
|
|
|
|
merger = AudioMerger()
|
|
merger.merge(sample_audio_dir, output_format="wav")
|
|
|
|
def test_merge_two_files(self, sample_audio_dir):
|
|
"""两个文件合并"""
|
|
with (
|
|
patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
|
|
patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
|
|
):
|
|
|
|
def fake_run_ffmpeg(cmd, timeout=120):
|
|
output_path = cmd[-1]
|
|
with open(output_path, "wb") as f:
|
|
f.write(b"two_files_merged")
|
|
return MagicMock()
|
|
|
|
mock_ffmpeg.side_effect = fake_run_ffmpeg
|
|
|
|
merger = AudioMerger()
|
|
result = merger.merge(sample_audio_dir[:2])
|
|
assert result == b"two_files_merged"
|