Files
freecad-robust-mcp-fc111/tests/unit/test_tools_objects.py
T
Sean P. KaneandGitHub 6a6cad9e65 feat: Add workbench and major cleanup, refactor, and updates (#21)
* FreeCAD addon support for Workbench and Plugins

* docs: refactor docs and clean up linters, etc.

* Remove mdformat

* test: improve test coverage

* test:Lots of general fixes

* chore: more general fixes
2026-01-06 15:44:27 -08:00

526 lines
18 KiB
Python

"""Tests for object tools module."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from freecad_mcp.bridge.base import ExecutionResult, ObjectInfo
class TestObjectTools:
"""Tests for object management tools."""
@pytest.fixture
def mock_mcp(self):
"""Create a mock MCP server that captures tool registrations."""
mcp = MagicMock()
mcp._registered_tools = {}
def tool_decorator():
def wrapper(func):
mcp._registered_tools[func.__name__] = func
return func
return wrapper
mcp.tool = tool_decorator
return mcp
@pytest.fixture
def mock_bridge(self):
"""Create a mock FreeCAD bridge."""
return AsyncMock()
@pytest.fixture
def register_tools(self, mock_mcp, mock_bridge):
"""Register object tools and return the registered functions."""
from freecad_mcp.tools.objects import register_object_tools
async def get_bridge():
return mock_bridge
register_object_tools(mock_mcp, get_bridge)
return mock_mcp._registered_tools
@pytest.mark.asyncio
async def test_list_objects_empty(self, register_tools, mock_bridge):
"""list_objects should return empty list when no objects."""
mock_bridge.get_objects = AsyncMock(return_value=[])
list_objects = register_tools["list_objects"]
result = await list_objects()
assert result == []
mock_bridge.get_objects.assert_called_once_with(None)
@pytest.mark.asyncio
async def test_list_objects_with_objects(self, register_tools, mock_bridge):
"""list_objects should return object info."""
mock_objects = [
ObjectInfo(
name="Box",
label="My Box",
type_id="Part::Box",
visibility=True,
children=[],
parents=[],
),
ObjectInfo(
name="Cylinder",
label="My Cylinder",
type_id="Part::Cylinder",
visibility=False,
children=[],
parents=[],
),
]
mock_bridge.get_objects = AsyncMock(return_value=mock_objects)
list_objects = register_tools["list_objects"]
result = await list_objects(doc_name="TestDoc")
assert len(result) == 2
assert result[0]["name"] == "Box"
assert result[0]["type_id"] == "Part::Box"
assert result[0]["visibility"] is True
assert result[1]["name"] == "Cylinder"
assert result[1]["visibility"] is False
mock_bridge.get_objects.assert_called_once_with("TestDoc")
@pytest.mark.asyncio
async def test_inspect_object(self, register_tools, mock_bridge):
"""inspect_object should return detailed object info."""
mock_object = ObjectInfo(
name="Box",
label="My Box",
type_id="Part::Box",
properties={"Length": 10.0, "Width": 20.0, "Height": 30.0},
shape_info={
"shape_type": "Solid",
"volume": 6000.0,
"area": 2200.0,
"is_valid": True,
},
visibility=True,
children=["Fillet001"],
parents=[],
)
mock_bridge.get_object = AsyncMock(return_value=mock_object)
inspect_object = register_tools["inspect_object"]
result = await inspect_object(object_name="Box")
assert result["name"] == "Box"
assert result["type_id"] == "Part::Box"
assert result["properties"]["Length"] == 10.0
assert result["shape_info"]["volume"] == 6000.0
assert result["children"] == ["Fillet001"]
mock_bridge.get_object.assert_called_once_with("Box", None)
@pytest.mark.asyncio
async def test_inspect_object_without_properties(self, register_tools, mock_bridge):
"""inspect_object should exclude properties when not requested."""
mock_object = ObjectInfo(
name="Box",
label="My Box",
type_id="Part::Box",
properties={"Length": 10.0},
shape_info=None,
visibility=True,
children=[],
parents=[],
)
mock_bridge.get_object = AsyncMock(return_value=mock_object)
inspect_object = register_tools["inspect_object"]
result = await inspect_object(
object_name="Box", include_properties=False, include_shape=False
)
assert result["name"] == "Box"
assert "properties" not in result
assert "shape_info" not in result
@pytest.mark.asyncio
async def test_create_object(self, register_tools, mock_bridge):
"""create_object should create and return object info."""
mock_object = ObjectInfo(
name="Box",
label="Box",
type_id="Part::Box",
visibility=True,
children=[],
parents=[],
)
mock_bridge.create_object = AsyncMock(return_value=mock_object)
create_object = register_tools["create_object"]
result = await create_object(type_id="Part::Box", name="Box")
assert result["name"] == "Box"
assert result["type_id"] == "Part::Box"
mock_bridge.create_object.assert_called_once()
@pytest.mark.asyncio
async def test_edit_object(self, register_tools, mock_bridge):
"""edit_object should update object properties."""
mock_object = ObjectInfo(
name="Box",
label="Box",
type_id="Part::Box",
properties={"Length": 20.0, "Width": 10.0},
visibility=True,
children=[],
parents=[],
)
mock_bridge.edit_object = AsyncMock(return_value=mock_object)
edit_object = register_tools["edit_object"]
result = await edit_object(object_name="Box", properties={"Length": 20.0})
assert result["name"] == "Box"
mock_bridge.edit_object.assert_called_once_with("Box", {"Length": 20.0}, None)
@pytest.mark.asyncio
async def test_delete_object(self, register_tools, mock_bridge):
"""delete_object should delete and return success."""
mock_bridge.delete_object = AsyncMock(return_value=True)
delete_object = register_tools["delete_object"]
result = await delete_object(object_name="Box")
assert result["success"] is True
mock_bridge.delete_object.assert_called_once_with("Box", None)
@pytest.mark.asyncio
async def test_create_box(self, register_tools, mock_bridge):
"""create_box should create a box primitive via create_object."""
mock_object = ObjectInfo(
name="Box",
label="Box",
type_id="Part::Box",
visibility=True,
children=[],
parents=[],
)
mock_bridge.create_object = AsyncMock(return_value=mock_object)
create_box = register_tools["create_box"]
result = await create_box(length=20.0, width=10.0, height=5.0)
assert result["name"] == "Box"
assert result["volume"] == 20.0 * 10.0 * 5.0
mock_bridge.create_object.assert_called_once()
@pytest.mark.asyncio
async def test_create_cylinder(self, register_tools, mock_bridge):
"""create_cylinder should create a cylinder primitive via create_object."""
mock_object = ObjectInfo(
name="Cylinder",
label="Cylinder",
type_id="Part::Cylinder",
visibility=True,
children=[],
parents=[],
)
mock_bridge.create_object = AsyncMock(return_value=mock_object)
create_cylinder = register_tools["create_cylinder"]
result = await create_cylinder(radius=5.0, height=20.0)
assert result["name"] == "Cylinder"
mock_bridge.create_object.assert_called_once()
@pytest.mark.asyncio
async def test_create_sphere(self, register_tools, mock_bridge):
"""create_sphere should create a sphere primitive via create_object."""
mock_object = ObjectInfo(
name="Sphere",
label="Sphere",
type_id="Part::Sphere",
visibility=True,
children=[],
parents=[],
)
mock_bridge.create_object = AsyncMock(return_value=mock_object)
create_sphere = register_tools["create_sphere"]
result = await create_sphere(radius=10.0)
assert result["name"] == "Sphere"
mock_bridge.create_object.assert_called_once()
@pytest.mark.asyncio
async def test_create_cone(self, register_tools, mock_bridge):
"""create_cone should create a cone primitive via create_object."""
mock_object = ObjectInfo(
name="Cone",
label="Cone",
type_id="Part::Cone",
visibility=True,
children=[],
parents=[],
)
mock_bridge.create_object = AsyncMock(return_value=mock_object)
create_cone = register_tools["create_cone"]
result = await create_cone(radius1=10.0, radius2=0.0, height=20.0)
assert result["name"] == "Cone"
mock_bridge.create_object.assert_called_once()
@pytest.mark.asyncio
async def test_create_torus(self, register_tools, mock_bridge):
"""create_torus should create a torus primitive via create_object."""
mock_object = ObjectInfo(
name="Torus",
label="Torus",
type_id="Part::Torus",
visibility=True,
children=[],
parents=[],
)
mock_bridge.create_object = AsyncMock(return_value=mock_object)
create_torus = register_tools["create_torus"]
result = await create_torus(radius1=20.0, radius2=5.0)
assert result["name"] == "Torus"
mock_bridge.create_object.assert_called_once()
@pytest.mark.asyncio
async def test_create_wedge(self, register_tools, mock_bridge):
"""create_wedge should create a wedge primitive via create_object."""
mock_object = ObjectInfo(
name="Wedge",
label="Wedge",
type_id="Part::Wedge",
visibility=True,
children=[],
parents=[],
)
mock_bridge.create_object = AsyncMock(return_value=mock_object)
create_wedge = register_tools["create_wedge"]
result = await create_wedge()
assert result["name"] == "Wedge"
mock_bridge.create_object.assert_called_once()
@pytest.mark.asyncio
async def test_create_helix(self, register_tools, mock_bridge):
"""create_helix should create a helix primitive via create_object."""
mock_object = ObjectInfo(
name="Helix",
label="Helix",
type_id="Part::Helix",
visibility=True,
children=[],
parents=[],
)
mock_bridge.create_object = AsyncMock(return_value=mock_object)
create_helix = register_tools["create_helix"]
result = await create_helix(pitch=5.0, height=20.0)
assert result["name"] == "Helix"
mock_bridge.create_object.assert_called_once()
# Tests for execute_python based tools
@pytest.mark.asyncio
async def test_boolean_operation_fuse(self, register_tools, mock_bridge):
"""boolean_operation should perform union operation via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"name": "Fusion",
"label": "Fusion",
"type_id": "Part::MultiFuse",
},
stdout="",
stderr="",
execution_time_ms=10.0,
)
)
boolean_operation = register_tools["boolean_operation"]
result = await boolean_operation(
operation="fuse", object1_name="Box", object2_name="Cylinder"
)
assert result["name"] == "Fusion"
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_set_placement(self, register_tools, mock_bridge):
"""set_placement should set position and rotation via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={"position": [10.0, 20.0, 30.0], "rotation": [0.0, 0.0, 45.0]},
stdout="",
stderr="",
execution_time_ms=5.0,
)
)
set_placement = register_tools["set_placement"]
result = await set_placement(object_name="Box", position=[10.0, 20.0, 30.0])
assert result["position"] == [10.0, 20.0, 30.0]
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_scale_object(self, register_tools, mock_bridge):
"""scale_object should scale an object via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"name": "ScaledBox",
"label": "ScaledBox",
"type_id": "Part::Feature",
},
stdout="",
stderr="",
execution_time_ms=15.0,
)
)
scale_object = register_tools["scale_object"]
result = await scale_object(object_name="Box", scale=2.0)
assert result["name"] == "ScaledBox"
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_rotate_object(self, register_tools, mock_bridge):
"""rotate_object should rotate an object via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={"position": [0.0, 0.0, 0.0], "rotation": [0.0, 0.0, 45.0]},
stdout="",
stderr="",
execution_time_ms=5.0,
)
)
rotate_object = register_tools["rotate_object"]
result = await rotate_object(
object_name="Box", axis=[0.0, 0.0, 1.0], angle=45.0
)
assert result["rotation"] == [0.0, 0.0, 45.0]
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_copy_object(self, register_tools, mock_bridge):
"""copy_object should create a copy via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={"name": "Box001", "label": "Box001", "type_id": "Part::Box"},
stdout="",
stderr="",
execution_time_ms=10.0,
)
)
copy_object = register_tools["copy_object"]
result = await copy_object(object_name="Box")
assert result["name"] == "Box001"
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_mirror_object(self, register_tools, mock_bridge):
"""mirror_object should mirror across a plane via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"name": "MirroredBox",
"label": "MirroredBox",
"type_id": "Part::Feature",
},
stdout="",
stderr="",
execution_time_ms=15.0,
)
)
mirror_object = register_tools["mirror_object"]
result = await mirror_object(object_name="Box", plane="XY")
assert result["name"] == "MirroredBox"
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_get_selection(self, register_tools, mock_bridge):
"""get_selection should return selected objects via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result=[
{
"name": "Box",
"label": "Box",
"type_id": "Part::Box",
"sub_elements": ["Face1"],
}
],
stdout="",
stderr="",
execution_time_ms=5.0,
)
)
get_selection = register_tools["get_selection"]
result = await get_selection()
assert len(result) == 1
assert result[0]["name"] == "Box"
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_set_selection(self, register_tools, mock_bridge):
"""set_selection should select objects via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={"success": True, "selected_count": 2},
stdout="",
stderr="",
execution_time_ms=5.0,
)
)
set_selection = register_tools["set_selection"]
result = await set_selection(object_names=["Box", "Cylinder"])
assert result["success"] is True
assert result["selected_count"] == 2
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_clear_selection(self, register_tools, mock_bridge):
"""clear_selection should clear selections via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={"success": True},
stdout="",
stderr="",
execution_time_ms=5.0,
)
)
clear_selection = register_tools["clear_selection"]
result = await clear_selection()
assert result["success"] is True
mock_bridge.execute_python.assert_called_once()