420 lines
15 KiB
Python
420 lines
15 KiB
Python
"""
|
|
Unit tests for AudioUtils class.
|
|
"""
|
|
|
|
import pytest
|
|
import os
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch, mock_open
|
|
|
|
from src.core.audio_utils_simple import AudioUtils
|
|
|
|
|
|
class TestAudioUtils:
|
|
"""Test AudioUtils class."""
|
|
|
|
def test_initialization(self):
|
|
"""Test AudioUtils initialization."""
|
|
with patch.object(AudioUtils, '_check_dependencies'):
|
|
utils = AudioUtils()
|
|
assert utils is not None
|
|
|
|
def test_supported_formats(self):
|
|
"""Test supported format constants."""
|
|
utils = AudioUtils()
|
|
|
|
expected_formats = {'.mp3', '.m4a', '.wav', '.flac', '.ogg', '.aac', '.wma'}
|
|
assert utils.SUPPORTED_FORMATS == expected_formats
|
|
|
|
def test_audio_quality_thresholds(self):
|
|
"""Test audio quality threshold constants."""
|
|
utils = AudioUtils()
|
|
|
|
assert utils.MIN_SAMPLE_RATE == 16000
|
|
assert utils.MAX_DURATION == 8 * 60 * 60 # 8 hours
|
|
assert utils.MAX_FILE_SIZE == 500 * 1024 * 1024 # 500MB
|
|
|
|
def test_censorship_defaults(self):
|
|
"""Test censorship default constants."""
|
|
utils = AudioUtils()
|
|
|
|
assert utils.DEFAULT_BEEP_FREQUENCY == 1000
|
|
assert utils.DEFAULT_BEEP_VOLUME == -20
|
|
assert utils.DEFAULT_FADE_DURATION == 10
|
|
|
|
@patch('src.core.audio_utils_simple.which')
|
|
def test_check_dependencies_with_ffmpeg(self, mock_which):
|
|
"""Test dependency check with ffmpeg available."""
|
|
mock_which.return_value = "/usr/bin/ffmpeg"
|
|
|
|
# Should not raise warnings
|
|
utils = AudioUtils()
|
|
assert utils is not None
|
|
|
|
@patch('src.core.audio_utils_simple.which')
|
|
def test_check_dependencies_without_ffmpeg(self, mock_which, caplog):
|
|
"""Test dependency check without ffmpeg."""
|
|
mock_which.return_value = None
|
|
|
|
utils = AudioUtils()
|
|
assert "ffmpeg not found" in caplog.text
|
|
|
|
def test_is_supported_format_valid(self):
|
|
"""Test supported format checking with valid formats."""
|
|
utils = AudioUtils()
|
|
|
|
# Test supported formats
|
|
assert utils.is_supported_format("audio.mp3") is True
|
|
assert utils.is_supported_format("audio.wav") is True
|
|
assert utils.is_supported_format("audio.flac") is True
|
|
assert utils.is_supported_format("audio.m4a") is True
|
|
assert utils.is_supported_format("audio.ogg") is True
|
|
assert utils.is_supported_format("audio.aac") is True
|
|
assert utils.is_supported_format("audio.wma") is True
|
|
|
|
# Test case insensitive
|
|
assert utils.is_supported_format("AUDIO.MP3") is True
|
|
assert utils.is_supported_format("Audio.WaV") is True
|
|
|
|
def test_is_supported_format_invalid(self):
|
|
"""Test supported format checking with invalid formats."""
|
|
utils = AudioUtils()
|
|
|
|
# Test unsupported formats
|
|
assert utils.is_supported_format("document.txt") is False
|
|
assert utils.is_supported_format("image.jpg") is False
|
|
assert utils.is_supported_format("video.mp4") is False
|
|
assert utils.is_supported_format("unknown.xyz") is False
|
|
assert utils.is_supported_format("no_extension") is False
|
|
|
|
def test_is_supported_format_error(self):
|
|
"""Test format checking with invalid input."""
|
|
utils = AudioUtils()
|
|
|
|
# Should handle errors gracefully
|
|
assert utils.is_supported_format(None) is False
|
|
assert utils.is_supported_format("") is False
|
|
|
|
def test_validate_audio_file_not_exists(self):
|
|
"""Test validation of non-existent file."""
|
|
utils = AudioUtils()
|
|
|
|
result = utils.validate_audio_file("nonexistent.mp3")
|
|
|
|
assert result["valid"] is False
|
|
assert result["file_exists"] is False
|
|
assert "File does not exist" in result["errors"]
|
|
|
|
@patch('os.path.exists')
|
|
@patch('os.path.getsize')
|
|
def test_validate_audio_file_too_large(self, mock_getsize, mock_exists):
|
|
"""Test validation of oversized file."""
|
|
mock_exists.return_value = True
|
|
mock_getsize.return_value = 600 * 1024 * 1024 # 600MB (over limit)
|
|
|
|
utils = AudioUtils()
|
|
result = utils.validate_audio_file("large.mp3")
|
|
|
|
assert result["valid"] is False
|
|
assert result["file_exists"] is True
|
|
assert any("too large" in error for error in result["errors"])
|
|
|
|
@patch('os.path.exists')
|
|
@patch('os.path.getsize')
|
|
def test_validate_audio_file_unsupported_format(self, mock_getsize, mock_exists):
|
|
"""Test validation of unsupported format."""
|
|
mock_exists.return_value = True
|
|
mock_getsize.return_value = 1024 * 1024 # 1MB
|
|
|
|
utils = AudioUtils()
|
|
result = utils.validate_audio_file("document.txt")
|
|
|
|
assert result["valid"] is False
|
|
assert result["file_exists"] is True
|
|
assert result["format_supported"] is False
|
|
assert "Unsupported file format" in result["errors"]
|
|
|
|
@patch('os.path.exists')
|
|
@patch('os.path.getsize')
|
|
def test_validate_audio_file_success(self, mock_getsize, mock_exists):
|
|
"""Test successful audio file validation."""
|
|
mock_exists.return_value = True
|
|
mock_getsize.return_value = 5 * 1024 * 1024 # 5MB
|
|
|
|
utils = AudioUtils()
|
|
|
|
# Mock get_audio_info method
|
|
mock_audio_info = {
|
|
"duration": 30.0,
|
|
"sample_rate": 44100,
|
|
"channels": 2
|
|
}
|
|
|
|
with patch.object(utils, 'get_audio_info', return_value=mock_audio_info):
|
|
with patch.object(utils, '_validate_audio_properties'):
|
|
result = utils.validate_audio_file("valid.mp3")
|
|
|
|
assert result["file_exists"] is True
|
|
assert result["format_supported"] is True
|
|
assert result["readable"] is True
|
|
assert result["duration"] == 30.0
|
|
assert result["sample_rate"] == 44100
|
|
assert result["channels"] == 2
|
|
|
|
def test_validate_audio_properties_valid(self):
|
|
"""Test audio properties validation with valid properties."""
|
|
utils = AudioUtils()
|
|
|
|
validation_result = {
|
|
"duration": 120.0, # 2 minutes
|
|
"sample_rate": 44100,
|
|
"channels": 2,
|
|
"errors": [],
|
|
"warnings": []
|
|
}
|
|
|
|
utils._validate_audio_properties(validation_result)
|
|
|
|
# Should not add any errors for valid properties
|
|
assert len(validation_result["errors"]) == 0
|
|
|
|
def test_validate_audio_properties_low_sample_rate(self):
|
|
"""Test audio properties validation with low sample rate."""
|
|
utils = AudioUtils()
|
|
|
|
validation_result = {
|
|
"duration": 60.0,
|
|
"sample_rate": 8000, # Below minimum
|
|
"channels": 1,
|
|
"errors": [],
|
|
"warnings": []
|
|
}
|
|
|
|
utils._validate_audio_properties(validation_result)
|
|
|
|
# Should add warning for low sample rate
|
|
assert len(validation_result["warnings"]) > 0
|
|
assert any("sample rate" in warning.lower() for warning in validation_result["warnings"])
|
|
|
|
def test_validate_audio_properties_too_long(self):
|
|
"""Test audio properties validation with excessive duration."""
|
|
utils = AudioUtils()
|
|
|
|
validation_result = {
|
|
"duration": 10 * 60 * 60, # 10 hours (over limit)
|
|
"sample_rate": 44100,
|
|
"channels": 2,
|
|
"errors": [],
|
|
"warnings": []
|
|
}
|
|
|
|
utils._validate_audio_properties(validation_result)
|
|
|
|
# Should add error for excessive duration
|
|
assert len(validation_result["errors"]) > 0
|
|
assert any("too long" in error.lower() for error in validation_result["errors"])
|
|
|
|
def test_validate_audio_properties_mono_warning(self):
|
|
"""Test audio properties validation with mono audio."""
|
|
utils = AudioUtils()
|
|
|
|
validation_result = {
|
|
"duration": 30.0,
|
|
"sample_rate": 44100,
|
|
"channels": 1, # Mono
|
|
"errors": [],
|
|
"warnings": []
|
|
}
|
|
|
|
utils._validate_audio_properties(validation_result)
|
|
|
|
# Should add warning for mono audio
|
|
assert len(validation_result["warnings"]) > 0
|
|
assert any("mono" in warning.lower() for warning in validation_result["warnings"])
|
|
|
|
@patch('src.core.audio_utils_simple.AudioSegment')
|
|
def test_get_audio_info_success(self, mock_audiosegment):
|
|
"""Test getting audio information successfully."""
|
|
# Mock AudioSegment
|
|
mock_audio = Mock()
|
|
mock_audio.duration_seconds = 45.0
|
|
mock_audio.frame_rate = 44100
|
|
mock_audio.channels = 2
|
|
mock_audio.__len__ = Mock(return_value=45000) # 45 seconds in milliseconds
|
|
|
|
mock_audiosegment.from_file.return_value = mock_audio
|
|
|
|
utils = AudioUtils()
|
|
info = utils.get_audio_info("test.mp3")
|
|
|
|
assert info["duration"] == 45.0
|
|
assert info["sample_rate"] == 44100
|
|
assert info["channels"] == 2
|
|
|
|
mock_audiosegment.from_file.assert_called_once_with("test.mp3")
|
|
|
|
@patch('src.core.audio_utils_simple.AudioSegment')
|
|
def test_get_audio_info_error(self, mock_audiosegment):
|
|
"""Test handling error when getting audio information."""
|
|
mock_audiosegment.from_file.side_effect = Exception("Cannot read file")
|
|
|
|
utils = AudioUtils()
|
|
|
|
with pytest.raises(Exception):
|
|
utils.get_audio_info("invalid.mp3")
|
|
|
|
@patch('src.core.audio_utils_simple.AudioSegment')
|
|
def test_load_audio_success(self, mock_audiosegment):
|
|
"""Test loading audio successfully."""
|
|
mock_audio = Mock()
|
|
mock_audiosegment.from_file.return_value = mock_audio
|
|
|
|
utils = AudioUtils()
|
|
result = utils.load_audio("test.wav")
|
|
|
|
assert result == mock_audio
|
|
mock_audiosegment.from_file.assert_called_once_with("test.wav")
|
|
|
|
@patch('src.core.audio_utils_simple.AudioSegment')
|
|
def test_load_audio_error(self, mock_audiosegment):
|
|
"""Test handling error when loading audio."""
|
|
mock_audiosegment.from_file.side_effect = Exception("Load failed")
|
|
|
|
utils = AudioUtils()
|
|
|
|
with pytest.raises(Exception):
|
|
utils.load_audio("broken.mp3")
|
|
|
|
def test_save_audio_success(self, temp_dir):
|
|
"""Test saving audio successfully."""
|
|
mock_audio = Mock()
|
|
mock_audio.export = Mock()
|
|
|
|
utils = AudioUtils()
|
|
output_path = str(temp_dir / "output.mp3")
|
|
|
|
result = utils.save_audio(mock_audio, output_path)
|
|
|
|
assert result is True
|
|
mock_audio.export.assert_called_once_with(output_path, format="mp3")
|
|
|
|
def test_save_audio_with_format(self, temp_dir):
|
|
"""Test saving audio with specified format."""
|
|
mock_audio = Mock()
|
|
mock_audio.export = Mock()
|
|
|
|
utils = AudioUtils()
|
|
output_path = str(temp_dir / "output.wav")
|
|
|
|
result = utils.save_audio(mock_audio, output_path, format="wav")
|
|
|
|
assert result is True
|
|
mock_audio.export.assert_called_once_with(output_path, format="wav")
|
|
|
|
def test_save_audio_error(self, temp_dir):
|
|
"""Test handling error when saving audio."""
|
|
mock_audio = Mock()
|
|
mock_audio.export.side_effect = Exception("Save failed")
|
|
|
|
utils = AudioUtils()
|
|
output_path = str(temp_dir / "output.mp3")
|
|
|
|
result = utils.save_audio(mock_audio, output_path)
|
|
|
|
assert result is False
|
|
|
|
def test_get_duration_success(self):
|
|
"""Test getting audio duration successfully."""
|
|
utils = AudioUtils()
|
|
|
|
with patch.object(utils, 'get_audio_info', return_value={"duration": 120.5}):
|
|
duration = utils.get_duration("test.mp3")
|
|
assert duration == 120.5
|
|
|
|
def test_get_duration_error(self):
|
|
"""Test handling error when getting duration."""
|
|
utils = AudioUtils()
|
|
|
|
with patch.object(utils, 'get_audio_info', side_effect=Exception("Error")):
|
|
with pytest.raises(Exception):
|
|
utils.get_duration("broken.mp3")
|
|
|
|
def test_apply_censorship_silence(self):
|
|
"""Test applying silence censorship."""
|
|
mock_audio = Mock()
|
|
mock_audio.__getitem__ = Mock(return_value=Mock()) # For slicing
|
|
mock_audio.__add__ = Mock(return_value=mock_audio) # For concatenation
|
|
|
|
# Mock AudioSegment.silent
|
|
with patch('src.core.audio_utils_simple.AudioSegment') as mock_audiosegment:
|
|
mock_audiosegment.silent.return_value = Mock()
|
|
|
|
utils = AudioUtils()
|
|
segments = [(1.0, 2.0), (5.0, 6.0)]
|
|
|
|
result = utils.apply_censorship(mock_audio, segments, "silence")
|
|
|
|
assert result == mock_audio
|
|
|
|
def test_apply_censorship_beep(self):
|
|
"""Test applying beep censorship."""
|
|
mock_audio = Mock()
|
|
mock_audio.frame_rate = 44100
|
|
mock_audio.__getitem__ = Mock(return_value=Mock())
|
|
mock_audio.__add__ = Mock(return_value=mock_audio)
|
|
|
|
# Mock Sine generator
|
|
with patch('src.core.audio_utils_simple.Sine') as mock_sine:
|
|
mock_beep = Mock()
|
|
mock_sine.return_value = mock_beep
|
|
|
|
utils = AudioUtils()
|
|
segments = [(2.0, 3.0)]
|
|
|
|
result = utils.apply_censorship(
|
|
mock_audio,
|
|
segments,
|
|
"beep",
|
|
frequency=800
|
|
)
|
|
|
|
assert result == mock_audio
|
|
mock_sine.assert_called_with(800)
|
|
|
|
def test_apply_censorship_invalid_method(self):
|
|
"""Test applying censorship with invalid method."""
|
|
mock_audio = Mock()
|
|
utils = AudioUtils()
|
|
|
|
with pytest.raises(ValueError, match="Unsupported censorship method"):
|
|
utils.apply_censorship(mock_audio, [(1.0, 2.0)], "invalid_method")
|
|
|
|
def test_normalize_audio_success(self):
|
|
"""Test audio normalization."""
|
|
mock_audio = Mock()
|
|
mock_audio.apply_gain.return_value = mock_audio
|
|
mock_audio.dBFS = -10.0 # Current level
|
|
|
|
utils = AudioUtils()
|
|
target_dbfs = -20.0
|
|
|
|
result = utils.normalize_audio(mock_audio, target_dbfs)
|
|
|
|
# Should apply gain to reach target level
|
|
expected_gain = target_dbfs - (-10.0) # -20 - (-10) = -10
|
|
mock_audio.apply_gain.assert_called_once_with(expected_gain)
|
|
assert result == mock_audio
|
|
|
|
def test_normalize_audio_error(self):
|
|
"""Test handling error during normalization."""
|
|
mock_audio = Mock()
|
|
mock_audio.dBFS = float('-inf') # Causes error
|
|
|
|
utils = AudioUtils()
|
|
|
|
with pytest.raises(ValueError, match="Cannot normalize"):
|
|
utils.normalize_audio(mock_audio, -20.0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
pytest.main([__file__, '-v']) |