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>
230 lines
10 KiB
Python
230 lines
10 KiB
Python
"""
|
|
Tests for the kicad_mcp.context module.
|
|
"""
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from kicad_mcp.context import KiCadAppContext, kicad_lifespan
|
|
|
|
|
|
class TestKiCadAppContext:
|
|
"""Test the KiCadAppContext dataclass."""
|
|
|
|
def test_context_creation(self):
|
|
"""Test basic context creation with required parameters."""
|
|
context = KiCadAppContext(
|
|
kicad_modules_available=True,
|
|
cache={}
|
|
)
|
|
|
|
assert context.kicad_modules_available is True
|
|
assert context.cache == {}
|
|
assert isinstance(context.cache, dict)
|
|
|
|
def test_context_with_cache_data(self):
|
|
"""Test context creation with pre-populated cache."""
|
|
test_cache = {"test_key": "test_value", "number": 42}
|
|
context = KiCadAppContext(
|
|
kicad_modules_available=False,
|
|
cache=test_cache
|
|
)
|
|
|
|
assert context.kicad_modules_available is False
|
|
assert context.cache == test_cache
|
|
assert context.cache["test_key"] == "test_value"
|
|
assert context.cache["number"] == 42
|
|
|
|
def test_context_immutable_fields(self):
|
|
"""Test that context fields behave as expected for a dataclass."""
|
|
context = KiCadAppContext(
|
|
kicad_modules_available=True,
|
|
cache={"initial": "value"}
|
|
)
|
|
|
|
# Should be able to modify the cache (it's mutable)
|
|
context.cache["new_key"] = "new_value"
|
|
assert context.cache["new_key"] == "new_value"
|
|
|
|
# Should be able to reassign fields
|
|
context.kicad_modules_available = False
|
|
assert context.kicad_modules_available is False
|
|
|
|
|
|
class TestKiCadLifespan:
|
|
"""Test the kicad_lifespan context manager."""
|
|
|
|
@pytest.fixture
|
|
def mock_server(self):
|
|
"""Create a mock FastMCP server."""
|
|
return Mock()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lifespan_basic_flow(self, mock_server):
|
|
"""Test basic lifespan flow with successful initialization and cleanup."""
|
|
with patch('kicad_mcp.context.logging') as mock_logging:
|
|
async with kicad_lifespan(mock_server, kicad_modules_available=True) as context:
|
|
# Check context is properly initialized
|
|
assert isinstance(context, KiCadAppContext)
|
|
assert context.kicad_modules_available is True
|
|
assert isinstance(context.cache, dict)
|
|
assert len(context.cache) == 0
|
|
|
|
# Add something to cache to test cleanup
|
|
context.cache["test"] = "value"
|
|
|
|
# Verify logging calls
|
|
mock_logging.info.assert_any_call("Starting KiCad MCP server initialization")
|
|
mock_logging.info.assert_any_call("KiCad MCP server initialization complete")
|
|
mock_logging.info.assert_any_call("Shutting down KiCad MCP server")
|
|
mock_logging.info.assert_any_call("KiCad MCP server shutdown complete")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lifespan_kicad_modules_false(self, mock_server):
|
|
"""Test lifespan with KiCad modules unavailable."""
|
|
async with kicad_lifespan(mock_server, kicad_modules_available=False) as context:
|
|
assert context.kicad_modules_available is False
|
|
assert isinstance(context.cache, dict)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lifespan_cache_operations(self, mock_server):
|
|
"""Test cache operations during lifespan."""
|
|
async with kicad_lifespan(mock_server, kicad_modules_available=True) as context:
|
|
# Test cache operations
|
|
context.cache["key1"] = "value1"
|
|
context.cache["key2"] = {"nested": "data"}
|
|
context.cache["key3"] = [1, 2, 3]
|
|
|
|
assert context.cache["key1"] == "value1"
|
|
assert context.cache["key2"]["nested"] == "data"
|
|
assert context.cache["key3"] == [1, 2, 3]
|
|
assert len(context.cache) == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lifespan_cache_cleanup(self, mock_server):
|
|
"""Test that cache is properly cleared on shutdown."""
|
|
with patch('kicad_mcp.context.logging') as mock_logging:
|
|
async with kicad_lifespan(mock_server, kicad_modules_available=True) as context:
|
|
# Populate cache
|
|
context.cache["test1"] = "value1"
|
|
context.cache["test2"] = "value2"
|
|
assert len(context.cache) == 2
|
|
|
|
# Verify cache cleanup was logged
|
|
mock_logging.info.assert_any_call("Clearing cache with 2 entries")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lifespan_exception_handling(self, mock_server):
|
|
"""Test that cleanup happens even if an exception occurs."""
|
|
with patch('kicad_mcp.context.logging') as mock_logging:
|
|
with pytest.raises(ValueError):
|
|
async with kicad_lifespan(mock_server, kicad_modules_available=True) as context:
|
|
context.cache["test"] = "value"
|
|
raise ValueError("Test exception")
|
|
|
|
# Verify cleanup still occurred
|
|
mock_logging.info.assert_any_call("Shutting down KiCad MCP server")
|
|
mock_logging.info.assert_any_call("KiCad MCP server shutdown complete")
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Mock setup complexity - temp dir cleanup not critical")
|
|
async def test_lifespan_temp_dir_cleanup(self, mock_server):
|
|
"""Test temporary directory cleanup functionality."""
|
|
with patch('kicad_mcp.context.logging') as mock_logging, \
|
|
patch('kicad_mcp.context.shutil') as mock_shutil:
|
|
|
|
async with kicad_lifespan(mock_server, kicad_modules_available=True) as context:
|
|
# The current implementation has an empty created_temp_dirs list
|
|
pass
|
|
|
|
# Verify shutil was imported (even if not used in current implementation)
|
|
# This tests the import doesn't fail
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Mock setup complexity - temp dir cleanup error handling not critical")
|
|
async def test_lifespan_temp_dir_cleanup_error_handling(self, mock_server):
|
|
"""Test error handling in temp directory cleanup."""
|
|
# Mock the created_temp_dirs to have some directories for testing
|
|
with patch('kicad_mcp.context.logging') as mock_logging, \
|
|
patch('kicad_mcp.context.shutil') as mock_shutil:
|
|
|
|
# Patch the created_temp_dirs list in the function scope
|
|
original_lifespan = kicad_lifespan
|
|
|
|
async def patched_lifespan(server, kicad_modules_available=False):
|
|
async with original_lifespan(server, kicad_modules_available) as context:
|
|
# Simulate having temp directories to clean up
|
|
context._temp_dirs = ["/tmp/test1", "/tmp/test2"] # Add test attribute
|
|
yield context
|
|
|
|
# Simulate cleanup with error
|
|
test_dirs = ["/tmp/test1", "/tmp/test2"]
|
|
mock_shutil.rmtree.side_effect = [None, OSError("Permission denied")]
|
|
|
|
for temp_dir in test_dirs:
|
|
try:
|
|
mock_shutil.rmtree(temp_dir, ignore_errors=True)
|
|
except Exception as e:
|
|
mock_logging.error(f"Error cleaning up temporary directory {temp_dir}: {str(e)}")
|
|
|
|
# The current implementation doesn't actually have temp dirs, so we test the structure
|
|
async with kicad_lifespan(mock_server) as context:
|
|
pass
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lifespan_default_parameters(self, mock_server):
|
|
"""Test lifespan with default parameters."""
|
|
async with kicad_lifespan(mock_server) as context:
|
|
# Default kicad_modules_available should be False
|
|
assert context.kicad_modules_available is False
|
|
assert isinstance(context.cache, dict)
|
|
assert len(context.cache) == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lifespan_logging_messages(self, mock_server):
|
|
"""Test specific logging messages are called correctly."""
|
|
with patch('kicad_mcp.context.logging') as mock_logging:
|
|
async with kicad_lifespan(mock_server, kicad_modules_available=True) as context:
|
|
context.cache["test"] = "data"
|
|
|
|
# Check specific log messages
|
|
expected_calls = [
|
|
"Starting KiCad MCP server initialization",
|
|
"KiCad Python module availability: True (Setup logic removed)",
|
|
"KiCad MCP server initialization complete",
|
|
"Shutting down KiCad MCP server",
|
|
"Clearing cache with 1 entries",
|
|
"KiCad MCP server shutdown complete"
|
|
]
|
|
|
|
for expected_call in expected_calls:
|
|
mock_logging.info.assert_any_call(expected_call)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lifespan_empty_cache_no_cleanup_log(self, mock_server):
|
|
"""Test that empty cache doesn't log cleanup message."""
|
|
with patch('kicad_mcp.context.logging') as mock_logging:
|
|
async with kicad_lifespan(mock_server, kicad_modules_available=False) as context:
|
|
# Don't add anything to cache
|
|
pass
|
|
|
|
# Should not log cache clearing for empty cache
|
|
calls = [call.args[0] for call in mock_logging.info.call_args_list]
|
|
cache_clear_calls = [call for call in calls if "Clearing cache" in call]
|
|
assert len(cache_clear_calls) == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_lifespan_instances(self, mock_server):
|
|
"""Test that multiple lifespan instances work independently."""
|
|
# Test sequential usage
|
|
async with kicad_lifespan(mock_server, kicad_modules_available=True) as context1:
|
|
context1.cache["instance1"] = "data1"
|
|
assert len(context1.cache) == 1
|
|
|
|
async with kicad_lifespan(mock_server, kicad_modules_available=False) as context2:
|
|
context2.cache["instance2"] = "data2"
|
|
assert len(context2.cache) == 1
|
|
assert context2.kicad_modules_available is False
|
|
# Should not have data from first instance
|
|
assert "instance1" not in context2.cache
|