"""Unit tests for the enhanced CLI interface.""" import pytest import tempfile import os from pathlib import Path from unittest.mock import Mock, patch, AsyncMock from click.testing import CliRunner from src.cli.enhanced_cli import EnhancedCLI, EnhancedTranscribeCommand, EnhancedBatchCommand from src.services.model_manager import ModelManager from src.services.transcription_service import TranscriptionConfig class TestEnhancedCLI: """Test the enhanced CLI interface structure.""" @pytest.fixture def cli(self): """Create an enhanced CLI instance for testing.""" return EnhancedCLI() @pytest.fixture def runner(self): """Create a Click test runner.""" return CliRunner() def test_cli_initialization(self, cli): """Test that CLI initializes correctly with model manager.""" assert cli.model_manager is not None assert cli.console is not None def test_help_display(self, runner): """Test that help documentation is displayed correctly.""" from src.cli.enhanced_cli import cli result = runner.invoke(cli, ['--help']) assert result.exit_code == 0 assert "Enhanced Audio Transcription Tool" in result.output # Note: The arguments are now under the subcommands, not the main CLI assert "transcribe" in result.output assert "batch" in result.output def test_single_file_transcription_arguments(self, runner): """Test single file transcription argument parsing.""" from src.cli.enhanced_cli import cli with tempfile.NamedTemporaryFile(suffix='.wav') as temp_file: result = runner.invoke(cli, [ 'transcribe', '--help' ]) # Should not fail on argument parsing assert result.exit_code == 0 assert "INPUT" in result.output # Positional argument assert "--output" in result.output assert "--format" in result.output assert "--model" in result.output assert "--device" in result.output assert "--domain" in result.output assert "--diarize" in result.output assert "--speakers" in result.output def test_batch_processing_arguments(self, runner): """Test batch processing argument parsing.""" from src.cli.enhanced_cli import cli result = runner.invoke(cli, [ 'batch', '--help' ]) # Should not fail on argument parsing assert result.exit_code == 0 assert "INPUT" in result.output # Positional argument assert "--output" in result.output assert "--concurrency" in result.output assert "--format" in result.output assert "--model" in result.output assert "--device" in result.output assert "--domain" in result.output assert "--diarize" in result.output assert "--speakers" in result.output def test_invalid_arguments(self, runner): """Test that invalid arguments are properly rejected.""" from src.cli.enhanced_cli import cli # Test invalid format result = runner.invoke(cli, [ 'transcribe', '--input', 'test.wav', '--format', 'invalid_format' ]) assert result.exit_code == 2 # Usage error # Test invalid device result = runner.invoke(cli, [ 'transcribe', '--input', 'test.wav', '--device', 'invalid_device' ]) assert result.exit_code == 2 # Usage error # Test invalid domain result = runner.invoke(cli, [ 'transcribe', '--input', 'test.wav', '--domain', 'invalid_domain' ]) assert result.exit_code == 2 # Usage error def test_model_manager_integration(self, cli): """Test that CLI properly integrates with ModelManager.""" mock_manager = Mock() cli.model_manager = mock_manager # Test that CLI can access model manager methods cli.model_manager.get_available_models.return_value = ['tiny', 'base', 'small', 'medium', 'large'] models = cli.model_manager.get_available_models() assert models == ['tiny', 'base', 'small', 'medium', 'large'] mock_manager.get_available_models.assert_called_once() class TestEnhancedTranscribeCommand: """Test the enhanced transcribe command.""" @pytest.fixture def command(self): """Create an enhanced transcribe command instance.""" return EnhancedTranscribeCommand() @pytest.fixture def runner(self): """Create a Click test runner.""" return CliRunner() @pytest.mark.asyncio async def test_single_file_transcription(self, command, runner): """Test single file transcription execution.""" with patch('src.cli.enhanced_cli.create_transcription_service') as mock_service_factory, \ patch.object(command, '_get_audio_duration') as mock_duration: mock_service = AsyncMock() mock_service_factory.return_value = mock_service mock_service.initialize = AsyncMock() mock_duration.return_value = 60.0 # Mock 60 seconds duration # Mock transcription result mock_result = Mock() mock_result.text_content = "Test transcription result" mock_result.accuracy = 95.5 mock_result.processing_time = 10.5 mock_result.quality_warnings = [] mock_service.transcribe_file.return_value = mock_result with tempfile.NamedTemporaryFile(suffix='.wav') as temp_file: # Create a real file for testing temp_file.write(b'test audio data') temp_file.flush() result = await command.execute_transcription( input_path=temp_file.name, output_dir='/tmp/output', format_type='json', model='base', device='cpu', domain=None, diarize=False, speakers=None ) assert result is not None mock_service.initialize.assert_called_once() mock_service.transcribe_file.assert_called_once() def test_progress_callback_integration(self, command): """Test that progress callback integrates with Rich progress bars.""" with patch('src.cli.enhanced_cli.Progress') as mock_progress: mock_task = Mock() mock_progress.return_value.__enter__.return_value.add_task.return_value = mock_task # Test progress callback creation and execution callback = command._create_progress_callback(mock_task, 100.0) # Verify callback is callable and executes without error assert callable(callback) callback(50.0, 100.0) # Should execute without error def test_export_formats(self, command): """Test export functionality for different formats.""" mock_result = Mock() mock_result.text_content = "Test transcription" mock_result.segments = [ {"start": 0.0, "end": 2.0, "text": "Hello world"}, {"start": 2.0, "end": 4.0, "text": "How are you"} ] with tempfile.TemporaryDirectory() as temp_dir: # Test JSON export json_path = command._export_result(mock_result, "test.wav", temp_dir, "json") assert Path(json_path).exists() assert json_path.endswith('.json') # Test TXT export txt_path = command._export_result(mock_result, "test.wav", temp_dir, "txt") assert Path(txt_path).exists() assert txt_path.endswith('.txt') # Test SRT export srt_path = command._export_result(mock_result, "test.wav", temp_dir, "srt") assert Path(srt_path).exists() assert srt_path.endswith('.srt') # Test VTT export vtt_path = command._export_result(mock_result, "test.wav", temp_dir, "vtt") assert Path(vtt_path).exists() assert vtt_path.endswith('.vtt') class TestEnhancedBatchCommand: """Test the enhanced batch command.""" @pytest.fixture def command(self): """Create an enhanced batch command instance.""" return EnhancedBatchCommand() @pytest.mark.asyncio async def test_batch_processing_setup(self, command): """Test batch processing setup and file discovery.""" with tempfile.TemporaryDirectory() as temp_dir: # Create test files test_files = [ Path(temp_dir) / "small.wav", Path(temp_dir) / "medium.mp3", Path(temp_dir) / "large.m4a" ] for file_path in test_files: file_path.touch() # Set different file sizes for intelligent queuing file_path.write_bytes(b'x' * (test_files.index(file_path) + 1) * 1024) files = command._discover_files(temp_dir) assert len(files) == 3 # Test intelligent queuing (smaller files first) sorted_files = command._sort_files_by_size(files) assert sorted_files[0].stat().st_size <= sorted_files[1].stat().st_size assert sorted_files[1].stat().st_size <= sorted_files[2].stat().st_size @pytest.mark.asyncio async def test_concurrent_processing(self, command): """Test concurrent processing with ThreadPoolExecutor.""" with patch('concurrent.futures.ThreadPoolExecutor') as mock_executor, \ patch('concurrent.futures.as_completed') as mock_as_completed: # Create mock futures that can be iterated mock_future1 = Mock() mock_future2 = Mock() mock_future3 = Mock() mock_executor.return_value.__enter__.return_value.submit.side_effect = [mock_future1, mock_future2, mock_future3] mock_as_completed.return_value = [mock_future1, mock_future2, mock_future3] test_files = ["file1.wav", "file2.wav", "file3.wav"] await command._process_concurrently( files=test_files, concurrency=2, transcription_func=Mock(), progress_callback=Mock() ) # Verify ThreadPoolExecutor was used with correct max_workers mock_executor.assert_called_with(max_workers=2) def test_performance_monitoring(self, command): """Test performance monitoring functionality.""" with patch('src.cli.enhanced_cli.psutil') as mock_psutil: mock_psutil.cpu_percent.return_value = 45.2 mock_psutil.virtual_memory.return_value = Mock( used=2 * 1024**3, # 2GB used total=8 * 1024**3, # 8GB total percent=25.0 ) mock_psutil.sensors_temperatures.return_value = { 'coretemp': [Mock(current=65.0)] } stats = command._get_performance_stats() assert 'cpu_percent' in stats assert 'memory_used_gb' in stats assert 'memory_total_gb' in stats assert 'memory_percent' in stats assert 'cpu_temperature' in stats assert stats['cpu_percent'] == 45.2 assert stats['memory_used_gb'] == 2.0 assert stats['memory_total_gb'] == 8.0 assert stats['memory_percent'] == 25.0 assert stats['cpu_temperature'] == 65.0 class TestErrorHandling: """Test error handling and user guidance.""" @pytest.fixture def cli(self): """Create an enhanced CLI instance for testing.""" return EnhancedCLI() def test_file_not_found_error_handling(self, cli): """Test handling of FileNotFoundError.""" error = FileNotFoundError("No such file or directory: 'nonexistent.wav'") guidance = cli._get_error_guidance(type(error).__name__, str(error)) assert "Check that the input file path is correct" in guidance assert "file exists" in guidance def test_permission_error_handling(self, cli): """Test handling of PermissionError.""" error = PermissionError("Permission denied: 'protected.wav'") guidance = cli._get_error_guidance(type(error).__name__, str(error)) assert "Check file permissions" in guidance assert "administrator privileges" in guidance def test_cuda_error_handling(self, cli): """Test handling of CUDA/GPU errors.""" error = RuntimeError("CUDA out of memory") guidance = cli._get_error_guidance(type(error).__name__, str(error)) assert "GPU-related error" in guidance assert "--device cpu" in guidance def test_memory_error_handling(self, cli): """Test handling of memory errors.""" error = MemoryError("Not enough memory") guidance = cli._get_error_guidance(type(error).__name__, str(error)) assert "Memory error" in guidance assert "--model small" in guidance assert "reduce concurrency" in guidance def test_generic_error_handling(self, cli): """Test handling of generic errors.""" error = ValueError("Invalid parameter") guidance = cli._get_error_guidance(type(error).__name__, str(error)) assert "Check input parameters" in guidance assert "try again" in guidance class TestIntegration: """Integration tests for the enhanced CLI.""" @pytest.mark.asyncio async def test_full_transcription_workflow(self): """Test complete transcription workflow integration.""" cli = EnhancedCLI() command = EnhancedTranscribeCommand() with patch('src.cli.enhanced_cli.ModelManager') as mock_manager_class: mock_manager = Mock() mock_manager_class.return_value = mock_manager with patch('src.cli.enhanced_cli.create_transcription_service') as mock_service_factory, \ patch.object(command, '_get_audio_duration') as mock_duration: mock_service = AsyncMock() mock_service_factory.return_value = mock_service mock_service.initialize = AsyncMock() mock_duration.return_value = 60.0 # Mock 60 seconds duration # Mock successful transcription mock_result = Mock() mock_result.text_content = "Test transcription" mock_result.accuracy = 95.0 mock_result.processing_time = 5.0 mock_service.transcribe_file.return_value = mock_result with tempfile.NamedTemporaryFile(suffix='.wav') as temp_file: # Create a real file for testing temp_file.write(b'test audio data') temp_file.flush() with tempfile.TemporaryDirectory() as output_dir: result = await command.execute_transcription( input_path=temp_file.name, output_dir=output_dir, format_type='json', model='base', device='cpu' ) assert result is not None mock_service.transcribe_file.assert_called_once() def test_cli_command_registration(self): """Test that CLI commands are properly registered.""" from src.cli.enhanced_cli import cli runner = CliRunner() # Test that commands are available result = runner.invoke(cli, ['--help']) assert result.exit_code == 0 # Test transcribe command result = runner.invoke(cli, ['transcribe', '--help']) assert result.exit_code == 0 assert "transcribe" in result.output.lower() # Test batch command result = runner.invoke(cli, ['batch', '--help']) assert result.exit_code == 0 assert "batch" in result.output.lower() if __name__ == "__main__": pytest.main([__file__])