* 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>
246 lines
9.1 KiB
Plaintext
246 lines
9.1 KiB
Plaintext
# Testing commands
|
|
# Usage: just testing::unit, just testing::integration, etc.
|
|
|
|
# Project root directory (justfile_directory() returns the main justfile's directory)
|
|
project_root := justfile_directory()
|
|
|
|
# Run unit tests only (excludes integration tests)
|
|
unit:
|
|
uv run pytest {{project_root}}/tests/unit
|
|
|
|
# Run tests with coverage (excludes integration tests)
|
|
# Note: Uses bash script to ensure .coverage file is created in project root
|
|
cov:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
cd "{{project_root}}"
|
|
uv run pytest tests/unit --cov=freecad_mcp --cov-report=term-missing --cov-report=html:htmlcov
|
|
|
|
# Run tests without slow markers (excludes integration tests)
|
|
fast:
|
|
uv run pytest {{project_root}}/tests/unit -m "not slow"
|
|
|
|
# Run only integration tests (requires running FreeCAD MCP bridge)
|
|
integration:
|
|
uv run pytest {{project_root}}/tests/integration -v
|
|
|
|
# Run tests with verbose output (excludes integration tests)
|
|
verbose:
|
|
uv run pytest {{project_root}}/tests/unit -v --tb=long
|
|
|
|
# Run all tests including integration (auto-starts FreeCAD headless)
|
|
# Runs unit tests first (no FreeCAD needed), then delegates to integration-freecad-auto
|
|
all:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
echo "Running unit tests..."
|
|
echo ""
|
|
uv run pytest "{{project_root}}/tests/unit" -v
|
|
|
|
echo ""
|
|
echo "Unit tests passed! Now running integration tests..."
|
|
echo ""
|
|
|
|
# Delegate to integration-freecad-auto for FreeCAD lifecycle management
|
|
just testing::integration-freecad-auto
|
|
|
|
# Run tests in watch mode (re-runs on file changes)
|
|
# Note: --config specifies .pytest-watch.cfg to avoid pytest-watch parsing
|
|
# pyproject.toml as INI (it fails on valid TOML [[array.tables]] syntax)
|
|
watch:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
echo ""
|
|
echo "========================================"
|
|
echo " WATCH MODE - Running initial tests..."
|
|
echo "========================================"
|
|
echo ""
|
|
|
|
uv run pytest-watch \
|
|
--config "{{project_root}}/.pytest-watch.cfg" \
|
|
--afterrun "echo '' && echo '========================================' && echo ' WATCHING for file changes...' && echo ' Press Ctrl+C to exit watch mode' && echo '========================================' && echo ''" \
|
|
"{{project_root}}/tests/unit"
|
|
|
|
# Run integration tests with automatic FreeCAD headless startup
|
|
integration-freecad-auto:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Track whether we started FreeCAD (so cleanup knows to stop it)
|
|
STARTED_FREECAD=false
|
|
|
|
# Helper function to kill process on a port (with fallback for systems without lsof)
|
|
# Usage: kill_port PORT [SIGNAL]
|
|
# Examples: kill_port 9875 (sends SIGTERM)
|
|
# kill_port 9875 -9 (sends SIGKILL)
|
|
kill_port() {
|
|
local port=$1
|
|
local signal=${2:--TERM} # Default to SIGTERM if no signal specified
|
|
local pids=""
|
|
|
|
if command -v lsof &>/dev/null; then
|
|
# Collect PIDs first, then kill only if non-empty
|
|
pids=$(lsof -ti:"$port" 2>/dev/null || true)
|
|
if [[ -n "$pids" ]]; then
|
|
echo "$pids" | xargs kill "$signal" 2>/dev/null || true
|
|
fi
|
|
elif command -v fuser &>/dev/null; then
|
|
# fuser -k sends SIGKILL by default; use --signal for others
|
|
# Check if port is in use first
|
|
if fuser "$port/tcp" 2>/dev/null; then
|
|
if [ "$signal" = "-9" ] || [ "$signal" = "-KILL" ]; then
|
|
fuser -k "$port/tcp" 2>/dev/null || true
|
|
else
|
|
fuser -k --signal "${signal#-}" "$port/tcp" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Cleanup function to ensure FreeCAD is stopped
|
|
# We kill by port since the subshell approach makes PID tracking unreliable
|
|
cleanup() {
|
|
if [ "$STARTED_FREECAD" = true ]; then
|
|
echo ""
|
|
echo "Stopping FreeCAD..."
|
|
# Kill processes on our ports - these are the ones we started
|
|
kill_port 9875
|
|
kill_port 9876
|
|
# Wait briefly for graceful shutdown
|
|
sleep 1
|
|
# Force kill if still running
|
|
kill_port 9875 -9
|
|
kill_port 9876 -9
|
|
fi
|
|
}
|
|
|
|
# Set trap: EXIT runs cleanup on normal exit, INT/TERM run cleanup then exit
|
|
trap cleanup EXIT
|
|
trap 'cleanup; exit 130' INT
|
|
trap 'cleanup; exit 143' TERM
|
|
|
|
echo "Starting FreeCAD headless server for integration tests..."
|
|
echo ""
|
|
|
|
# Check if a bridge is already running and responsive
|
|
if curl -s --connect-timeout 1 --max-time 1 http://localhost:9875 > /dev/null 2>&1; then
|
|
# Try to ping - if it responds, there's a healthy bridge already running
|
|
if uv run python -c "import socket; socket.setdefaulttimeout(2); import xmlrpc.client; print(xmlrpc.client.ServerProxy('http://localhost:9875').ping())" 2>/dev/null | grep -q "pong"; then
|
|
echo "ERROR: A FreeCAD MCP bridge is already running on port 9875."
|
|
echo ""
|
|
echo "Options:"
|
|
echo " 1. Use 'just testing::integration' to run tests against the existing bridge"
|
|
echo " 2. Stop the existing FreeCAD instance and try again"
|
|
echo " 3. If this is a zombie process, run: just testing::kill-bridge"
|
|
exit 1
|
|
else
|
|
# Port is bound but not responding to ping - likely a zombie
|
|
echo "WARNING: Port 9875 is bound but not responding (zombie process?)"
|
|
echo "Attempting to kill zombie process..."
|
|
kill_port 9875 -9
|
|
kill_port 9876 -9
|
|
sleep 2
|
|
fi
|
|
fi
|
|
|
|
# Mark that we're starting FreeCAD (for cleanup)
|
|
STARTED_FREECAD=true
|
|
|
|
# Start FreeCAD headless in background
|
|
# Redirect stderr to log file (not /dev/null) so startup failures are visible
|
|
# Background process won't fail the script when killed by cleanup trap
|
|
FREECAD_LOG="{{project_root}}/freecad-headless.log"
|
|
just freecad::run-headless 2>"$FREECAD_LOG" &
|
|
|
|
# Give FreeCAD time to start the XML-RPC server
|
|
echo "Waiting for FreeCAD MCP bridge to start..."
|
|
sleep 5
|
|
|
|
# Check if the bridge is ready (verify XML-RPC ping, not just port open)
|
|
MAX_RETRIES=30
|
|
RETRY_COUNT=0
|
|
while ! uv run python -c "import socket; socket.setdefaulttimeout(2); import xmlrpc.client; print(xmlrpc.client.ServerProxy('http://localhost:9875').ping())" 2>/dev/null | grep -q "pong"; do
|
|
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
|
|
echo "ERROR: FreeCAD MCP bridge did not start within timeout"
|
|
echo "Check log file for details: $FREECAD_LOG"
|
|
if [ -f "$FREECAD_LOG" ]; then
|
|
echo "--- Last 20 lines of log ---"
|
|
tail -20 "$FREECAD_LOG"
|
|
fi
|
|
exit 1
|
|
fi
|
|
echo " Waiting... ($RETRY_COUNT/$MAX_RETRIES)"
|
|
sleep 1
|
|
done
|
|
|
|
echo "FreeCAD MCP bridge is ready!"
|
|
echo ""
|
|
|
|
# Run integration tests
|
|
TEST_EXIT_CODE=0
|
|
uv run pytest "{{project_root}}/tests/integration" -v || TEST_EXIT_CODE=$?
|
|
|
|
# Cleanup is handled by trap
|
|
exit $TEST_EXIT_CODE
|
|
|
|
# =============================================================================
|
|
# Just Command Tests
|
|
# =============================================================================
|
|
|
|
# Run just command syntax tests (fast, validates all commands parse correctly)
|
|
just-syntax:
|
|
uv run pytest {{project_root}}/tests/just_commands -m "just_syntax" -v
|
|
|
|
# Run just command runtime tests (slower, actually executes commands)
|
|
just-runtime:
|
|
uv run pytest {{project_root}}/tests/just_commands -m "just_runtime and not slow" -v
|
|
|
|
# Run all just command tests
|
|
just-all:
|
|
uv run pytest {{project_root}}/tests/just_commands -v
|
|
|
|
# Run just command release tests (tests release commands with cleanup)
|
|
just-release:
|
|
uv run pytest {{project_root}}/tests/just_commands -m "just_release" -v
|
|
|
|
# =============================================================================
|
|
# Bridge Management
|
|
# =============================================================================
|
|
|
|
# Kill any zombie FreeCAD MCP bridge processes on the default ports
|
|
kill-bridge:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
echo "Killing any processes on MCP bridge ports (9875, 9876)..."
|
|
|
|
# Helper function to kill process on a port (with fallback for systems without lsof)
|
|
# Collects PIDs first to avoid calling kill with no arguments
|
|
kill_port() {
|
|
local port=$1
|
|
local pids=""
|
|
|
|
if command -v lsof &>/dev/null; then
|
|
# Collect PIDs first, then kill only if non-empty
|
|
pids=$(lsof -ti:"$port" 2>/dev/null || true)
|
|
if [[ -n "$pids" ]]; then
|
|
echo "$pids" | xargs kill -9 2>/dev/null || true
|
|
fi
|
|
elif command -v fuser &>/dev/null; then
|
|
# Check if port is in use first
|
|
if fuser "$port/tcp" 2>/dev/null; then
|
|
fuser -k -9 "$port/tcp" 2>/dev/null || true
|
|
fi
|
|
else
|
|
echo "Warning: Neither lsof nor fuser available, cannot kill port $port"
|
|
fi
|
|
}
|
|
|
|
kill_port 9875
|
|
kill_port 9876
|
|
|
|
echo "Done."
|