Files
freecad-robust-mcp-fc111/tests/integration/conftest.py
T
Sean P. KaneandGitHub a775852ce9 chrore: major overhaul of docs, commands, and workflows (#23)
* 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
2026-01-07 09:44:45 -08:00

258 lines
8.0 KiB
Python

"""Pytest configuration for integration tests.
This module handles connection checking and provides consolidated skip behavior
when the FreeCAD MCP bridge is not available.
Instance ID Verification:
The FreeCAD MCP bridge generates a unique instance ID at startup which is
printed to stdout. Tests can capture this ID and verify they're connected
to the expected instance using the `bridge_instance_id` fixture or by
calling proxy.get_instance_id().
"""
from __future__ import annotations
import os
import warnings
import xmlrpc.client
from typing import Any
import pytest
# Global flag to track bridge availability (checked once per session)
_bridge_available: bool | None = None
_bridge_error: str | None = None
_bridge_instance_id: str | None = None
_gui_available: bool | None = None
_warning_emitted: bool = False
def _check_bridge_connection() -> tuple[bool, str | None, str | None]:
"""Check if the FreeCAD MCP bridge is available and get its instance ID.
Returns:
Tuple of (is_available, error_message, instance_id)
"""
global _bridge_available, _bridge_error, _bridge_instance_id, _gui_available
if _bridge_available is not None:
return _bridge_available, _bridge_error, _bridge_instance_id
try:
proxy = xmlrpc.client.ServerProxy("http://localhost:9875", allow_none=True)
result: dict[str, Any] = proxy.ping() # type: ignore[assignment]
if result.get("pong"):
_bridge_available = True
_bridge_error = None
# The ping response includes instance_id
_bridge_instance_id = result.get("instance_id")
# Check if GUI is available via get_status
try:
status: dict[str, Any] = proxy.get_status() # type: ignore[assignment]
_gui_available = status.get("gui_available", False)
except Exception:
# If get_status fails, assume headless
_gui_available = False
else:
_bridge_available = False
_bridge_error = "FreeCAD MCP bridge not responding to ping"
_bridge_instance_id = None
_gui_available = None
except ConnectionRefusedError:
_bridge_available = False
_bridge_error = "Connection refused - FreeCAD MCP bridge not running"
_bridge_instance_id = None
_gui_available = None
except Exception as e:
_bridge_available = False
_bridge_error = f"Cannot connect to FreeCAD MCP bridge: {e}"
_bridge_instance_id = None
_gui_available = None
return _bridge_available, _bridge_error, _bridge_instance_id
def is_gui_available() -> bool:
"""Check if FreeCAD GUI is available.
Returns:
True if running in GUI mode, False if headless.
"""
# Ensure bridge check has been performed
_check_bridge_connection()
return _gui_available is True
def is_headless_mode() -> bool:
"""Check if FreeCAD is running in headless mode.
Returns:
True if running in headless mode, False if GUI is available.
"""
return not is_gui_available()
# Skip marker for GUI-only tests
requires_gui = pytest.mark.skipif(
is_headless_mode(),
reason="Test requires FreeCAD GUI mode (running in headless mode)",
)
def pytest_collection_modifyitems(
config: pytest.Config, # noqa: ARG001
items: list[pytest.Item],
) -> None:
"""Skip all integration tests if the bridge is not available.
This runs once during test collection and emits a single warning instead of
per-test skip messages.
"""
global _warning_emitted
# Filter to only integration tests in this directory
integration_tests = [
item for item in items if "tests/integration" in str(item.fspath)
]
if not integration_tests:
return
# Check bridge connection once
is_available, error, _instance_id = _check_bridge_connection()
if not is_available:
# Apply skip marker to all integration tests
skip_marker = pytest.mark.skip(reason="FreeCAD MCP bridge unavailable")
for item in integration_tests:
item.add_marker(skip_marker)
# Emit a single warning (only once)
if not _warning_emitted:
_warning_emitted = True
warnings.warn(
f"Skipping {len(integration_tests)} integration tests: {error}. "
f"Start the bridge with 'just run-gui' or 'just run-headless'.",
pytest.PytestWarning,
stacklevel=1,
)
@pytest.fixture(scope="module")
def xmlrpc_proxy() -> xmlrpc.client.ServerProxy:
"""Create XML-RPC proxy to FreeCAD MCP bridge.
This fixture is shared across all integration test modules.
The connection check has already been performed during collection.
"""
is_available, error, _ = _check_bridge_connection()
if not is_available:
pytest.skip(error or "FreeCAD MCP bridge not available")
return xmlrpc.client.ServerProxy("http://localhost:9875", allow_none=True)
@pytest.fixture(scope="module")
def bridge_instance_id() -> str | None:
"""Get the instance ID of the connected FreeCAD MCP bridge.
This fixture returns the unique instance ID that was generated when
the bridge started. Use this to verify you're connected to the expected
bridge instance.
Returns:
The instance ID string, or None if not available.
"""
is_available, _, instance_id = _check_bridge_connection()
if not is_available:
return None
return instance_id
@pytest.fixture(scope="module")
def expected_bridge_instance_id() -> str | None:
"""Get the expected bridge instance ID from environment variable.
When running tests that start the bridge themselves (e.g., in CI),
the startup script can capture the instance ID from the bridge's
stdout and set it as EXPECTED_BRIDGE_INSTANCE_ID environment variable.
Returns:
The expected instance ID from env, or None if not set.
"""
return os.environ.get("EXPECTED_BRIDGE_INSTANCE_ID")
@pytest.fixture(scope="module")
def freecad_gui_available() -> bool:
"""Check if FreeCAD GUI is available.
This fixture returns True if FreeCAD is running in GUI mode,
False if running in headless mode. Use this to conditionally
skip tests that require GUI features.
Returns:
True if GUI is available, False if headless.
Example:
def test_screenshot(freecad_gui_available):
if not freecad_gui_available:
pytest.skip("Test requires GUI mode")
# ... test that needs GUI
"""
return is_gui_available()
@pytest.fixture(scope="module")
def freecad_is_headless() -> bool:
"""Check if FreeCAD is running in headless mode.
This fixture returns True if FreeCAD is running in headless mode
(no GUI), False if GUI is available.
Returns:
True if headless, False if GUI is available.
Example:
def test_some_feature(freecad_is_headless):
if freecad_is_headless:
pytest.skip("Test requires GUI mode")
# ... test that needs GUI
"""
return is_headless_mode()
def verify_bridge_instance(
proxy: xmlrpc.client.ServerProxy,
expected_id: str | None,
) -> bool:
"""Verify we're connected to the expected bridge instance.
Args:
proxy: XML-RPC proxy to the bridge.
expected_id: Expected instance ID, or None to skip verification.
Returns:
True if verification passed or was skipped (no expected_id).
Raises:
AssertionError: If instance ID doesn't match expected.
"""
if expected_id is None:
return True
result: dict[str, Any] = proxy.get_instance_id() # type: ignore[assignment]
actual_id = result.get("instance_id")
if actual_id != expected_id:
msg = (
f"Bridge instance ID mismatch!\n"
f" Expected: {expected_id}\n"
f" Actual: {actual_id}\n"
f"This may indicate you're connected to a different bridge instance."
)
raise AssertionError(msg)
return True