* 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
305 lines
10 KiB
Python
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"
|