""" 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