Skip to main content
Glama
test_integration.py21.2 kB
""" Integration tests for MCP FreeCAD Server. These tests verify that all components work together correctly and simulate real-world usage scenarios. """ import asyncio from typing import Any, Dict, List from unittest.mock import AsyncMock, MagicMock, patch import pytest # Test imports (these will need to be adjusted based on actual structure) from tests.mocks.freecad_mock import MockFreeCAD class MockMCPServer: """Mock MCP Server for integration testing.""" def __init__(self): self.tools = {} self.active_document = None self.freecad = MockFreeCAD() def register_tool(self, name: str, tool_provider): """Register a tool provider.""" self.tools[name] = tool_provider async def call_tool(self, tool_name: str, arguments: Dict[str, Any]): """Call a registered tool.""" if tool_name not in self.tools: raise ValueError(f"Tool {tool_name} not found") tool = self.tools[tool_name] return await tool.execute_tool( arguments.get("tool_id"), arguments.get("params", {}) ) def list_tools(self) -> List[str]: """List all registered tools.""" return list(self.tools.keys()) class TestServerIntegration: """Test server component integration.""" @pytest.fixture def mock_server(self): """Create a mock MCP server.""" return MockMCPServer() @pytest.fixture def setup_server_tools( self, mock_server, primitive_tool_provider, model_manipulation_provider ): """Setup server with tool providers.""" mock_server.register_tool("primitives", primitive_tool_provider) mock_server.register_tool("model_manipulation", model_manipulation_provider) return mock_server def test_server_tool_registration(self, mock_server, primitive_tool_provider): """Test that tools can be registered with the server.""" mock_server.register_tool("primitives", primitive_tool_provider) assert "primitives" in mock_server.list_tools() assert mock_server.tools["primitives"] is primitive_tool_provider @pytest.mark.asyncio async def test_server_tool_execution(self, setup_server_tools): """Test that tools can be executed through the server.""" server = setup_server_tools # Execute a primitive tool result = await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": 10.0, "width": 8.0, "height": 3.0, "name": "test_box", }, }, ) assert result.status == "success" assert "Box created" in result.result["message"] assert result.result["object_name"] == "test_box" @pytest.mark.asyncio async def test_server_tool_chain_execution(self, setup_server_tools): """Test executing a chain of tools to build complex objects.""" server = setup_server_tools # Step 1: Create a box box_result = await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": 10.0, "width": 8.0, "height": 3.0, "name": "foundation", }, }, ) assert box_result.status == "success" # Step 2: Create a cylinder cylinder_result = await server.call_tool( "primitives", { "tool_id": "create_cylinder", "params": {"radius": 2.0, "height": 5.0, "name": "column"}, }, ) assert cylinder_result.status == "success" # Step 3: Move the cylinder move_result = await server.call_tool( "model_manipulation", { "tool_id": "transform", "params": {"object": "column", "translation": [5.0, 4.0, 3.0]}, }, ) assert move_result.status == "success" # Step 4: Perform boolean union union_result = await server.call_tool( "model_manipulation", { "tool_id": "boolean_operation", "params": { "operation": "union", "object1": "foundation", "object2": "column", "result_name": "foundation_with_column", }, }, ) assert union_result.status == "success" @pytest.mark.asyncio async def test_server_error_handling(self, setup_server_tools): """Test that server handles tool errors correctly.""" server = setup_server_tools # Test calling non-existent tool with pytest.raises(ValueError, match="Tool nonexistent not found"): await server.call_tool("nonexistent", {}) # Test calling with invalid parameters result = await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": -10.0, # Invalid negative length "width": 8.0, "height": 3.0, }, }, ) assert result.status == "error" assert "positive" in result.error.lower() @pytest.mark.asyncio async def test_server_concurrent_tool_execution(self, setup_server_tools): """Test that server can handle concurrent tool execution.""" server = setup_server_tools # Create multiple tasks that run concurrently async def create_object(name: str, index: int): return await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": 1.0, "width": 1.0, "height": 1.0, "name": f"{name}_{index}", }, }, ) # Execute multiple tool calls concurrently tasks = [create_object("box", i) for i in range(5)] results = await asyncio.gather(*tasks) # All should succeed for i, result in enumerate(results): assert result.status == "success" assert f"box_{i}" in result.result["object_name"] # Shared fixtures for integration tests @pytest.fixture def workflow_server(mock_freecad, primitive_tool_provider, model_manipulation_provider): """Setup server for workflow testing.""" server = MockMCPServer() server.freecad = mock_freecad server.register_tool("primitives", primitive_tool_provider) server.register_tool("model_manipulation", model_manipulation_provider) return server class TestWorkflowIntegration: """Test complete workflow integration scenarios.""" @pytest.mark.asyncio async def test_simple_building_workflow(self, workflow_server): """Test a simple building creation workflow.""" server = workflow_server # Create foundation foundation = await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": 20.0, "width": 15.0, "height": 1.0, "name": "foundation", }, }, ) assert foundation.status == "success" # Create walls wall_tasks = [] wall_specs = [ {"length": 20.0, "width": 0.3, "height": 3.0, "name": "wall_front"}, {"length": 20.0, "width": 0.3, "height": 3.0, "name": "wall_back"}, {"length": 15.0, "width": 0.3, "height": 3.0, "name": "wall_left"}, {"length": 15.0, "width": 0.3, "height": 3.0, "name": "wall_right"}, ] for spec in wall_specs: task = server.call_tool( "primitives", {"tool_id": "create_box", "params": spec} ) wall_tasks.append(task) wall_results = await asyncio.gather(*wall_tasks) # All walls should be created successfully for result in wall_results: assert result.status == "success" # Position walls on foundation positioning_tasks = [ server.call_tool( "model_manipulation", { "tool_id": "transform", "params": { "object": "wall_front", "translation": [0.0, -7.35, 1.5], }, }, ), server.call_tool( "model_manipulation", { "tool_id": "transform", "params": {"object": "wall_back", "translation": [0.0, 7.35, 1.5]}, }, ), ] positioning_results = await asyncio.gather(*positioning_tasks) for result in positioning_results: assert result.status == "success" @pytest.mark.asyncio async def test_complex_modeling_workflow(self, workflow_server): """Test a complex modeling workflow with boolean operations.""" server = workflow_server # Create base object base = await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": 10.0, "width": 10.0, "height": 5.0, "name": "base_block", }, }, ) assert base.status == "success" # Create holes hole_tasks = [] for i in range(3): hole_task = server.call_tool( "primitives", { "tool_id": "create_cylinder", "params": {"radius": 1.0, "height": 6.0, "name": f"hole_{i}"}, }, ) hole_tasks.append(hole_task) hole_results = await asyncio.gather(*hole_tasks) # Position holes positions = [(2.0, 2.0, 0.0), (-2.0, 2.0, 0.0), (0.0, -2.0, 0.0)] positioning_tasks = [] for i, (x, y, z) in enumerate(positions): task = server.call_tool( "model_manipulation", { "tool_id": "transform", "params": {"object": f"hole_{i}", "translation": [x, y, z]}, }, ) positioning_tasks.append(task) await asyncio.gather(*positioning_tasks) # Cut holes from base cutting_tasks = [] current_base = "base_block" for i in range(3): result_name = f"base_with_holes_{i}" task = server.call_tool( "model_manipulation", { "tool_id": "boolean_operation", "params": { "operation": "difference", "object1": current_base, "object2": f"hole_{i}", "result_name": result_name, }, }, ) cutting_tasks.append(task) current_base = result_name cutting_results = await asyncio.gather(*cutting_tasks) for result in cutting_results: assert result.status == "success" @pytest.mark.asyncio async def test_error_recovery_workflow(self, workflow_server): """Test workflow with error recovery.""" server = workflow_server # Try to create an object with invalid parameters invalid_result = await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": -5.0, # Invalid "width": 10.0, "height": 5.0, "name": "invalid_box", }, }, ) assert invalid_result.status == "error" # Recover with valid parameters valid_result = await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": 5.0, # Valid "width": 10.0, "height": 5.0, "name": "valid_box", }, }, ) assert valid_result.status == "success" # Continue workflow normally move_result = await server.call_tool( "model_manipulation", { "tool_id": "transform", "params": {"object": "valid_box", "translation": [5.0, 0.0, 0.0]}, }, ) assert move_result.status == "success" class TestPerformanceIntegration: """Test performance aspects of integration.""" @pytest.mark.performance @pytest.mark.asyncio async def test_bulk_operation_performance(self, workflow_server): """Test performance with bulk operations.""" server = workflow_server import time start_time = time.time() # Create many objects tasks = [] for i in range(20): task = server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": 1.0, "width": 1.0, "height": 1.0, "name": f"bulk_box_{i}", }, }, ) tasks.append(task) results = await asyncio.gather(*tasks) end_time = time.time() execution_time = end_time - start_time # All operations should succeed for result in results: assert result.status == "success" # Should complete within reasonable time (adjust threshold as needed) assert execution_time < 2.0, f"Bulk operations took {execution_time:.2f}s" @pytest.mark.performance @pytest.mark.asyncio async def test_memory_usage_stability(self, workflow_server): """Test that memory usage remains stable during operations.""" server = workflow_server # Simulate many operations for iteration in range(10): # Create some objects for i in range(5): await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": 1.0, "width": 1.0, "height": 1.0, "name": f"mem_test_{iteration}_{i}", }, }, ) # Clean up by creating fewer objects in next iteration # This tests that memory doesn't grow unbounded # If we get here without memory issues, the test passes assert True class TestRealWorldScenarios: """Test scenarios that simulate real-world usage.""" @pytest.mark.integration @pytest.mark.asyncio async def test_architectural_design_scenario(self, workflow_server): """Test a complete architectural design scenario.""" server = workflow_server # Design a simple house house_components = [] # Foundation foundation = await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": 12.0, "width": 8.0, "height": 0.5, "name": "house_foundation", }, }, ) assert foundation.status == "success" house_components.append("house_foundation") # Main structure main_box = await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": 12.0, "width": 8.0, "height": 3.0, "name": "main_structure", }, }, ) assert main_box.status == "success" # Position main structure await server.call_tool( "model_manipulation", { "tool_id": "transform", "params": {"object": "main_structure", "translation": [0.0, 0.0, 1.75]}, }, ) # Add windows (as holes to cut out later) window_positions = [ (5.0, -4.0, 1.5), # Front window (-5.0, -4.0, 1.5), # Front window 2 (0.0, 4.0, 1.5), # Back window ] for i, (x, y, z) in enumerate(window_positions): window = await server.call_tool( "primitives", { "tool_id": "create_box", "params": { "length": 2.0, "width": 0.5, "height": 1.5, "name": f"window_{i}", }, }, ) assert window.status == "success" await server.call_tool( "model_manipulation", { "tool_id": "transform", "params": {"object": f"window_{i}", "translation": [x, y, z]}, }, ) # This represents a real architectural workflow assert len(house_components) >= 1 @pytest.mark.integration @pytest.mark.asyncio async def test_mechanical_part_design_scenario(self, workflow_server): """Test designing a mechanical part.""" server = workflow_server # Create main body main_body = await server.call_tool( "primitives", { "tool_id": "create_cylinder", "params": {"radius": 5.0, "height": 2.0, "name": "main_body"}, }, ) assert main_body.status == "success" # Create central hole central_hole = await server.call_tool( "primitives", { "tool_id": "create_cylinder", "params": {"radius": 1.0, "height": 2.5, "name": "central_hole"}, }, ) assert central_hole.status == "success" # Create mounting holes mounting_holes = [] hole_positions = [ (3.0, 0.0, 0.0), (-3.0, 0.0, 0.0), (0.0, 3.0, 0.0), (0.0, -3.0, 0.0), ] for i, (x, y, z) in enumerate(hole_positions): hole = await server.call_tool( "primitives", { "tool_id": "create_cylinder", "params": { "radius": 0.5, "height": 2.5, "name": f"mounting_hole_{i}", }, }, ) assert hole.status == "success" mounting_holes.append(f"mounting_hole_{i}") await server.call_tool( "model_manipulation", { "tool_id": "transform", "params": { "object": f"mounting_hole_{i}", "translation": [x, y, z], }, }, ) # Cut holes from main body current_part = "main_body" # Cut central hole central_cut = await server.call_tool( "model_manipulation", { "tool_id": "boolean_operation", "params": { "operation": "difference", "object1": current_part, "object2": "central_hole", "result_name": "part_with_central_hole", }, }, ) assert central_cut.status == "success" current_part = "part_with_central_hole" # Cut mounting holes for i, hole_name in enumerate(mounting_holes): result_name = f"part_with_{i+1}_mounting_holes" cut_result = await server.call_tool( "model_manipulation", { "tool_id": "boolean_operation", "params": { "operation": "difference", "object1": current_part, "object2": hole_name, "result_name": result_name, }, }, ) assert cut_result.status == "success" current_part = result_name # Final part should be created successfully assert current_part == "part_with_4_mounting_holes"

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jango-blockchained/mcp-freecad'

If you have feedback or need assistance with the MCP directory API, please join our Discord server