feat: Initial commit of FreeCAD MCP/tooling proj.
This commit is contained in:
@@ -0,0 +1,579 @@
|
||||
"""Integration tests for FreeCAD MCP GUI mode.
|
||||
|
||||
These tests verify that the MCP bridge works correctly when FreeCAD is running
|
||||
in GUI mode (with full graphical interface). They test GUI-specific features
|
||||
like visibility, display modes, colors, and camera operations.
|
||||
|
||||
Note: These tests require a running FreeCAD GUI server.
|
||||
Start it with: just run-gui
|
||||
|
||||
To run these tests:
|
||||
pytest tests/integration/test_gui_mode.py -v
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import xmlrpc.client
|
||||
from collections.abc import Generator
|
||||
|
||||
# Mark all tests in this module as integration tests and gui tests
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.gui]
|
||||
|
||||
# Note: xmlrpc_proxy fixture is defined in conftest.py
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def temp_dir() -> Generator[str, None, None]:
|
||||
"""Create a temporary directory for test files."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
yield tmpdir
|
||||
|
||||
|
||||
def execute_code(proxy: xmlrpc.client.ServerProxy, code: str) -> dict[str, Any]:
|
||||
"""Execute Python code via the MCP bridge and return the result."""
|
||||
result: dict[str, Any] = proxy.execute(code) # type: ignore[assignment]
|
||||
assert result.get("success"), f"Execution failed: {result.get('error_traceback')}"
|
||||
return result
|
||||
|
||||
|
||||
class TestGUIConnection:
|
||||
"""Tests for GUI mode connectivity and detection."""
|
||||
|
||||
def test_ping(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Test that the server responds to ping."""
|
||||
result: dict[str, Any] = xmlrpc_proxy.ping() # type: ignore[assignment]
|
||||
assert result["pong"] is True
|
||||
assert "timestamp" in result
|
||||
|
||||
def test_gui_mode_detected(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Test that FreeCAD is running in GUI mode (GuiUp=True)."""
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
_result_ = {"gui_up": FreeCAD.GuiUp}
|
||||
""",
|
||||
)
|
||||
# In GUI mode, GuiUp should be True (1)
|
||||
assert result["result"]["gui_up"] == 1 or result["result"]["gui_up"] is True
|
||||
|
||||
|
||||
class TestGUIObjectCreation:
|
||||
"""Tests for object creation in GUI mode."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Create a fresh document for each test."""
|
||||
execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
# Close any existing GUITestDoc
|
||||
if "GUITestDoc" in FreeCAD.listDocuments():
|
||||
FreeCAD.closeDocument("GUITestDoc")
|
||||
doc = FreeCAD.newDocument("GUITestDoc")
|
||||
_result_ = True
|
||||
""",
|
||||
)
|
||||
|
||||
def test_create_box_with_view_object(
|
||||
self, xmlrpc_proxy: xmlrpc.client.ServerProxy
|
||||
) -> None:
|
||||
"""Test creating a box with ViewObject in GUI mode."""
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
import Part
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
box = Part.makeBox(10, 20, 30)
|
||||
obj = doc.addObject("Part::Feature", "GUIBox")
|
||||
obj.Shape = box
|
||||
doc.recompute()
|
||||
|
||||
has_view_object = hasattr(obj, "ViewObject") and obj.ViewObject is not None
|
||||
|
||||
_result_ = {
|
||||
"name": obj.Name,
|
||||
"volume": obj.Shape.Volume,
|
||||
"has_view_object": has_view_object,
|
||||
"valid": obj.Shape.isValid()
|
||||
}
|
||||
""",
|
||||
)
|
||||
assert result["result"]["name"] == "GUIBox"
|
||||
assert abs(result["result"]["volume"] - 6000.0) < 0.01
|
||||
assert result["result"]["has_view_object"] is True
|
||||
assert result["result"]["valid"] is True
|
||||
|
||||
|
||||
class TestVisibility:
|
||||
"""Tests for object visibility operations in GUI mode."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Create a document with test objects."""
|
||||
execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
import Part
|
||||
|
||||
if "VisibilityTestDoc" in FreeCAD.listDocuments():
|
||||
FreeCAD.closeDocument("VisibilityTestDoc")
|
||||
doc = FreeCAD.newDocument("VisibilityTestDoc")
|
||||
|
||||
box = Part.makeBox(10, 10, 10)
|
||||
obj = doc.addObject("Part::Feature", "VisBox")
|
||||
obj.Shape = box
|
||||
doc.recompute()
|
||||
_result_ = True
|
||||
""",
|
||||
)
|
||||
|
||||
def test_hide_object(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Test hiding an object."""
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
obj = doc.getObject("VisBox")
|
||||
|
||||
# Hide the object
|
||||
obj.ViewObject.Visibility = False
|
||||
|
||||
_result_ = {
|
||||
"visible": obj.ViewObject.Visibility
|
||||
}
|
||||
""",
|
||||
)
|
||||
assert result["result"]["visible"] is False
|
||||
|
||||
def test_show_object(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Test showing an object."""
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
obj = doc.getObject("VisBox")
|
||||
|
||||
# First hide, then show
|
||||
obj.ViewObject.Visibility = False
|
||||
obj.ViewObject.Visibility = True
|
||||
|
||||
_result_ = {
|
||||
"visible": obj.ViewObject.Visibility
|
||||
}
|
||||
""",
|
||||
)
|
||||
assert result["result"]["visible"] is True
|
||||
|
||||
|
||||
class TestDisplayMode:
|
||||
"""Tests for display mode operations in GUI mode."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Create a document with test objects."""
|
||||
execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
import Part
|
||||
|
||||
if "DisplayModeTestDoc" in FreeCAD.listDocuments():
|
||||
FreeCAD.closeDocument("DisplayModeTestDoc")
|
||||
doc = FreeCAD.newDocument("DisplayModeTestDoc")
|
||||
|
||||
box = Part.makeBox(10, 10, 10)
|
||||
obj = doc.addObject("Part::Feature", "DisplayBox")
|
||||
obj.Shape = box
|
||||
doc.recompute()
|
||||
_result_ = True
|
||||
""",
|
||||
)
|
||||
|
||||
def test_get_display_modes(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Test getting available display modes."""
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
obj = doc.getObject("DisplayBox")
|
||||
|
||||
modes = obj.ViewObject.listDisplayModes()
|
||||
|
||||
_result_ = {
|
||||
"modes": list(modes),
|
||||
"current": obj.ViewObject.DisplayMode
|
||||
}
|
||||
""",
|
||||
)
|
||||
# Typical modes: "Flat Lines", "Shaded", "Wireframe", etc.
|
||||
assert len(result["result"]["modes"]) > 0
|
||||
assert result["result"]["current"] is not None
|
||||
|
||||
def test_set_display_mode(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Test setting display mode."""
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
obj = doc.getObject("DisplayBox")
|
||||
|
||||
modes = obj.ViewObject.listDisplayModes()
|
||||
# Set to a different mode if available
|
||||
target_mode = "Wireframe" if "Wireframe" in modes else modes[0]
|
||||
obj.ViewObject.DisplayMode = target_mode
|
||||
|
||||
_result_ = {
|
||||
"mode_set": obj.ViewObject.DisplayMode,
|
||||
"target_mode": target_mode
|
||||
}
|
||||
""",
|
||||
)
|
||||
assert result["result"]["mode_set"] == result["result"]["target_mode"]
|
||||
|
||||
|
||||
class TestObjectColor:
|
||||
"""Tests for object color operations in GUI mode."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Create a document with test objects."""
|
||||
execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
import Part
|
||||
|
||||
if "ColorTestDoc" in FreeCAD.listDocuments():
|
||||
FreeCAD.closeDocument("ColorTestDoc")
|
||||
doc = FreeCAD.newDocument("ColorTestDoc")
|
||||
|
||||
box = Part.makeBox(10, 10, 10)
|
||||
obj = doc.addObject("Part::Feature", "ColorBox")
|
||||
obj.Shape = box
|
||||
doc.recompute()
|
||||
_result_ = True
|
||||
""",
|
||||
)
|
||||
|
||||
def test_set_shape_color(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Test setting object shape color."""
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
obj = doc.getObject("ColorBox")
|
||||
|
||||
# Set color to red (RGB as tuple of floats 0-1)
|
||||
obj.ViewObject.ShapeColor = (1.0, 0.0, 0.0)
|
||||
|
||||
color = obj.ViewObject.ShapeColor
|
||||
|
||||
_result_ = {
|
||||
"r": color[0],
|
||||
"g": color[1],
|
||||
"b": color[2]
|
||||
}
|
||||
""",
|
||||
)
|
||||
assert result["result"]["r"] == pytest.approx(1.0, rel=0.01)
|
||||
assert result["result"]["g"] == pytest.approx(0.0, abs=0.01)
|
||||
assert result["result"]["b"] == pytest.approx(0.0, abs=0.01)
|
||||
|
||||
def test_set_transparency(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Test setting object transparency."""
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
obj = doc.getObject("ColorBox")
|
||||
|
||||
# Set 50% transparency
|
||||
obj.ViewObject.Transparency = 50
|
||||
|
||||
_result_ = {
|
||||
"transparency": obj.ViewObject.Transparency
|
||||
}
|
||||
""",
|
||||
)
|
||||
assert result["result"]["transparency"] == 50
|
||||
|
||||
|
||||
class TestCameraOperations:
|
||||
"""Tests for camera/view operations in GUI mode."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Create a document with test objects."""
|
||||
execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
import Part
|
||||
|
||||
if "CameraTestDoc" in FreeCAD.listDocuments():
|
||||
FreeCAD.closeDocument("CameraTestDoc")
|
||||
doc = FreeCAD.newDocument("CameraTestDoc")
|
||||
|
||||
box = Part.makeBox(10, 10, 10)
|
||||
obj = doc.addObject("Part::Feature", "CameraBox")
|
||||
obj.Shape = box
|
||||
doc.recompute()
|
||||
_result_ = True
|
||||
""",
|
||||
)
|
||||
|
||||
def test_fit_all(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Test fit all view operation."""
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
view = FreeCADGui.ActiveDocument.ActiveView
|
||||
|
||||
# Fit all objects in view
|
||||
view.fitAll()
|
||||
|
||||
_result_ = {"success": True}
|
||||
""",
|
||||
)
|
||||
assert result["result"]["success"] is True
|
||||
|
||||
def test_set_view_direction(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
|
||||
"""Test setting view direction (front, top, etc.)."""
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
view = FreeCADGui.ActiveDocument.ActiveView
|
||||
|
||||
# Set to front view
|
||||
view.viewFront()
|
||||
|
||||
# Get camera orientation
|
||||
cam = view.getCameraOrientation()
|
||||
|
||||
_result_ = {
|
||||
"success": True,
|
||||
"camera_orientation": [cam.Q[0], cam.Q[1], cam.Q[2], cam.Q[3]]
|
||||
}
|
||||
""",
|
||||
)
|
||||
assert result["result"]["success"] is True
|
||||
|
||||
|
||||
class TestScreenshot:
|
||||
"""Tests for screenshot functionality in GUI mode."""
|
||||
|
||||
def test_capture_screenshot(
|
||||
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, temp_dir: str
|
||||
) -> None:
|
||||
"""Test capturing a screenshot."""
|
||||
# First create a document with objects
|
||||
execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
import Part
|
||||
|
||||
if "ScreenshotTestDoc" in FreeCAD.listDocuments():
|
||||
FreeCAD.closeDocument("ScreenshotTestDoc")
|
||||
doc = FreeCAD.newDocument("ScreenshotTestDoc")
|
||||
|
||||
box = Part.makeBox(20, 20, 20)
|
||||
obj = doc.addObject("Part::Feature", "ScreenshotBox")
|
||||
obj.Shape = box
|
||||
obj.ViewObject.ShapeColor = (0.0, 0.5, 1.0) # Blue color
|
||||
doc.recompute()
|
||||
_result_ = True
|
||||
""",
|
||||
)
|
||||
|
||||
screenshot_path = Path(temp_dir) / "test_screenshot.png"
|
||||
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
f"""
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import os
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
view = FreeCADGui.ActiveDocument.ActiveView
|
||||
|
||||
# Fit objects in view
|
||||
view.fitAll()
|
||||
|
||||
# Save screenshot
|
||||
view.saveImage({screenshot_path!r}, 800, 600, "White")
|
||||
|
||||
_result_ = {{
|
||||
"saved": os.path.exists({screenshot_path!r}),
|
||||
"path": {screenshot_path!r}
|
||||
}}
|
||||
""",
|
||||
)
|
||||
assert result["result"]["saved"] is True
|
||||
assert screenshot_path.exists()
|
||||
# Verify file has content
|
||||
assert screenshot_path.stat().st_size > 0
|
||||
|
||||
|
||||
class TestComplexGUIWorkflow:
|
||||
"""Tests for complex workflows that combine GUI and modeling features."""
|
||||
|
||||
def test_create_assembly_with_colors(
|
||||
self, xmlrpc_proxy: xmlrpc.client.ServerProxy
|
||||
) -> None:
|
||||
"""Test creating multiple objects with different colors."""
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
"""
|
||||
import FreeCAD
|
||||
import Part
|
||||
|
||||
if "AssemblyTestDoc" in FreeCAD.listDocuments():
|
||||
FreeCAD.closeDocument("AssemblyTestDoc")
|
||||
doc = FreeCAD.newDocument("AssemblyTestDoc")
|
||||
|
||||
# Create base plate (gray)
|
||||
base = Part.makeBox(100, 100, 5)
|
||||
base_obj = doc.addObject("Part::Feature", "BasePlate")
|
||||
base_obj.Shape = base
|
||||
base_obj.ViewObject.ShapeColor = (0.5, 0.5, 0.5)
|
||||
|
||||
# Create pillar 1 (red)
|
||||
pillar1 = Part.makeBox(10, 10, 50, FreeCAD.Vector(10, 10, 5))
|
||||
pillar1_obj = doc.addObject("Part::Feature", "Pillar1")
|
||||
pillar1_obj.Shape = pillar1
|
||||
pillar1_obj.ViewObject.ShapeColor = (1.0, 0.0, 0.0)
|
||||
|
||||
# Create pillar 2 (green)
|
||||
pillar2 = Part.makeBox(10, 10, 50, FreeCAD.Vector(80, 10, 5))
|
||||
pillar2_obj = doc.addObject("Part::Feature", "Pillar2")
|
||||
pillar2_obj.Shape = pillar2
|
||||
pillar2_obj.ViewObject.ShapeColor = (0.0, 1.0, 0.0)
|
||||
|
||||
# Create pillar 3 (blue)
|
||||
pillar3 = Part.makeBox(10, 10, 50, FreeCAD.Vector(80, 80, 5))
|
||||
pillar3_obj = doc.addObject("Part::Feature", "Pillar3")
|
||||
pillar3_obj.Shape = pillar3
|
||||
pillar3_obj.ViewObject.ShapeColor = (0.0, 0.0, 1.0)
|
||||
|
||||
# Create pillar 4 (yellow)
|
||||
pillar4 = Part.makeBox(10, 10, 50, FreeCAD.Vector(10, 80, 5))
|
||||
pillar4_obj = doc.addObject("Part::Feature", "Pillar4")
|
||||
pillar4_obj.Shape = pillar4
|
||||
pillar4_obj.ViewObject.ShapeColor = (1.0, 1.0, 0.0)
|
||||
|
||||
# Create top plate (semi-transparent gray)
|
||||
top = Part.makeBox(100, 100, 5, FreeCAD.Vector(0, 0, 55))
|
||||
top_obj = doc.addObject("Part::Feature", "TopPlate")
|
||||
top_obj.Shape = top
|
||||
top_obj.ViewObject.ShapeColor = (0.5, 0.5, 0.5)
|
||||
top_obj.ViewObject.Transparency = 30
|
||||
|
||||
doc.recompute()
|
||||
|
||||
_result_ = {
|
||||
"object_count": len(doc.Objects),
|
||||
"objects": [obj.Name for obj in doc.Objects],
|
||||
"all_visible": all(obj.ViewObject.Visibility for obj in doc.Objects)
|
||||
}
|
||||
""",
|
||||
)
|
||||
assert result["result"]["object_count"] == 6
|
||||
assert "BasePlate" in result["result"]["objects"]
|
||||
assert "TopPlate" in result["result"]["objects"]
|
||||
assert result["result"]["all_visible"] is True
|
||||
|
||||
def test_export_with_screenshot(
|
||||
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, temp_dir: str
|
||||
) -> None:
|
||||
"""Test creating model, setting view, taking screenshot, and exporting."""
|
||||
step_path = Path(temp_dir) / "workflow_export.step"
|
||||
screenshot_path = Path(temp_dir) / "workflow_screenshot.png"
|
||||
|
||||
result = execute_code(
|
||||
xmlrpc_proxy,
|
||||
f"""
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Part
|
||||
import os
|
||||
|
||||
if "WorkflowTestDoc" in FreeCAD.listDocuments():
|
||||
FreeCAD.closeDocument("WorkflowTestDoc")
|
||||
doc = FreeCAD.newDocument("WorkflowTestDoc")
|
||||
|
||||
# Create a simple bracket shape
|
||||
# Base
|
||||
base = Part.makeBox(50, 30, 5)
|
||||
# Vertical support
|
||||
vertical = Part.makeBox(5, 30, 40, FreeCAD.Vector(0, 0, 5))
|
||||
# Top flange with hole
|
||||
top_flange = Part.makeBox(20, 30, 5, FreeCAD.Vector(0, 0, 45))
|
||||
hole = Part.makeCylinder(4, 10, FreeCAD.Vector(10, 15, 40))
|
||||
|
||||
# Combine and cut
|
||||
bracket = base.fuse(vertical).fuse(top_flange).cut(hole)
|
||||
bracket_obj = doc.addObject("Part::Feature", "Bracket")
|
||||
bracket_obj.Shape = bracket
|
||||
bracket_obj.ViewObject.ShapeColor = (0.2, 0.4, 0.8)
|
||||
|
||||
doc.recompute()
|
||||
|
||||
# Set isometric view
|
||||
view = FreeCADGui.ActiveDocument.ActiveView
|
||||
view.viewIsometric()
|
||||
view.fitAll()
|
||||
|
||||
# Take screenshot
|
||||
view.saveImage({screenshot_path!r}, 800, 600, "White")
|
||||
|
||||
# Export to STEP
|
||||
bracket_obj.Shape.exportStep({step_path!r})
|
||||
|
||||
_result_ = {{
|
||||
"bracket_valid": bracket_obj.Shape.isValid(),
|
||||
"bracket_volume": bracket_obj.Shape.Volume,
|
||||
"screenshot_exists": os.path.exists({screenshot_path!r}),
|
||||
"step_exists": os.path.exists({step_path!r})
|
||||
}}
|
||||
""",
|
||||
)
|
||||
assert result["result"]["bracket_valid"] is True
|
||||
assert result["result"]["bracket_volume"] > 0
|
||||
assert result["result"]["screenshot_exists"] is True
|
||||
assert result["result"]["step_exists"] is True
|
||||
assert screenshot_path.exists()
|
||||
assert step_path.exists()
|
||||
Reference in New Issue
Block a user