* ci: add status badges to README * chore: major overhaul - docs, commands & workflows * fix: general fixes and improvements * chore: various updates and fixes * chore: minor fixes
583 lines
16 KiB
Python
583 lines
16 KiB
Python
"""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 freecad::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"
|
|
screenshot_path_str = str(screenshot_path)
|
|
|
|
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_str!r}, 800, 600, "White")
|
|
|
|
_result_ = {{
|
|
"saved": os.path.exists({screenshot_path_str!r}),
|
|
"path": {screenshot_path_str!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"
|
|
step_path_str = str(step_path)
|
|
screenshot_path_str = str(screenshot_path)
|
|
|
|
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_str!r}, 800, 600, "White")
|
|
|
|
# Export to STEP
|
|
bracket_obj.Shape.exportStep({step_path_str!r})
|
|
|
|
_result_ = {{
|
|
"bracket_valid": bracket_obj.Shape.isValid(),
|
|
"bracket_volume": bracket_obj.Shape.Volume,
|
|
"screenshot_exists": os.path.exists({screenshot_path_str!r}),
|
|
"step_exists": os.path.exists({step_path_str!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()
|