""" Tests for the kicad_mcp.utils.kicad_cli module. """ import os import platform import subprocess from unittest.mock import Mock, patch, MagicMock import pytest from kicad_mcp.utils.kicad_cli import ( KiCadCLIError, KiCadCLIManager, get_cli_manager, find_kicad_cli, get_kicad_cli_path, is_kicad_cli_available, get_kicad_version ) class TestKiCadCLIError: """Test KiCadCLIError exception.""" def test_exception_creation(self): """Test that KiCadCLIError can be created and raised.""" with pytest.raises(KiCadCLIError) as exc_info: raise KiCadCLIError("Test error message") assert str(exc_info.value) == "Test error message" class TestKiCadCLIManager: """Test KiCadCLIManager class.""" def setup_method(self): """Set up test instance.""" self.manager = KiCadCLIManager() def test_init(self): """Test manager initialization.""" manager = KiCadCLIManager() assert manager._cached_cli_path is None assert manager._cache_validated is False assert manager._system == platform.system() @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager._detect_cli_path') @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager._validate_cli_path') def test_find_kicad_cli_success(self, mock_validate, mock_detect): """Test successful CLI detection.""" mock_detect.return_value = "/usr/bin/kicad-cli" mock_validate.return_value = True result = self.manager.find_kicad_cli() assert result == "/usr/bin/kicad-cli" assert self.manager._cached_cli_path == "/usr/bin/kicad-cli" assert self.manager._cache_validated is True @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager._detect_cli_path') def test_find_kicad_cli_not_found(self, mock_detect): """Test CLI detection failure.""" mock_detect.return_value = None result = self.manager.find_kicad_cli() assert result is None assert self.manager._cached_cli_path is None assert self.manager._cache_validated is False @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager._detect_cli_path') @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager._validate_cli_path') def test_find_kicad_cli_validation_failure(self, mock_validate, mock_detect): """Test CLI detection with validation failure.""" mock_detect.return_value = "/usr/bin/kicad-cli" mock_validate.return_value = False result = self.manager.find_kicad_cli() assert result is None assert self.manager._cached_cli_path is None assert self.manager._cache_validated is False def test_find_kicad_cli_cached(self): """Test that cached CLI path is returned.""" self.manager._cached_cli_path = "/cached/path" self.manager._cache_validated = True with patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager._detect_cli_path') as mock_detect: result = self.manager.find_kicad_cli() assert result == "/cached/path" mock_detect.assert_not_called() def test_find_kicad_cli_force_refresh(self): """Test force refresh ignores cache.""" self.manager._cached_cli_path = "/cached/path" self.manager._cache_validated = True with patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager._detect_cli_path') as mock_detect, \ patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager._validate_cli_path') as mock_validate: mock_detect.return_value = "/new/path" mock_validate.return_value = True result = self.manager.find_kicad_cli(force_refresh=True) assert result == "/new/path" mock_detect.assert_called_once() @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager.find_kicad_cli') def test_get_cli_path_success(self, mock_find): """Test successful CLI path retrieval.""" mock_find.return_value = "/usr/bin/kicad-cli" result = self.manager.get_cli_path() assert result == "/usr/bin/kicad-cli" @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager.find_kicad_cli') def test_get_cli_path_not_required(self, mock_find): """Test CLI path retrieval when not required.""" mock_find.return_value = None result = self.manager.get_cli_path(required=False) assert result is None @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager.find_kicad_cli') def test_get_cli_path_required_raises(self, mock_find): """Test that exception is raised when CLI required but not found.""" mock_find.return_value = None with pytest.raises(KiCadCLIError) as exc_info: self.manager.get_cli_path(required=True) assert "KiCad CLI not found" in str(exc_info.value) @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager.find_kicad_cli') def test_is_available_true(self, mock_find): """Test is_available returns True when CLI found.""" mock_find.return_value = "/usr/bin/kicad-cli" assert self.manager.is_available() is True @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager.find_kicad_cli') def test_is_available_false(self, mock_find): """Test is_available returns False when CLI not found.""" mock_find.return_value = None assert self.manager.is_available() is False @patch('kicad_mcp.utils.kicad_cli.subprocess.run') @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager.find_kicad_cli') def test_get_version_success(self, mock_find, mock_run): """Test successful version retrieval.""" mock_find.return_value = "/usr/bin/kicad-cli" mock_result = Mock() mock_result.returncode = 0 mock_result.stdout = "KiCad 7.0.0\n" mock_run.return_value = mock_result version = self.manager.get_version() assert version == "KiCad 7.0.0" mock_run.assert_called_once() @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager.find_kicad_cli') def test_get_version_cli_not_found(self, mock_find): """Test version retrieval when CLI not found.""" mock_find.return_value = None version = self.manager.get_version() assert version is None @patch('kicad_mcp.utils.kicad_cli.subprocess.run') @patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager.find_kicad_cli') def test_get_version_subprocess_error(self, mock_find, mock_run): """Test version retrieval with subprocess error.""" mock_find.return_value = "/usr/bin/kicad-cli" mock_run.side_effect = subprocess.SubprocessError("Test error") version = self.manager.get_version() assert version is None @patch('kicad_mcp.utils.kicad_cli.os.environ.get') @patch('kicad_mcp.utils.kicad_cli.os.path.isfile') @patch('kicad_mcp.utils.kicad_cli.os.access') def test_detect_cli_path_environment_variable(self, mock_access, mock_isfile, mock_env_get): """Test CLI detection from environment variable.""" mock_env_get.return_value = "/custom/kicad-cli" mock_isfile.return_value = True mock_access.return_value = True result = self.manager._detect_cli_path() assert result == "/custom/kicad-cli" @patch('kicad_mcp.utils.kicad_cli.os.environ.get') @patch('kicad_mcp.utils.kicad_cli.shutil.which') def test_detect_cli_path_system_path(self, mock_which, mock_env_get): """Test CLI detection from system PATH.""" mock_env_get.return_value = None mock_which.return_value = "/usr/bin/kicad-cli" result = self.manager._detect_cli_path() assert result == "/usr/bin/kicad-cli" @patch('kicad_mcp.utils.kicad_cli.os.environ.get') @patch('kicad_mcp.utils.kicad_cli.shutil.which') @patch('kicad_mcp.utils.kicad_cli.os.path.isfile') @patch('kicad_mcp.utils.kicad_cli.os.access') def test_detect_cli_path_common_locations(self, mock_access, mock_isfile, mock_which, mock_env_get): """Test CLI detection from common installation paths.""" mock_env_get.return_value = None mock_which.return_value = None mock_isfile.side_effect = lambda x: x == "/usr/local/bin/kicad-cli" mock_access.return_value = True result = self.manager._detect_cli_path() assert result == "/usr/local/bin/kicad-cli" def test_get_cli_executable_name_windows(self): """Test CLI executable name on Windows.""" with patch('platform.system', return_value='Windows'): manager = KiCadCLIManager() name = manager._get_cli_executable_name() assert name == "kicad-cli.exe" def test_get_cli_executable_name_unix(self): """Test CLI executable name on Unix-like systems.""" with patch('platform.system', return_value='Linux'): manager = KiCadCLIManager() name = manager._get_cli_executable_name() assert name == "kicad-cli" def test_get_common_installation_paths_macos(self): """Test common installation paths on macOS.""" with patch('platform.system', return_value='Darwin'): manager = KiCadCLIManager() paths = manager._get_common_installation_paths() assert "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" in paths assert "/opt/homebrew/bin/kicad-cli" in paths def test_get_common_installation_paths_windows(self): """Test common installation paths on Windows.""" with patch('platform.system', return_value='Windows'): manager = KiCadCLIManager() paths = manager._get_common_installation_paths() assert r"C:\Program Files\KiCad\bin\kicad-cli.exe" in paths assert r"C:\Program Files (x86)\KiCad\bin\kicad-cli.exe" in paths def test_get_common_installation_paths_linux(self): """Test common installation paths on Linux.""" with patch('platform.system', return_value='Linux'): manager = KiCadCLIManager() paths = manager._get_common_installation_paths() assert "/usr/bin/kicad-cli" in paths assert "/snap/kicad/current/usr/bin/kicad-cli" in paths @patch('kicad_mcp.utils.kicad_cli.subprocess.run') def test_validate_cli_path_success(self, mock_run): """Test successful CLI validation.""" mock_result = Mock() mock_result.returncode = 0 mock_run.return_value = mock_result result = self.manager._validate_cli_path("/usr/bin/kicad-cli") assert result is True @patch('kicad_mcp.utils.kicad_cli.subprocess.run') def test_validate_cli_path_failure(self, mock_run): """Test CLI validation failure.""" mock_result = Mock() mock_result.returncode = 1 mock_run.return_value = mock_result result = self.manager._validate_cli_path("/usr/bin/kicad-cli") assert result is False @patch('kicad_mcp.utils.kicad_cli.subprocess.run') def test_validate_cli_path_exception(self, mock_run): """Test CLI validation with exception.""" mock_run.side_effect = subprocess.SubprocessError("Test error") result = self.manager._validate_cli_path("/usr/bin/kicad-cli") assert result is False class TestGlobalFunctions: """Test global convenience functions.""" def setup_method(self): """Reset global manager before each test.""" import kicad_mcp.utils.kicad_cli kicad_mcp.utils.kicad_cli._cli_manager = None def test_get_cli_manager_singleton(self): """Test that get_cli_manager returns singleton instance.""" manager1 = get_cli_manager() manager2 = get_cli_manager() assert manager1 is manager2 assert isinstance(manager1, KiCadCLIManager) @patch('kicad_mcp.utils.kicad_cli.get_cli_manager') def test_find_kicad_cli_convenience(self, mock_get_manager): """Test find_kicad_cli convenience function.""" mock_manager = Mock() mock_manager.find_kicad_cli.return_value = "/usr/bin/kicad-cli" mock_get_manager.return_value = mock_manager result = find_kicad_cli(force_refresh=True) assert result == "/usr/bin/kicad-cli" mock_manager.find_kicad_cli.assert_called_once_with(True) @patch('kicad_mcp.utils.kicad_cli.get_cli_manager') def test_get_kicad_cli_path_convenience(self, mock_get_manager): """Test get_kicad_cli_path convenience function.""" mock_manager = Mock() mock_manager.get_cli_path.return_value = "/usr/bin/kicad-cli" mock_get_manager.return_value = mock_manager result = get_kicad_cli_path(required=False) assert result == "/usr/bin/kicad-cli" mock_manager.get_cli_path.assert_called_once_with(False) @patch('kicad_mcp.utils.kicad_cli.get_cli_manager') def test_is_kicad_cli_available_convenience(self, mock_get_manager): """Test is_kicad_cli_available convenience function.""" mock_manager = Mock() mock_manager.is_available.return_value = True mock_get_manager.return_value = mock_manager result = is_kicad_cli_available() assert result is True mock_manager.is_available.assert_called_once() @patch('kicad_mcp.utils.kicad_cli.get_cli_manager') def test_get_kicad_version_convenience(self, mock_get_manager): """Test get_kicad_version convenience function.""" mock_manager = Mock() mock_manager.get_version.return_value = "KiCad 7.0.0" mock_get_manager.return_value = mock_manager result = get_kicad_version() assert result == "KiCad 7.0.0" mock_manager.get_version.assert_called_once() class TestIntegration: """Integration tests for KiCad CLI functionality.""" def test_manager_lifecycle(self): """Test complete manager lifecycle.""" manager = KiCadCLIManager() # Initial state assert manager._cached_cli_path is None assert not manager._cache_validated # Simulate finding CLI with patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager._detect_cli_path') as mock_detect, \ patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager._validate_cli_path') as mock_validate: mock_detect.return_value = "/test/kicad-cli" mock_validate.return_value = True # First call should detect and cache path1 = manager.find_kicad_cli() assert path1 == "/test/kicad-cli" assert manager._cached_cli_path == "/test/kicad-cli" assert manager._cache_validated # Second call should use cache path2 = manager.find_kicad_cli() assert path2 == "/test/kicad-cli" assert mock_detect.call_count == 1 # Should only be called once # Force refresh should re-detect mock_detect.return_value = "/new/path" path3 = manager.find_kicad_cli(force_refresh=True) assert path3 == "/new/path" assert mock_detect.call_count == 2 def test_error_propagation(self): """Test that errors are properly propagated.""" manager = KiCadCLIManager() with patch('kicad_mcp.utils.kicad_cli.KiCadCLIManager.find_kicad_cli') as mock_find: mock_find.return_value = None # Should not raise when required=False result = manager.get_cli_path(required=False) assert result is None # Should raise when required=True with pytest.raises(KiCadCLIError): manager.get_cli_path(required=True)