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