* fix: lots of fixes and name refactoring * feat: Add workbench preferences * fix: MCP bridge status widget and just command fixes * fix(tests): Use the correct mesa-glx package * fix(ci): Add fontconfig to GUI test dependencies FreeCAD GUI was failing to start with: "Fontconfig error: Cannot load default config file: No such file" Added fontconfig and fonts-dejavu-core packages to the GUI test job dependencies to resolve the font configuration issue. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(addon): Extract path utilities into shared module Create path_utils.py module that consolidates duplicated path-finding logic from commands.py and InitGui.py: - get_addon_path(): Find addon directory with caching and fallbacks - get_icon_path(): Get full path to an icon file - get_icons_dir(): Get path to icons directory - get_workbench_icon(): Get path to workbench main icon This removes ~100 lines of duplicated code while preserving the same behavior including _addon_path_cache and all fallback methods. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(addon): Prevent stale plugin state on startup failure The StartMCPBridgeCommand.Activated method could leave _mcp_plugin in a partially initialized state if FreecadMCPPlugin.start() failed after the plugin was instantiated. Changes: - Create plugin in a local variable first - Only assign to _mcp_plugin after start() succeeds - Explicitly clear _mcp_plugin and _running_config in exception handlers to ensure clean state for subsequent retry attempts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Lot of broad improvements * fix(ci): Use blocking headless_server.py for GUI tests The GUI test was using startup_bridge.py which is non-blocking (designed for interactive use). For CI, even in GUI mode, we need the blocking headless_server.py that calls run_forever() to keep FreeCAD running. GUI features are still available since we use the 'freecad' executable instead of 'freecadcmd'. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(addon): Rename headless_server.py to blocking_bridge.py The old name was misleading because: - It works with both GUI (freecad) and headless (freecadcmd) modes - The key characteristic is that it BLOCKS with run_forever() New naming convention clarifies the difference: - blocking_bridge.py: Starts bridge and blocks (for CI, servers) - startup_bridge.py: Starts bridge and returns (for interactive GUI) Updated all references across: - GitHub workflow (macro-test.yaml) - Just commands (freecad.just) - Unit tests (test_addon_structure.py) - Documentation (5 files) - CLAUDE.md Also improved the script to detect GUI mode dynamically using FreeCAD.GuiUp and display the appropriate status message. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(just): Remove erroneous rm of startup_bridge.py on error The startup script is now a permanent source file in the repository, not a generated temporary file. The rm -f would have deleted source code if FreeCAD wasn't found. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: General improvements * fix: Lots of general fixes and only stable to PyPi * fix: small cleanup * fix: Small fixes and hopefully fixes the GUI tests * fix: Add proper library paths for FreeCAD GUI in CI - Create wrapper scripts instead of symlinks for AppImage binaries - Set LD_LIBRARY_PATH, QT_PLUGIN_PATH for GUI mode - Add diagnostic output to identify startup failures * fix: Use apprun for GUI tests in CI * fix: Improving Xvfb tests * fix: GUI tests worlk * chore: remove invalid --no-splash comments * fix: ARM64 architecture support and other fixes * fix: cleanup * test: just commands test suite * test: improve just command tests * fix: more general improvements * fix: more cleanup * fix: more updates * fix: small tweaks --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
128 lines
4.3 KiB
Python
128 lines
4.3 KiB
Python
"""Tests for docker module just commands.
|
|
|
|
These tests verify that Docker-related commands work correctly.
|
|
Some tests require Docker to be running.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import subprocess
|
|
from typing import TYPE_CHECKING, ClassVar
|
|
|
|
import pytest
|
|
|
|
from tests.just_commands.conftest import assert_command_executed
|
|
|
|
if TYPE_CHECKING:
|
|
from tests.just_commands.conftest import JustRunner
|
|
|
|
# Docker image name used in tests - defined once for maintainability
|
|
DOCKER_IMAGE_NAME = "freecad-robust-mcp"
|
|
|
|
# Mapping of commands to their required arguments for dry-run testing
|
|
COMMAND_ARG_MAP: dict[str, tuple[str, ...]] = {
|
|
"docker::build-tag": ("test",),
|
|
"docker::build-push": ("test",),
|
|
"docker::scan-sarif": ("test.sarif",),
|
|
"docker::run-env": ("-e", "TEST=1"),
|
|
"docker::gui-test-cmd": ("echo", "test"),
|
|
}
|
|
|
|
|
|
def docker_available() -> bool:
|
|
"""Check if Docker is available and running."""
|
|
if not shutil.which("docker"):
|
|
return False
|
|
try:
|
|
# S603, S607: docker is a well-known command, safe in test context
|
|
result = subprocess.run(
|
|
["docker", "info"], # noqa: S607
|
|
capture_output=True,
|
|
timeout=10,
|
|
check=False,
|
|
)
|
|
return result.returncode == 0
|
|
except (subprocess.TimeoutExpired, OSError):
|
|
return False
|
|
|
|
|
|
class TestDockerSyntax:
|
|
"""Syntax validation tests for docker commands."""
|
|
|
|
DOCKER_COMMANDS: ClassVar[list[str]] = [
|
|
"docker::build",
|
|
"docker::build-tag",
|
|
"docker::build-multi",
|
|
"docker::build-push",
|
|
"docker::build-load",
|
|
"docker::run",
|
|
"docker::run-env",
|
|
"docker::shell",
|
|
"docker::inspect",
|
|
"docker::clean",
|
|
"docker::clean-all",
|
|
"docker::scan",
|
|
"docker::scan-strict",
|
|
"docker::scan-sarif",
|
|
"docker::setup-buildx",
|
|
"docker::test",
|
|
"docker::build-gui-test",
|
|
"docker::gui-test-shell",
|
|
"docker::gui-test-run",
|
|
"docker::gui-test-cmd",
|
|
"docker::gui-test",
|
|
]
|
|
|
|
@pytest.mark.just_syntax
|
|
@pytest.mark.parametrize("command", DOCKER_COMMANDS)
|
|
def test_docker_command_syntax(self, just: JustRunner, command: str) -> None:
|
|
"""Docker command should have valid syntax."""
|
|
# Get args from mapping, or empty tuple for commands without required args
|
|
args = COMMAND_ARG_MAP.get(command, ())
|
|
result = just.dry_run(command, *args)
|
|
assert result.success, f"Syntax error in '{command}': {result.stderr}"
|
|
|
|
|
|
@pytest.mark.requires_docker
|
|
class TestDockerRuntime:
|
|
"""Runtime tests for docker commands (require Docker)."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def skip_if_no_docker(self) -> None:
|
|
"""Skip tests if Docker is not available."""
|
|
if not docker_available():
|
|
pytest.skip("Docker not available")
|
|
|
|
@pytest.mark.just_runtime
|
|
@pytest.mark.slow
|
|
def test_build_runs(
|
|
self, just: JustRunner, docker_image_cleanup: list[str]
|
|
) -> None:
|
|
"""Docker build should run successfully."""
|
|
docker_image_cleanup.append(DOCKER_IMAGE_NAME)
|
|
# 10 minute timeout: Docker builds can be slow, especially on first run
|
|
# when base images need to be pulled and dependencies compiled
|
|
result = just.run("docker::build", timeout=600)
|
|
assert result.success, f"Docker build failed: {result.stderr}"
|
|
|
|
@pytest.mark.just_runtime
|
|
def test_inspect_runs(self, just: JustRunner) -> None:
|
|
"""Docker inspect should run (may fail if no image)."""
|
|
result = just.run("docker::inspect", timeout=30)
|
|
# May fail if image doesn't exist, but should run without missing deps
|
|
assert_command_executed(result, "docker::inspect")
|
|
|
|
@pytest.mark.just_runtime
|
|
def test_clean_runs(self, just: JustRunner) -> None:
|
|
"""Docker clean should run."""
|
|
result = just.run("docker::clean", timeout=60)
|
|
# May fail if no images to clean, but should run without missing deps
|
|
assert_command_executed(result, "docker::clean")
|
|
|
|
@pytest.mark.just_runtime
|
|
def test_setup_buildx_runs(self, just: JustRunner) -> None:
|
|
"""Setup buildx should run."""
|
|
result = just.run("docker::setup-buildx", timeout=120)
|
|
assert result.success, f"Setup buildx failed: {result.stderr}"
|