Files
freecad-robust-mcp-fc111/tests/integration/test_gui_mode.py
T

580 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 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()