clean-tracks/tests/unit/test_audio_utils.py

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'])