- Implement 3D model analysis and mechanical constraints checking - Add advanced DRC rule customization for HDI, RF, and automotive applications - Create symbol library management with analysis and validation tools - Implement PCB layer stack-up analysis with impedance calculations - Fix Context parameter validation errors causing client failures - Add enhanced tool annotations with examples for better LLM compatibility - Include comprehensive test coverage improvements (22.21% coverage) - Add CLAUDE.md documentation for development guidance New Advanced Tools: • 3D model analysis: analyze_3d_models, check_mechanical_constraints • Advanced DRC: create_drc_rule_set, analyze_pcb_drc_violations • Symbol management: analyze_symbol_library, validate_symbol_library • Layer analysis: analyze_pcb_stackup, calculate_trace_impedance 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
228 lines
9.3 KiB
Python
228 lines
9.3 KiB
Python
"""
|
|
Tests for the kicad_mcp.config module.
|
|
"""
|
|
import os
|
|
import platform
|
|
from unittest.mock import patch, MagicMock
|
|
import pytest
|
|
|
|
|
|
class TestConfigModule:
|
|
"""Test config module constants and platform-specific behavior."""
|
|
|
|
def test_system_detection(self):
|
|
"""Test that system is properly detected."""
|
|
from kicad_mcp.config import system
|
|
|
|
assert system in ['Darwin', 'Windows', 'Linux'] or isinstance(system, str)
|
|
assert system == platform.system()
|
|
|
|
def test_macos_paths(self):
|
|
"""Test macOS-specific path configuration."""
|
|
with patch('platform.system', return_value='Darwin'):
|
|
# Need to reload the config module after patching
|
|
import importlib
|
|
import kicad_mcp.config
|
|
importlib.reload(kicad_mcp.config)
|
|
|
|
from kicad_mcp.config import KICAD_USER_DIR, KICAD_APP_PATH, KICAD_PYTHON_BASE
|
|
|
|
assert KICAD_USER_DIR == os.path.expanduser("~/Documents/KiCad")
|
|
assert KICAD_APP_PATH == "/Applications/KiCad/KiCad.app"
|
|
assert "Contents/Frameworks/Python.framework" in KICAD_PYTHON_BASE
|
|
|
|
def test_windows_paths(self):
|
|
"""Test Windows-specific path configuration."""
|
|
with patch('platform.system', return_value='Windows'):
|
|
import importlib
|
|
import kicad_mcp.config
|
|
importlib.reload(kicad_mcp.config)
|
|
|
|
from kicad_mcp.config import KICAD_USER_DIR, KICAD_APP_PATH, KICAD_PYTHON_BASE
|
|
|
|
assert KICAD_USER_DIR == os.path.expanduser("~/Documents/KiCad")
|
|
assert KICAD_APP_PATH == r"C:\Program Files\KiCad"
|
|
assert KICAD_PYTHON_BASE == ""
|
|
|
|
def test_linux_paths(self):
|
|
"""Test Linux-specific path configuration."""
|
|
with patch('platform.system', return_value='Linux'):
|
|
import importlib
|
|
import kicad_mcp.config
|
|
importlib.reload(kicad_mcp.config)
|
|
|
|
from kicad_mcp.config import KICAD_USER_DIR, KICAD_APP_PATH, KICAD_PYTHON_BASE
|
|
|
|
assert KICAD_USER_DIR == os.path.expanduser("~/KiCad")
|
|
assert KICAD_APP_PATH == "/usr/share/kicad"
|
|
assert KICAD_PYTHON_BASE == ""
|
|
|
|
def test_unknown_system_defaults_to_macos(self):
|
|
"""Test that unknown systems default to macOS paths."""
|
|
with patch('platform.system', return_value='FreeBSD'):
|
|
import importlib
|
|
import kicad_mcp.config
|
|
importlib.reload(kicad_mcp.config)
|
|
|
|
from kicad_mcp.config import KICAD_USER_DIR, KICAD_APP_PATH
|
|
|
|
assert KICAD_USER_DIR == os.path.expanduser("~/Documents/KiCad")
|
|
assert KICAD_APP_PATH == "/Applications/KiCad/KiCad.app"
|
|
|
|
def test_kicad_extensions(self):
|
|
"""Test KiCad file extension mappings."""
|
|
from kicad_mcp.config import KICAD_EXTENSIONS
|
|
|
|
expected_keys = ["project", "pcb", "schematic", "design_rules",
|
|
"worksheet", "footprint", "netlist", "kibot_config"]
|
|
|
|
for key in expected_keys:
|
|
assert key in KICAD_EXTENSIONS
|
|
assert isinstance(KICAD_EXTENSIONS[key], str)
|
|
assert KICAD_EXTENSIONS[key].startswith(('.', '_'))
|
|
|
|
def test_data_extensions(self):
|
|
"""Test data file extensions list."""
|
|
from kicad_mcp.config import DATA_EXTENSIONS
|
|
|
|
assert isinstance(DATA_EXTENSIONS, list)
|
|
assert len(DATA_EXTENSIONS) > 0
|
|
|
|
expected_extensions = [".csv", ".pos", ".net", ".zip", ".drl"]
|
|
for ext in expected_extensions:
|
|
assert ext in DATA_EXTENSIONS
|
|
|
|
def test_circuit_defaults(self):
|
|
"""Test circuit default parameters."""
|
|
from kicad_mcp.config import CIRCUIT_DEFAULTS
|
|
|
|
required_keys = ["grid_spacing", "component_spacing", "wire_width",
|
|
"text_size", "pin_length"]
|
|
|
|
for key in required_keys:
|
|
assert key in CIRCUIT_DEFAULTS
|
|
|
|
# Test specific types
|
|
assert isinstance(CIRCUIT_DEFAULTS["text_size"], list)
|
|
assert len(CIRCUIT_DEFAULTS["text_size"]) == 2
|
|
assert all(isinstance(x, (int, float)) for x in CIRCUIT_DEFAULTS["text_size"])
|
|
|
|
def test_common_libraries_structure(self):
|
|
"""Test common libraries configuration structure."""
|
|
from kicad_mcp.config import COMMON_LIBRARIES
|
|
|
|
expected_categories = ["basic", "power", "connectors"]
|
|
|
|
for category in expected_categories:
|
|
assert category in COMMON_LIBRARIES
|
|
assert isinstance(COMMON_LIBRARIES[category], dict)
|
|
|
|
for component, info in COMMON_LIBRARIES[category].items():
|
|
assert "library" in info
|
|
assert "symbol" in info
|
|
assert isinstance(info["library"], str)
|
|
assert isinstance(info["symbol"], str)
|
|
|
|
def test_default_footprints_structure(self):
|
|
"""Test default footprints configuration structure."""
|
|
from kicad_mcp.config import DEFAULT_FOOTPRINTS
|
|
|
|
# Test that at least some common components are present
|
|
common_components = ["R", "C", "LED", "D"]
|
|
|
|
for component in common_components:
|
|
assert component in DEFAULT_FOOTPRINTS
|
|
assert isinstance(DEFAULT_FOOTPRINTS[component], list)
|
|
assert len(DEFAULT_FOOTPRINTS[component]) > 0
|
|
|
|
# All footprints should be strings
|
|
for footprint in DEFAULT_FOOTPRINTS[component]:
|
|
assert isinstance(footprint, str)
|
|
assert ":" in footprint # Should be in format "Library:Footprint"
|
|
|
|
def test_timeout_constants(self):
|
|
"""Test timeout constants are reasonable values."""
|
|
from kicad_mcp.config import TIMEOUT_CONSTANTS
|
|
|
|
required_keys = ["kicad_cli_version_check", "kicad_cli_export",
|
|
"application_open", "subprocess_default"]
|
|
|
|
for key in required_keys:
|
|
assert key in TIMEOUT_CONSTANTS
|
|
timeout = TIMEOUT_CONSTANTS[key]
|
|
assert isinstance(timeout, (int, float))
|
|
assert 0 < timeout <= 300 # Reasonable timeout range
|
|
|
|
def test_progress_constants(self):
|
|
"""Test progress constants are valid percentages."""
|
|
from kicad_mcp.config import PROGRESS_CONSTANTS
|
|
|
|
required_keys = ["start", "detection", "setup", "processing",
|
|
"finishing", "validation", "complete"]
|
|
|
|
for key in required_keys:
|
|
assert key in PROGRESS_CONSTANTS
|
|
progress = PROGRESS_CONSTANTS[key]
|
|
assert isinstance(progress, int)
|
|
assert 0 <= progress <= 100
|
|
|
|
def test_display_constants(self):
|
|
"""Test display constants are reasonable values."""
|
|
from kicad_mcp.config import DISPLAY_CONSTANTS
|
|
|
|
assert "bom_preview_limit" in DISPLAY_CONSTANTS
|
|
limit = DISPLAY_CONSTANTS["bom_preview_limit"]
|
|
assert isinstance(limit, int)
|
|
assert limit > 0
|
|
|
|
def test_empty_search_paths_environment(self):
|
|
"""Test behavior with empty KICAD_SEARCH_PATHS."""
|
|
with patch.dict(os.environ, {"KICAD_SEARCH_PATHS": ""}):
|
|
import importlib
|
|
import kicad_mcp.config
|
|
importlib.reload(kicad_mcp.config)
|
|
|
|
# Should still have default locations if they exist
|
|
from kicad_mcp.config import ADDITIONAL_SEARCH_PATHS
|
|
assert isinstance(ADDITIONAL_SEARCH_PATHS, list)
|
|
|
|
def test_nonexistent_search_paths_ignored(self):
|
|
"""Test that nonexistent search paths are ignored."""
|
|
with patch.dict(os.environ, {"KICAD_SEARCH_PATHS": "/nonexistent/path1,/nonexistent/path2"}), \
|
|
patch('os.path.exists', return_value=False):
|
|
import importlib
|
|
import kicad_mcp.config
|
|
importlib.reload(kicad_mcp.config)
|
|
|
|
from kicad_mcp.config import ADDITIONAL_SEARCH_PATHS
|
|
|
|
# Should not contain the nonexistent paths
|
|
assert "/nonexistent/path1" not in ADDITIONAL_SEARCH_PATHS
|
|
assert "/nonexistent/path2" not in ADDITIONAL_SEARCH_PATHS
|
|
|
|
def test_search_paths_expansion_and_trimming(self):
|
|
"""Test that search paths are expanded and trimmed."""
|
|
with patch.dict(os.environ, {"KICAD_SEARCH_PATHS": "~/test_path1, ~/test_path2 "}), \
|
|
patch('os.path.exists', return_value=True), \
|
|
patch('os.path.expanduser', side_effect=lambda x: x.replace("~", "/home/user")):
|
|
|
|
import importlib
|
|
import kicad_mcp.config
|
|
importlib.reload(kicad_mcp.config)
|
|
|
|
from kicad_mcp.config import ADDITIONAL_SEARCH_PATHS
|
|
|
|
# Should contain expanded paths
|
|
assert "/home/user/test_path1" in ADDITIONAL_SEARCH_PATHS
|
|
assert "/home/user/test_path2" in ADDITIONAL_SEARCH_PATHS
|
|
|
|
def test_default_project_locations_expanded(self):
|
|
"""Test that default project locations are properly expanded."""
|
|
from kicad_mcp.config import DEFAULT_PROJECT_LOCATIONS
|
|
|
|
assert isinstance(DEFAULT_PROJECT_LOCATIONS, list)
|
|
assert len(DEFAULT_PROJECT_LOCATIONS) > 0
|
|
|
|
# All should start with ~/
|
|
for location in DEFAULT_PROJECT_LOCATIONS:
|
|
assert location.startswith("~/") |