Files
Sean P. KaneandGitHub f14ab8177d Fix: rename workbench and prepare for release (#27)
* chore: rename workbench to FreecadRobustMCPBridge

* fix: stdio cleanup and bug fix for JSON RPC

* fix: update FreeCAD MCP bridge and tests

* test: Fix GUI Integration tests and other issues

* fix: unit tests in CI and a few other things

* docs: generate GitHub pages site and link to it.

* fix: General code improvements and DRY refactoring

* chore: General cleanup

* chore: small fixes

* docs: cleanup

* docs: fixes

* docs: small corrections

* docs: fix

* test: release check

* bump: workbench v0.6.0

* chore: bump macros to 0.6.0

* docs: Release improvements

* refactor: improve DRYness of code
2026-01-12 09:30:05 -08:00

830 lines
22 KiB
Python

"""Integration tests for FreeCAD Robust MCP Bridge functionality.
These tests verify that the MCP bridge works correctly with FreeCAD, including
object creation, manipulation, and export functionality. Most tests work in
both GUI and headless modes, with specific tests marked as headless-only.
Note: These tests require a running FreeCAD server.
Start with: just freecad::run-gui (GUI mode)
or: just freecad::run-headless (headless mode)
To run these tests:
pytest tests/integration/test_headless_mode.py -v
"""
from __future__ import annotations
import tempfile
import time
from pathlib import Path
from typing import TYPE_CHECKING, Any
import pytest
from tests.integration.conftest import requires_headless
if TYPE_CHECKING:
import xmlrpc.client
from collections.abc import Generator
# Mark all tests in this module as integration tests
pytestmark = pytest.mark.integration
# Note: xmlrpc_proxy fixture is defined in conftest.py
def _unique_suffix() -> str:
"""Generate a unique suffix using timestamp."""
return time.strftime("%Y%m%d%H%M%S")
@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
@pytest.fixture(scope="module")
def unique_suffix() -> str:
"""Generate a unique suffix for document names in this test session.
Uses timestamp format YYYYMMDDHHMMSS to ensure unique names across runs.
"""
return _unique_suffix()
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
# Common code snippet for centering viewport in GUI mode
CENTER_VIEWPORT_CODE = """
# Center viewport for GUI mode recording
if FreeCAD.GuiUp:
import FreeCADGui
if FreeCADGui.ActiveDocument and FreeCADGui.ActiveDocument.ActiveView:
FreeCADGui.ActiveDocument.ActiveView.viewIsometric()
FreeCADGui.ActiveDocument.ActiveView.fitAll()
"""
def center_viewport(proxy: xmlrpc.client.ServerProxy) -> None:
"""Center all objects in the viewport if GUI is available.
Call this after creating/modifying objects to keep them centered.
Safe to call in headless mode - it will simply do nothing.
"""
proxy.execute( # type: ignore[union-attr]
f"""
import FreeCAD
{CENTER_VIEWPORT_CODE}
_result_ = True
"""
)
class TestHeadlessConnection:
"""Tests for basic headless mode connectivity."""
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
@requires_headless
def test_headless_mode_detected(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy
) -> None:
"""Test that FreeCAD is running in headless mode (GuiUp=False).
This test is skipped when FreeCAD is running in GUI mode.
"""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
_result_ = {"gui_up": FreeCAD.GuiUp}
""",
)
# In headless mode, GuiUp should be False (0)
assert result["result"]["gui_up"] == 0 or result["result"]["gui_up"] is False
class TestDocumentManagement:
"""Tests for document creation and management in headless mode."""
def test_create_document(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, unique_suffix: str
) -> None:
"""Test creating a new document."""
doc_name = f"TestDoc_{unique_suffix}"
result = execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
doc_name = {doc_name!r}
# Close if it already exists (from a previous failed run)
if doc_name in FreeCAD.listDocuments():
FreeCAD.closeDocument(doc_name)
doc = FreeCAD.newDocument(doc_name)
_result_ = {{"name": doc.Name, "object_count": len(doc.Objects)}}
""",
)
assert result["result"]["name"] == doc_name
assert result["result"]["object_count"] == 0
def test_list_documents(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, unique_suffix: str
) -> None:
"""Test listing open documents."""
doc_name = f"ListTestDoc_{unique_suffix}"
# First create a document
execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
doc_name = {doc_name!r}
if doc_name in FreeCAD.listDocuments():
FreeCAD.closeDocument(doc_name)
FreeCAD.newDocument(doc_name)
_result_ = True
""",
)
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
docs = list(FreeCAD.listDocuments().keys())
_result_ = {"documents": docs, "count": len(docs)}
""",
)
assert result["result"]["count"] >= 1
def test_close_document(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, unique_suffix: str
) -> None:
"""Test closing a document."""
doc_name = f"ToClose_{unique_suffix}"
# Create a document to close
execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
doc_name = {doc_name!r}
if doc_name in FreeCAD.listDocuments():
FreeCAD.closeDocument(doc_name)
doc = FreeCAD.newDocument(doc_name)
_result_ = True
""",
)
result = execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
doc_name = {doc_name!r}
FreeCAD.closeDocument(doc_name)
_result_ = {{"closed": doc_name not in FreeCAD.listDocuments()}}
""",
)
assert result["result"]["closed"] is True
class TestPrimitiveCreation:
"""Tests for creating primitive shapes."""
@pytest.fixture(autouse=True)
def setup_document(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, unique_suffix: str
) -> None:
"""Create a fresh document for each test."""
doc_name = f"PrimitiveTestDoc_{unique_suffix}"
execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
doc_name = {doc_name!r}
# Close any existing document with this name
if doc_name in FreeCAD.listDocuments():
FreeCAD.closeDocument(doc_name)
doc = FreeCAD.newDocument(doc_name)
# Set up initial view for GUI mode
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.viewIsometric()
_result_ = True
""",
)
def test_create_box(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test creating a Part::Box primitive."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
doc = FreeCAD.ActiveDocument
box = Part.makeBox(10, 20, 30)
obj = doc.addObject("Part::Feature", "TestBox")
obj.Shape = box
# Center viewport immediately after adding object (before it renders off-center)
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.fitAll()
doc.recompute()
# Final viewport adjustment
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.ActiveView.fitAll()
_result_ = {
"name": obj.Name,
"volume": obj.Shape.Volume,
"valid": obj.Shape.isValid()
}
""",
)
assert result["result"]["name"] == "TestBox"
# Volume should be 10 * 20 * 30 = 6000
assert abs(result["result"]["volume"] - 6000.0) < 0.01
assert result["result"]["valid"] is True
def test_create_cylinder(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test creating a Part::Cylinder primitive."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
import math
doc = FreeCAD.ActiveDocument
cylinder = Part.makeCylinder(5, 20)
obj = doc.addObject("Part::Feature", "TestCylinder")
obj.Shape = cylinder
# Center viewport immediately after adding object
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.fitAll()
doc.recompute()
# Final viewport adjustment
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.ActiveView.fitAll()
expected_volume = math.pi * 5**2 * 20
_result_ = {
"name": obj.Name,
"volume": obj.Shape.Volume,
"expected_volume": expected_volume,
"valid": obj.Shape.isValid()
}
""",
)
assert result["result"]["name"] == "TestCylinder"
# Volume should be π * r² * h = π * 25 * 20 ≈ 1570.8
assert (
abs(result["result"]["volume"] - result["result"]["expected_volume"]) < 0.1
)
assert result["result"]["valid"] is True
def test_create_sphere(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test creating a Part::Sphere primitive."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
import math
doc = FreeCAD.ActiveDocument
sphere = Part.makeSphere(10)
obj = doc.addObject("Part::Feature", "TestSphere")
obj.Shape = sphere
# Center viewport immediately after adding object
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.fitAll()
doc.recompute()
# Final viewport adjustment
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.ActiveView.fitAll()
expected_volume = (4/3) * math.pi * 10**3
_result_ = {
"name": obj.Name,
"volume": obj.Shape.Volume,
"expected_volume": expected_volume,
"valid": obj.Shape.isValid()
}
""",
)
assert result["result"]["name"] == "TestSphere"
# Volume should be (4/3) * π * r³ ≈ 4188.79
assert (
abs(result["result"]["volume"] - result["result"]["expected_volume"]) < 1.0
)
assert result["result"]["valid"] is True
def test_create_cone(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test creating a Part::Cone primitive."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
doc = FreeCAD.ActiveDocument
cone = Part.makeCone(10, 0, 20) # Base radius=10, top radius=0, height=20
obj = doc.addObject("Part::Feature", "TestCone")
obj.Shape = cone
# Center viewport immediately after adding object
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.fitAll()
doc.recompute()
# Final viewport adjustment
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.ActiveView.fitAll()
_result_ = {
"name": obj.Name,
"volume": obj.Shape.Volume,
"valid": obj.Shape.isValid()
}
""",
)
assert result["result"]["name"] == "TestCone"
# Volume of a cone = (1/3) * π * r² * h ≈ 2094.4
assert result["result"]["volume"] > 2000
assert result["result"]["valid"] is True
def test_create_torus(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test creating a Part::Torus primitive."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
doc = FreeCAD.ActiveDocument
torus = Part.makeTorus(20, 5) # Major radius=20, minor radius=5
obj = doc.addObject("Part::Feature", "TestTorus")
obj.Shape = torus
# Center viewport immediately after adding object
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.fitAll()
doc.recompute()
# Final viewport adjustment
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.ActiveView.fitAll()
_result_ = {
"name": obj.Name,
"volume": obj.Shape.Volume,
"valid": obj.Shape.isValid()
}
""",
)
assert result["result"]["name"] == "TestTorus"
assert result["result"]["volume"] > 0
assert result["result"]["valid"] is True
class TestBooleanOperations:
"""Tests for boolean operations."""
@pytest.fixture(autouse=True)
def setup_document(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, unique_suffix: str
) -> None:
"""Create a fresh document for each test."""
doc_name = f"BooleanTestDoc_{unique_suffix}"
execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
doc_name = {doc_name!r}
if doc_name in FreeCAD.listDocuments():
FreeCAD.closeDocument(doc_name)
doc = FreeCAD.newDocument(doc_name)
# Set up initial view for GUI mode
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.viewIsometric()
_result_ = True
""",
)
def test_boolean_fuse(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test boolean fuse (union) operation."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
doc = FreeCAD.ActiveDocument
# Create two overlapping boxes
box1 = Part.makeBox(10, 10, 10)
box2 = Part.makeBox(10, 10, 10, FreeCAD.Vector(5, 0, 0))
obj1 = doc.addObject("Part::Feature", "Box1")
obj1.Shape = box1
obj2 = doc.addObject("Part::Feature", "Box2")
obj2.Shape = box2
# Center viewport after adding initial objects
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.fitAll()
# Fuse them
fused = box1.fuse(box2)
obj_fused = doc.addObject("Part::Feature", "Fused")
obj_fused.Shape = fused
doc.recompute()
# Final viewport adjustment
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.ActiveView.fitAll()
# Fused volume should be less than 2000 (two boxes) due to overlap
_result_ = {
"fused_volume": obj_fused.Shape.Volume,
"valid": obj_fused.Shape.isValid()
}
""",
)
# Two 10x10x10 boxes overlapping by 5mm = 2000 - 500 = 1500
assert result["result"]["fused_volume"] == pytest.approx(1500.0, rel=0.01)
assert result["result"]["valid"] is True
def test_boolean_cut(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test boolean cut (subtract) operation."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
doc = FreeCAD.ActiveDocument
# Create a box and a cylinder to cut from it
box = Part.makeBox(20, 20, 20)
cylinder = Part.makeCylinder(5, 30, FreeCAD.Vector(10, 10, -5))
# Cut cylinder from box
cut = box.cut(cylinder)
obj_cut = doc.addObject("Part::Feature", "Cut")
obj_cut.Shape = cut
# Center viewport immediately after adding object
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.fitAll()
doc.recompute()
# Final viewport adjustment
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.ActiveView.fitAll()
box_volume = 20 * 20 * 20
import math
cylinder_volume_in_box = math.pi * 5**2 * 20 # Only 20mm of cylinder is in box
_result_ = {
"cut_volume": obj_cut.Shape.Volume,
"expected_volume": box_volume - cylinder_volume_in_box,
"valid": obj_cut.Shape.isValid()
}
""",
)
# Volume should be box - cylinder intersection
assert (
abs(result["result"]["cut_volume"] - result["result"]["expected_volume"])
< 10
)
assert result["result"]["valid"] is True
class TestObjectManipulation:
"""Tests for object manipulation."""
@pytest.fixture(autouse=True)
def setup_document(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, unique_suffix: str
) -> None:
"""Create a fresh document with a test box."""
doc_name = f"ManipTestDoc_{unique_suffix}"
execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
import Part
doc_name = {doc_name!r}
if doc_name in FreeCAD.listDocuments():
FreeCAD.closeDocument(doc_name)
doc = FreeCAD.newDocument(doc_name)
box = Part.makeBox(10, 10, 10)
obj = doc.addObject("Part::Feature", "ManipBox")
obj.Shape = box
doc.recompute()
# Center viewport for GUI mode recording
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.viewIsometric()
FreeCADGui.ActiveDocument.ActiveView.fitAll()
_result_ = True
""",
)
def test_set_placement_position(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy
) -> None:
"""Test setting object position."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
doc = FreeCAD.ActiveDocument
obj = doc.getObject("ManipBox")
# Move the object
obj.Placement.Base = FreeCAD.Vector(100, 200, 300)
# Center viewport for GUI mode recording
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.viewIsometric()
FreeCADGui.ActiveDocument.ActiveView.fitAll()
_result_ = {
"x": obj.Placement.Base.x,
"y": obj.Placement.Base.y,
"z": obj.Placement.Base.z
}
""",
)
assert result["result"]["x"] == 100.0
assert result["result"]["y"] == 200.0
assert result["result"]["z"] == 300.0
def test_set_placement_rotation(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy
) -> None:
"""Test setting object rotation."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
doc = FreeCAD.ActiveDocument
obj = doc.getObject("ManipBox")
# Rotate the object 45 degrees around Z axis
obj.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 45)
doc.recompute()
# Center viewport for GUI mode recording
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.viewIsometric()
FreeCADGui.ActiveDocument.ActiveView.fitAll()
euler = obj.Placement.Rotation.toEuler()
_result_ = {
"rotation_z": euler[0], # Yaw
"valid": obj.Shape.isValid()
}
""",
)
# Check rotation is approximately 45 degrees
assert abs(result["result"]["rotation_z"] - 45.0) < 0.1
assert result["result"]["valid"] is True
class TestExportOperations:
"""Tests for export functionality."""
@pytest.fixture(autouse=True)
def setup_document(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, temp_dir: str, unique_suffix: str
) -> None:
"""Create a document with objects for export tests."""
doc_name = f"ExportTestDoc_{unique_suffix}"
execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
import Part
doc_name = {doc_name!r}
if doc_name in FreeCAD.listDocuments():
FreeCAD.closeDocument(doc_name)
doc = FreeCAD.newDocument(doc_name)
# Create a simple box
box = Part.makeBox(10, 10, 10)
obj = doc.addObject("Part::Feature", "ExportBox")
obj.Shape = box
doc.recompute()
# Center viewport for GUI mode recording
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.viewIsometric()
FreeCADGui.ActiveDocument.ActiveView.fitAll()
_result_ = True
""",
)
self.temp_dir = temp_dir
def test_export_step(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, temp_dir: str
) -> None:
"""Test exporting to STEP format."""
step_path = Path(temp_dir) / "test_export.step"
step_path_str = str(step_path)
result = execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
doc = FreeCAD.ActiveDocument
obj = doc.getObject("ExportBox")
obj.Shape.exportStep({step_path_str!r})
import os
_result_ = {{"exported": os.path.exists({step_path_str!r})}}
""",
)
assert result["result"]["exported"] is True
assert step_path.exists()
def test_save_fcstd(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, temp_dir: str
) -> None:
"""Test saving as FreeCAD native format."""
fcstd_path = Path(temp_dir) / "test_save.FCStd"
fcstd_path_str = str(fcstd_path)
result = execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
doc = FreeCAD.ActiveDocument
doc.saveAs({fcstd_path_str!r})
import os
_result_ = {{
"saved": os.path.exists({fcstd_path_str!r}),
"path": {fcstd_path_str!r}
}}
""",
)
assert result["result"]["saved"] is True
assert fcstd_path.exists()
@requires_headless
class TestGUIFeaturesInHeadless:
"""Tests to verify GUI features fail gracefully in headless mode.
These tests are skipped when FreeCAD is running in GUI mode.
"""
def test_gui_features_not_available(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy
) -> None:
"""Test that GUI-only features return appropriate errors."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
gui_available = FreeCAD.GuiUp
if not gui_available:
_result_ = {
"success": False,
"error": "GUI not available in headless mode",
"gui_up": False
}
else:
_result_ = {
"success": True,
"gui_up": True
}
""",
)
# In headless mode, GUI should not be available
assert result["result"]["gui_up"] is False
class TestComplexWorkflow:
"""Tests for complex multi-step workflows."""
def test_parametric_modeling_workflow(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, unique_suffix: str
) -> None:
"""Test a complete parametric modeling workflow."""
doc_name = f"WorkflowTestDoc_{unique_suffix}"
result = execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
import Part
# Create document
doc_name = {doc_name!r}
if doc_name in FreeCAD.listDocuments():
FreeCAD.closeDocument(doc_name)
doc = FreeCAD.newDocument(doc_name)
# Step 1: Create base box
base_box = Part.makeBox(50, 50, 10)
base_obj = doc.addObject("Part::Feature", "Base")
base_obj.Shape = base_box
# Step 2: Create cylinder to cut
cylinder = Part.makeCylinder(5, 20, FreeCAD.Vector(25, 25, -5))
# Step 3: Cut hole in base
result_shape = base_box.cut(cylinder)
final_obj = doc.addObject("Part::Feature", "BaseWithHole")
final_obj.Shape = result_shape
# Step 4: Add fillet (rounded edges) - using Part API
# Select edges and apply fillet
try:
filleted = final_obj.Shape.makeFillet(2, final_obj.Shape.Edges[:4])
fillet_obj = doc.addObject("Part::Feature", "Filleted")
fillet_obj.Shape = filleted
fillet_success = True
except Exception:
fillet_success = False
# Center viewport immediately after adding objects (before recompute)
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.ActiveDocument.ActiveView.viewIsometric()
FreeCADGui.ActiveDocument.ActiveView.fitAll()
doc.recompute()
# Final viewport adjustment after recompute
if FreeCAD.GuiUp:
FreeCADGui.ActiveDocument.ActiveView.fitAll()
_result_ = {{
"objects": [obj.Name for obj in doc.Objects],
"final_volume": final_obj.Shape.Volume,
"final_valid": final_obj.Shape.isValid(),
"fillet_success": fillet_success
}}
""",
)
assert "Base" in result["result"]["objects"]
assert "BaseWithHole" in result["result"]["objects"]
assert result["result"]["final_valid"] is True
# Volume should be base - cylinder hole
assert result["result"]["final_volume"] > 0