Some checks are pending
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / Build Package (push) Blocked by required conditions
Add intelligent analysis and recommendation tools for KiCad designs: ## New AI Tools (kicad_mcp/tools/ai_tools.py) - suggest_components_for_circuit: Smart component suggestions based on circuit analysis - recommend_design_rules: Automated design rule recommendations for different technologies - optimize_pcb_layout: PCB layout optimization for signal integrity, thermal, and cost - analyze_design_completeness: Comprehensive design completeness analysis ## Enhanced Utilities - component_utils.py: Add ComponentType enum and component classification functions - pattern_recognition.py: Enhanced circuit pattern analysis and recommendations - netlist_parser.py: Implement missing parse_netlist_file function for AI tools ## Key Features - Circuit pattern recognition for power supplies, amplifiers, microcontrollers - Technology-specific design rules (standard, HDI, RF, automotive) - Layout optimization suggestions with implementation steps - Component suggestion system with standard values and examples - Design completeness scoring with actionable recommendations ## Server Integration - Register AI tools in FastMCP server - Integrate with existing KiCad utilities and file parsers - Error handling and graceful fallbacks for missing data Fixes ImportError that prevented server startup and enables advanced AI-powered design assistance for KiCad projects. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
414 lines
15 KiB
Python
414 lines
15 KiB
Python
"""
|
|
Tests for the kicad_mcp.utils.kicad_cli module.
|
|
"""
|
|
import platform
|
|
import subprocess
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from kicad_mcp.utils.kicad_cli import (
|
|
KiCadCLIError,
|
|
KiCadCLIManager,
|
|
find_kicad_cli,
|
|
get_cli_manager,
|
|
get_kicad_cli_path,
|
|
get_kicad_version,
|
|
is_kicad_cli_available,
|
|
)
|
|
|
|
|
|
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)
|