Files
freecad-robust-mcp-fc111/tests/unit/test_tools_export.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

305 lines
10 KiB
Python

"""Tests for export/import tools module."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from freecad_mcp.bridge.base import ExecutionResult
class TestExportTools:
"""Tests for export/import 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 export tools and return the registered functions."""
from freecad_mcp.tools.export import register_export_tools
async def get_bridge():
return mock_bridge
register_export_tools(mock_mcp, get_bridge)
return mock_mcp._registered_tools
@pytest.mark.asyncio
async def test_export_step(self, register_tools, mock_bridge):
"""export_step should export to STEP format via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"success": True,
"path": "/tmp/output.step",
"object_count": 2,
},
stdout="",
stderr="",
execution_time_ms=50.0,
)
)
export_step = register_tools["export_step"]
result = await export_step(
file_path="/tmp/output.step", object_names=["Box", "Cylinder"]
)
assert result["success"] is True
assert result["path"] == "/tmp/output.step"
assert result["object_count"] == 2
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_export_step_all_visible(self, register_tools, mock_bridge):
"""export_step should export all visible objects when no names given."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"success": True,
"path": "/tmp/output.step",
"object_count": 5,
},
stdout="",
stderr="",
execution_time_ms=75.0,
)
)
export_step = register_tools["export_step"]
result = await export_step(file_path="/tmp/output.step")
assert result["success"] is True
assert result["object_count"] == 5
@pytest.mark.asyncio
async def test_export_stl(self, register_tools, mock_bridge):
"""export_stl should export to STL format via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"success": True,
"path": "/tmp/output.stl",
"object_count": 1,
},
stdout="",
stderr="",
execution_time_ms=30.0,
)
)
export_stl = register_tools["export_stl"]
result = await export_stl(file_path="/tmp/output.stl", object_names=["Box"])
assert result["success"] is True
assert result["path"] == "/tmp/output.stl"
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_export_stl_with_tolerance(self, register_tools, mock_bridge):
"""export_stl should accept mesh tolerance parameter."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"success": True,
"path": "/tmp/fine.stl",
"object_count": 1,
},
stdout="",
stderr="",
execution_time_ms=45.0,
)
)
export_stl = register_tools["export_stl"]
result = await export_stl(file_path="/tmp/fine.stl", mesh_tolerance=0.01)
assert result["success"] is True
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_export_3mf(self, register_tools, mock_bridge):
"""export_3mf should export to 3MF format via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"success": True,
"path": "/tmp/output.3mf",
"object_count": 1,
},
stdout="",
stderr="",
execution_time_ms=40.0,
)
)
export_3mf = register_tools["export_3mf"]
result = await export_3mf(file_path="/tmp/output.3mf")
assert result["success"] is True
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_export_obj(self, register_tools, mock_bridge):
"""export_obj should export to OBJ format via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"success": True,
"path": "/tmp/output.obj",
"object_count": 1,
},
stdout="",
stderr="",
execution_time_ms=35.0,
)
)
export_obj = register_tools["export_obj"]
result = await export_obj(file_path="/tmp/output.obj")
assert result["success"] is True
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_export_iges(self, register_tools, mock_bridge):
"""export_iges should export to IGES format via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"success": True,
"path": "/tmp/output.iges",
"object_count": 1,
},
stdout="",
stderr="",
execution_time_ms=55.0,
)
)
export_iges = register_tools["export_iges"]
result = await export_iges(file_path="/tmp/output.iges")
assert result["success"] is True
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_import_step(self, register_tools, mock_bridge):
"""import_step should import STEP files via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"success": True,
"document": "Imported",
"objects": ["Part", "Assembly"],
},
stdout="",
stderr="",
execution_time_ms=100.0,
)
)
import_step = register_tools["import_step"]
result = await import_step(file_path="/tmp/input.step")
assert result["success"] is True
assert result["document"] == "Imported"
assert len(result["objects"]) == 2
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_import_stl(self, register_tools, mock_bridge):
"""import_stl should import STL files via execute_python."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"success": True,
"document": "Mesh",
"object": "Mesh001",
},
stdout="",
stderr="",
execution_time_ms=80.0,
)
)
import_stl = register_tools["import_stl"]
result = await import_stl(file_path="/tmp/input.stl")
assert result["success"] is True
assert result["object"] == "Mesh001"
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_export_step_failure(self, register_tools, mock_bridge):
"""export_step should raise ValueError on failure."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=False,
result=None,
stdout="",
stderr="FileNotFoundError: Directory does not exist",
execution_time_ms=5.0,
error_type="FileNotFoundError",
error_traceback="Traceback: FileNotFoundError: Directory does not exist",
)
)
export_step = register_tools["export_step"]
with pytest.raises(ValueError) as exc_info:
await export_step(file_path="/nonexistent/output.step")
assert "FileNotFoundError" in str(exc_info.value) or "Traceback" in str(
exc_info.value
)
@pytest.mark.asyncio
async def test_import_step_into_document(self, register_tools, mock_bridge):
"""import_step should import into specified document."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={
"success": True,
"document": "MyDoc",
"objects": ["ImportedPart"],
},
stdout="",
stderr="",
execution_time_ms=90.0,
)
)
import_step = register_tools["import_step"]
result = await import_step(file_path="/tmp/part.step", doc_name="MyDoc")
assert result["success"] is True
assert result["document"] == "MyDoc"