"""VoiceExtractor 纯逻辑单测 — 命令构建 + 边界用例. 通过 mock run_ffmpeg 验证 FFmpeg 命令参数是否正确, 不实际执行 FFmpeg,确保测试轻量快速。 """ from __future__ import annotations import os from unittest.mock import MagicMock, patch import pytest from worker_app.tasks.voice_extraction import VoiceExtractor class TestVoiceExtractorExtractVoiceCommand: """extract_voice 命令构建测试.""" def test_default_params_correct_command(self): """默认参数下 FFmpeg 命令正确.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: result = extractor.extract_voice("/tmp/input.mp4", "/tmp/output.mp3") assert result == "/tmp/output.mp3" mock_run.assert_called_once() cmd = mock_run.call_args[0][0] # 基本结构验证 assert cmd[0] == "ffmpeg" assert "-y" in cmd assert cmd[cmd.index("-i") + 1] == "/tmp/input.mp4" assert "-vn" in cmd # 无视频流 assert cmd[-1] == "/tmp/output.mp3" # 音频滤镜验证 af_idx = cmd.index("-af") af_value = cmd[af_idx + 1] assert "highpass=f=200" in af_value assert "afftdn=bn=20" in af_value assert "bandpass=f=300:width_type=h:width=3000" in af_value assert "loudnorm" in af_value # 编码验证 assert "libmp3lame" in cmd assert "-q:a" in cmd assert cmd[cmd.index("-q:a") + 1] == "2" def test_custom_highpass(self): """自定义 highpass 频率.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", highpass=500) cmd = mock_run.call_args[0][0] af_value = cmd[cmd.index("-af") + 1] assert "highpass=f=500" in af_value def test_custom_bandpass_freq(self): """自定义 bandpass 中心频率.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", bandpass_freq=500) cmd = mock_run.call_args[0][0] af_value = cmd[cmd.index("-af") + 1] assert "bandpass=f=500:" in af_value def test_custom_bandpass_width(self): """自定义 bandpass 宽度.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", bandpass_width=5000) cmd = mock_run.call_args[0][0] af_value = cmd[cmd.index("-af") + 1] assert "width=5000" in af_value def test_custom_noise_reduction(self): """自定义降噪强度.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", noise_reduction=30) cmd = mock_run.call_args[0][0] af_value = cmd[cmd.index("-af") + 1] assert "afftdn=bn=30" in af_value def test_filter_order_is_correct(self): """滤镜顺序:highpass → 降噪 → bandpass → loudnorm.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3") cmd = mock_run.call_args[0][0] af_value = cmd[cmd.index("-af") + 1] hp_pos = af_value.index("highpass") dn_pos = af_value.index("afftdn") bp_pos = af_value.index("bandpass") ln_pos = af_value.index("loudnorm") assert hp_pos < dn_pos < bp_pos < ln_pos def test_creates_output_directory(self, tmp_path): """输出目录不存在时自动创建.""" out_dir = tmp_path / "nested" / "deep" out_file = out_dir / "voice.mp3" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg"): extractor.extract_voice("/tmp/in.mp4", str(out_file)) assert out_dir.exists() assert out_dir.is_dir() def test_returns_output_path(self): """返回值为输出路径.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg"): result = extractor.extract_voice("/tmp/in.mp4", "/tmp/voice.mp3") assert result == "/tmp/voice.mp3" class TestVoiceExtractorExtractBackgroundCommand: """extract_background 命令构建测试.""" def test_default_params_correct_command(self): """默认参数下 FFmpeg 命令正确.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: result = extractor.extract_background("/tmp/input.mp4", "/tmp/output.mp3") assert result == "/tmp/output.mp3" mock_run.assert_called_once() cmd = mock_run.call_args[0][0] # 基本结构 assert cmd[0] == "ffmpeg" assert "-y" in cmd assert cmd[cmd.index("-i") + 1] == "/tmp/input.mp4" assert "-vn" in cmd assert cmd[-1] == "/tmp/output.mp3" # 音频滤镜 af_idx = cmd.index("-af") af_value = cmd[af_idx + 1] assert "lowpass=f=200" in af_value assert "loudnorm" in af_value # 编码 assert "libmp3lame" in cmd def test_custom_lowpass_freq(self): """自定义 lowpass 频率.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: extractor.extract_background("/tmp/in.mp4", "/tmp/out.mp3", lowpass=500) cmd = mock_run.call_args[0][0] af_value = cmd[cmd.index("-af") + 1] assert "lowpass=f=500" in af_value def test_filter_order_background(self): """背景音滤镜顺序:lowpass → loudnorm.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: extractor.extract_background("/tmp/in.mp4", "/tmp/out.mp3") cmd = mock_run.call_args[0][0] af_value = cmd[cmd.index("-af") + 1] lp_pos = af_value.index("lowpass") ln_pos = af_value.index("loudnorm") assert lp_pos < ln_pos def test_background_creates_output_directory(self, tmp_path): """背景音输出目录不存在时自动创建.""" out_dir = tmp_path / "bgm" / "tracks" out_file = out_dir / "bg.mp3" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg"): extractor.extract_background("/tmp/in.mp4", str(out_file)) assert out_dir.exists() class TestVoiceExtractorEdgeCases: """边界情况测试.""" def test_zero_highpass(self): """highpass=0 时的行为(极端低值).""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", highpass=0) cmd = mock_run.call_args[0][0] af_value = cmd[cmd.index("-af") + 1] assert "highpass=f=0" in af_value def test_zero_bandpass_freq(self): """bandpass_freq=0 时的极端情况.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", bandpass_freq=0) cmd = mock_run.call_args[0][0] af_value = cmd[cmd.index("-af") + 1] assert "bandpass=f=0:" in af_value def test_very_high_noise_reduction(self): """极高降噪强度.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", noise_reduction=100) cmd = mock_run.call_args[0][0] af_value = cmd[cmd.index("-af") + 1] assert "afftdn=bn=100" in af_value def test_negative_lowpass_allowed(self): """lowpass 负值(由调用方保证合法性,函数不做校验).""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run: extractor.extract_background("/tmp/in.mp4", "/tmp/out.mp3", lowpass=-10) cmd = mock_run.call_args[0][0] af_value = cmd[cmd.index("-af") + 1] assert "lowpass=f=-10" in af_value def test_run_ffmpeg_propagates_error(self): """_run_ffmpeg 抛出异常时向上传递.""" extractor = VoiceExtractor() with patch.object(VoiceExtractor, "_run_ffmpeg", side_effect=RuntimeError("FFmpeg failed")): with pytest.raises(RuntimeError, match="FFmpeg failed"): extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3") def test_voice_extractor_is_static_method(self): """_run_ffmpeg 是静态方法,可在类上直接调用.""" # 验证 VoiceExtractor 可以直接实例化(无需参数) extractor = VoiceExtractor() assert extractor is not None def test_multiple_extractions_same_instance(self): """同一个实例可多次执行提取.""" extractor = VoiceExtractor() call_count = 0 def fake_run(cmd): nonlocal call_count call_count += 1 with patch.object(VoiceExtractor, "_run_ffmpeg", side_effect=fake_run): extractor.extract_voice("/tmp/a.mp4", "/tmp/a_voice.mp3") extractor.extract_background("/tmp/a.mp4", "/tmp/a_bg.mp3") assert call_count == 2