Skip to main content
Glama
test_model_manipulation.py17.3 kB
""" Unit tests for ModelManipulationToolProvider. This module tests the model manipulation and transformation functionality. """ import asyncio from unittest.mock import MagicMock, patch import pytest from tests.fixtures import ( boolean_operations, mock_freecad, model_manipulation_tool_provider, primitive_tool_provider, test_document, test_utilities, ) class TestModelManipulationToolProvider: """Test suite for ModelManipulationToolProvider.""" def test_tool_schema(self, model_manipulation_tool_provider): """Test that the tool schema is properly defined.""" schema = model_manipulation_tool_provider.tool_schema assert schema.name == "model_manipulation" assert schema.description is not None assert "properties" in schema.parameters assert "tool_id" in schema.parameters["properties"] assert "params" in schema.parameters["properties"] # Validate tool_id enum values tool_ids = schema.parameters["properties"]["tool_id"]["enum"] expected_tools = [ "transform", "boolean_operation", "fillet_edge", "chamfer_edge", "mirror", "scale", "offset", "thicken", ] for tool in expected_tools: assert tool in tool_ids @pytest.mark.asyncio async def test_transform_translation( self, model_manipulation_tool_provider, primitive_tool_provider, mock_freecad ): """Test object transformation with translation.""" # First create an object to transform box_result = await primitive_tool_provider.execute_tool( "create_box", {"length": 5.0, "width": 3.0, "height": 2.0} ) assert box_result.status == "success" box_id = box_result.result["object_id"] # Transform the object transform_params = {"object": box_id, "translation": [10.0, 5.0, 2.0]} result = await model_manipulation_tool_provider.execute_tool( "transform", transform_params ) assert result.status == "success" assert "object" in result.result assert result.result["object"] == box_id assert "modifications" in result.result @pytest.mark.asyncio async def test_transform_rotation( self, model_manipulation_tool_provider, primitive_tool_provider, mock_freecad ): """Test object transformation with rotation.""" # Create an object to transform cylinder_result = await primitive_tool_provider.execute_tool( "create_cylinder", {"radius": 3.0, "height": 8.0} ) assert cylinder_result.status == "success" cylinder_id = cylinder_result.result["object_id"] # Rotate the object transform_params = { "object": cylinder_id, "rotation": [90.0, 0.0, 45.0], # Euler angles in degrees } result = await model_manipulation_tool_provider.execute_tool( "transform", transform_params ) assert result.status == "success" assert "modifications" in result.result @pytest.mark.asyncio async def test_transform_combined( self, model_manipulation_tool_provider, primitive_tool_provider, mock_freecad ): """Test object transformation with both translation and rotation.""" # Create an object sphere_result = await primitive_tool_provider.execute_tool( "create_sphere", {"radius": 4.0} ) assert sphere_result.status == "success" sphere_id = sphere_result.result["object_id"] # Apply combined transformation transform_params = { "object": sphere_id, "translation": [5.0, 0.0, 3.0], "rotation": [0.0, 90.0, 0.0], } result = await model_manipulation_tool_provider.execute_tool( "transform", transform_params ) assert result.status == "success" assert len(result.result["modifications"]) == 2 # Translation + rotation @pytest.mark.asyncio async def test_boolean_union( self, model_manipulation_tool_provider, primitive_tool_provider, mock_freecad ): """Test boolean union operation.""" # Create two objects box1_result = await primitive_tool_provider.execute_tool( "create_box", {"length": 5.0, "width": 5.0, "height": 5.0} ) box2_result = await primitive_tool_provider.execute_tool( "create_box", {"length": 3.0, "width": 3.0, "height": 8.0} ) assert box1_result.status == "success" assert box2_result.status == "success" # Perform union union_params = { "operation": "union", "object1": box1_result.result["object_id"], "object2": box2_result.result["object_id"], "result_name": "UnionResult", } result = await model_manipulation_tool_provider.execute_tool( "boolean_operation", union_params ) assert result.status == "success" assert result.result["result_object"] == "UnionResult" assert result.result["operation"] == "union" # Verify the result object exists doc = mock_freecad.ActiveDocument union_obj = doc.getObject("UnionResult") assert union_obj is not None @pytest.mark.asyncio async def test_boolean_difference( self, model_manipulation_tool_provider, primitive_tool_provider, mock_freecad ): """Test boolean difference (cut) operation.""" # Create two objects box_result = await primitive_tool_provider.execute_tool( "create_box", {"length": 10.0, "width": 10.0, "height": 5.0} ) cylinder_result = await primitive_tool_provider.execute_tool( "create_cylinder", {"radius": 2.0, "height": 6.0} ) assert box_result.status == "success" assert cylinder_result.status == "success" # Cut cylinder from box cut_params = { "operation": "difference", "object1": box_result.result["object_id"], "object2": cylinder_result.result["object_id"], "result_name": "CutResult", } result = await model_manipulation_tool_provider.execute_tool( "boolean_operation", cut_params ) assert result.status == "success" assert result.result["operation"] == "difference" @pytest.mark.asyncio async def test_boolean_intersection( self, model_manipulation_tool_provider, primitive_tool_provider, mock_freecad ): """Test boolean intersection operation.""" # Create two overlapping objects box_result = await primitive_tool_provider.execute_tool( "create_box", {"length": 6.0, "width": 6.0, "height": 6.0} ) sphere_result = await primitive_tool_provider.execute_tool( "create_sphere", {"radius": 4.0} ) assert box_result.status == "success" assert sphere_result.status == "success" # Find intersection intersection_params = { "operation": "intersection", "object1": box_result.result["object_id"], "object2": sphere_result.result["object_id"], "result_name": "IntersectionResult", } result = await model_manipulation_tool_provider.execute_tool( "boolean_operation", intersection_params ) assert result.status == "success" assert result.result["operation"] == "intersection" @pytest.mark.asyncio async def test_fillet_edge( self, model_manipulation_tool_provider, primitive_tool_provider, mock_freecad ): """Test edge filleting operation.""" # Create a box to fillet box_result = await primitive_tool_provider.execute_tool( "create_box", {"length": 8.0, "width": 6.0, "height": 4.0} ) assert box_result.status == "success" # Apply fillet fillet_params = { "object": box_result.result["object_id"], "radius": 1.0, "result_name": "FilletedBox", } result = await model_manipulation_tool_provider.execute_tool( "fillet_edge", fillet_params ) assert result.status == "success" assert result.result["result_object"] == "FilletedBox" assert result.result["radius"] == 1.0 @pytest.mark.asyncio async def test_mirror_object( self, model_manipulation_tool_provider, primitive_tool_provider, mock_freecad ): """Test object mirroring operation.""" # Create an object to mirror cone_result = await primitive_tool_provider.execute_tool( "create_cone", {"radius1": 5.0, "radius2": 2.0, "height": 8.0} ) assert cone_result.status == "success" # Mirror across XY plane mirror_params = { "object": cone_result.result["object_id"], "plane": "xy", "result_name": "MirroredCone", } result = await model_manipulation_tool_provider.execute_tool( "mirror", mirror_params ) assert result.status == "success" assert result.result["result_object"] == "MirroredCone" assert "xy" in result.result["mirror_plane"] @pytest.mark.asyncio async def test_mirror_with_normal_vector( self, model_manipulation_tool_provider, primitive_tool_provider, mock_freecad ): """Test object mirroring with custom normal vector.""" # Create an object box_result = await primitive_tool_provider.execute_tool( "create_box", {"length": 4.0, "width": 4.0, "height": 4.0} ) assert box_result.status == "success" # Mirror with custom normal mirror_params = { "object": box_result.result["object_id"], "normal": [1.0, 1.0, 0.0], # Diagonal mirror "result_name": "DiagonalMirror", } result = await model_manipulation_tool_provider.execute_tool( "mirror", mirror_params ) assert result.status == "success" @pytest.mark.asyncio async def test_transform_nonexistent_object(self, model_manipulation_tool_provider): """Test transformation of non-existent object.""" transform_params = { "object": "NonExistentObject", "translation": [1.0, 0.0, 0.0], } result = await model_manipulation_tool_provider.execute_tool( "transform", transform_params ) # In our mock, this might still succeed, but in real FreeCAD it would fail # This test documents the expected behavior @pytest.mark.asyncio async def test_boolean_operation_missing_objects( self, model_manipulation_tool_provider ): """Test boolean operation with missing objects.""" boolean_params = { "operation": "union", "object1": "MissingObject1", "object2": "MissingObject2", } result = await model_manipulation_tool_provider.execute_tool( "boolean_operation", boolean_params ) # Should handle missing objects gracefully @pytest.mark.asyncio async def test_invalid_boolean_operation(self, model_manipulation_tool_provider): """Test invalid boolean operation type.""" boolean_params = { "operation": "invalid_operation", "object1": "Object1", "object2": "Object2", } result = await model_manipulation_tool_provider.execute_tool( "boolean_operation", boolean_params ) assert result.status == "error" assert "Unknown operation type" in result.error @pytest.mark.asyncio async def test_transform_invalid_parameters(self, model_manipulation_tool_provider): """Test transformation with invalid parameters.""" # Test invalid translation format invalid_params = { "object": "SomeObject", "translation": [1.0, 2.0], # Missing Z component } result = await model_manipulation_tool_provider.execute_tool( "transform", invalid_params ) assert result.status == "error" assert "Invalid translation format" in result.error @pytest.mark.parametrize("operation", ["union", "difference", "intersection"]) @pytest.mark.asyncio async def test_boolean_operations_parametrized( self, operation, model_manipulation_tool_provider, primitive_tool_provider, mock_freecad, ): """Test all boolean operations with parametrized testing.""" # Create two objects obj1_result = await primitive_tool_provider.execute_tool( "create_box", {"length": 5.0, "width": 5.0, "height": 5.0} ) obj2_result = await primitive_tool_provider.execute_tool( "create_sphere", {"radius": 3.0} ) assert obj1_result.status == "success" assert obj2_result.status == "success" # Perform boolean operation boolean_params = { "operation": operation, "object1": obj1_result.result["object_id"], "object2": obj2_result.result["object_id"], "result_name": f"{operation.title()}Result", } result = await model_manipulation_tool_provider.execute_tool( "boolean_operation", boolean_params ) assert result.status == "success" assert result.result["operation"] == operation @pytest.mark.asyncio async def test_no_freecad_connection(self): """Test behavior when FreeCAD is not available.""" from src.mcp_freecad.tools.model_manipulation import ( ModelManipulationToolProvider, ) # Create provider without FreeCAD provider = ModelManipulationToolProvider(freecad_app=None) result = await provider.execute_tool( "transform", {"object": "TestObject", "translation": [1, 0, 0]} ) assert result.status == "error" assert "FreeCAD is not available" in result.error @pytest.mark.asyncio async def test_unimplemented_tools(self, model_manipulation_tool_provider): """Test that unimplemented tools return appropriate errors.""" unimplemented_tools = ["chamfer_edge", "scale", "offset", "thicken"] for tool in unimplemented_tools: result = await model_manipulation_tool_provider.execute_tool( tool, {"object": "TestObject"} ) assert result.status == "error" assert "not yet implemented" in result.error @pytest.mark.asyncio async def test_complex_transformation_sequence( self, model_manipulation_tool_provider, primitive_tool_provider, mock_freecad ): """Test a sequence of complex transformations.""" # Create an object box_result = await primitive_tool_provider.execute_tool( "create_box", {"length": 4.0, "width": 4.0, "height": 4.0} ) assert box_result.status == "success" box_id = box_result.result["object_id"] # Apply multiple transformations transformations = [ {"translation": [2.0, 0.0, 0.0]}, {"rotation": [0.0, 0.0, 45.0]}, {"translation": [0.0, 3.0, 0.0]}, {"rotation": [90.0, 0.0, 0.0]}, ] for i, transform in enumerate(transformations): transform["object"] = box_id result = await model_manipulation_tool_provider.execute_tool( "transform", transform ) assert result.status == "success", f"Transformation {i+1} failed" @pytest.mark.performance @pytest.mark.asyncio async def test_boolean_operation_performance( self, model_manipulation_tool_provider, primitive_tool_provider, performance_benchmarks, ): """Test that boolean operations meet performance requirements.""" import time # Create objects box_result = await primitive_tool_provider.execute_tool( "create_box", {"length": 10.0, "width": 10.0, "height": 10.0} ) cylinder_result = await primitive_tool_provider.execute_tool( "create_cylinder", {"radius": 5.0, "height": 12.0} ) # Time the boolean operation start_time = time.time() result = await model_manipulation_tool_provider.execute_tool( "boolean_operation", { "operation": "union", "object1": box_result.result["object_id"], "object2": cylinder_result.result["object_id"], }, ) elapsed_time = time.time() - start_time benchmark = performance_benchmarks["boolean_operation"] assert result.status == "success" assert ( elapsed_time < benchmark["max_time_seconds"] ), f"Boolean operation took {elapsed_time:.3f}s, expected < {benchmark['max_time_seconds']}s"

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