* 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>
291 lines
8.1 KiB
Bash
Executable File
291 lines
8.1 KiB
Bash
Executable File
#!/bin/bash
|
|
# Run GUI tests with Xvfb + openbox - for testing FreeCAD GUI functionality
|
|
#
|
|
# FreeCAD GUI requires a window manager to generate expose/configure events.
|
|
# Without a WM, the GUI binary hangs during Qt initialization.
|
|
#
|
|
# Solution: Xvfb + openbox + xdotool events
|
|
#
|
|
set -e
|
|
|
|
echo "========================================"
|
|
echo "FreeCAD GUI Test Runner"
|
|
echo "========================================"
|
|
|
|
# Setup XDG_RUNTIME_DIR to avoid Qt warnings
|
|
export XDG_RUNTIME_DIR=/tmp/runtime-root
|
|
mkdir -p "$XDG_RUNTIME_DIR"
|
|
chmod 700 "$XDG_RUNTIME_DIR"
|
|
|
|
# Start Xvfb if not already running
|
|
if ! pgrep -x Xvfb > /dev/null; then
|
|
echo "Starting Xvfb..."
|
|
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
|
|
XVFB_PID=$!
|
|
sleep 2
|
|
if ! ps -p $XVFB_PID > /dev/null; then
|
|
echo "ERROR: Xvfb failed to start"
|
|
exit 1
|
|
fi
|
|
echo "Xvfb started on display :99 (PID: $XVFB_PID)"
|
|
else
|
|
echo "Xvfb already running"
|
|
fi
|
|
|
|
export DISPLAY=:99
|
|
export QT_QPA_PLATFORM=xcb
|
|
export LIBGL_ALWAYS_SOFTWARE=1
|
|
|
|
# Start openbox window manager (required for FreeCAD GUI)
|
|
if ! pgrep -x openbox > /dev/null; then
|
|
echo "Starting openbox window manager..."
|
|
openbox &
|
|
sleep 1
|
|
echo "openbox started"
|
|
else
|
|
echo "openbox already running"
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Environment ==="
|
|
echo "DISPLAY=$DISPLAY"
|
|
echo "QT_QPA_PLATFORM=$QT_QPA_PLATFORM"
|
|
echo "LIBGL_ALWAYS_SOFTWARE=$LIBGL_ALWAYS_SOFTWARE"
|
|
echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
|
|
|
|
echo ""
|
|
echo "=== Test 1: Verify X11 display ==="
|
|
xdpyinfo -display :99 | head -5 || echo "Cannot connect to display :99"
|
|
|
|
echo ""
|
|
echo "=== Test 2: freecadcmd --version (headless) ==="
|
|
timeout 30 freecadcmd --version 2>&1 || echo "freecadcmd version check failed"
|
|
|
|
echo ""
|
|
echo "=== Test 3: FreeCAD GUI mode with openbox ==="
|
|
# Helper function to run FreeCAD GUI with xdotool events
|
|
run_freecad_gui() {
|
|
local SCRIPT="$1"
|
|
local TIMEOUT="${2:-30}"
|
|
|
|
# Start FreeCAD
|
|
if [ -n "$SCRIPT" ]; then
|
|
freecad "$SCRIPT" > /tmp/fc_stdout.log 2>&1 &
|
|
else
|
|
freecad --version > /tmp/fc_stdout.log 2>&1 &
|
|
fi
|
|
local FC_PID
|
|
FC_PID=$!
|
|
|
|
# Send xdotool events to help initialization
|
|
local START
|
|
START=$(date +%s)
|
|
while ps -p "$FC_PID" > /dev/null 2>&1; do
|
|
local NOW
|
|
NOW=$(date +%s)
|
|
local ELAPSED=$((NOW - START))
|
|
if [ "$ELAPSED" -gt "$TIMEOUT" ]; then
|
|
echo "Timeout after ${TIMEOUT}s"
|
|
kill "$FC_PID" 2>/dev/null || true
|
|
return 124
|
|
fi
|
|
|
|
# Send synthetic events
|
|
xdotool mousemove $((400 + ELAPSED*5)) $((300 + ELAPSED*5)) click 1 key Escape 2>/dev/null || true
|
|
sleep 0.5
|
|
done
|
|
|
|
wait "$FC_PID" 2>/dev/null
|
|
return $?
|
|
}
|
|
|
|
# Test basic GUI initialization
|
|
cat > /tmp/gui_test.py << 'PYEOF'
|
|
import FreeCAD
|
|
import sys
|
|
|
|
with open("/tmp/gui_result.txt", "w") as f:
|
|
f.write(f"GuiUp: {FreeCAD.GuiUp}\n")
|
|
f.write(f"Version: {FreeCAD.Version()}\n")
|
|
|
|
if FreeCAD.GuiUp:
|
|
import FreeCADGui
|
|
f.write("GUI is available!\n")
|
|
f.write(f"Workbenches: {len(FreeCADGui.listWorkbenches())}\n")
|
|
else:
|
|
f.write("GUI is NOT available\n")
|
|
|
|
sys.exit(0)
|
|
PYEOF
|
|
|
|
echo "Running GUI initialization test..."
|
|
run_freecad_gui /tmp/gui_test.py 30 || true # FreeCAD may crash on exit, check result file
|
|
echo ""
|
|
echo "=== GUI Test Result ==="
|
|
if [ -f /tmp/gui_result.txt ]; then
|
|
cat /tmp/gui_result.txt
|
|
if grep -q "GUI is available" /tmp/gui_result.txt; then
|
|
echo "GUI initialization test PASSED"
|
|
else
|
|
echo "GUI initialization test FAILED - GUI not available"
|
|
fi
|
|
else
|
|
echo "No result file (test failed to run)"
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Test 4: Full GUI functionality (screenshot, objects, etc.) ==="
|
|
|
|
cat > /tmp/full_gui_test.py << 'PYEOF'
|
|
import FreeCAD
|
|
import sys
|
|
|
|
results = []
|
|
|
|
try:
|
|
results.append(f"GuiUp: {FreeCAD.GuiUp}")
|
|
|
|
if not FreeCAD.GuiUp:
|
|
results.append("ERROR: GUI not available")
|
|
with open("/tmp/full_gui_result.txt", "w") as f:
|
|
f.write("\n".join(results))
|
|
sys.exit(1)
|
|
|
|
import FreeCADGui
|
|
import Part
|
|
|
|
# Create a document with objects
|
|
doc = FreeCAD.newDocument("GUITest")
|
|
results.append(f"Document: {doc.Name}")
|
|
|
|
# Create objects
|
|
box = doc.addObject("Part::Box", "TestBox")
|
|
box.Length = 50
|
|
box.Width = 30
|
|
box.Height = 20
|
|
doc.recompute()
|
|
results.append(f"Created: {box.Name} ({box.Length}x{box.Width}x{box.Height})")
|
|
|
|
# Test view operations
|
|
view = FreeCADGui.ActiveDocument.ActiveView
|
|
results.append(f"ActiveView: {type(view).__name__}")
|
|
|
|
FreeCADGui.SendMsgToActiveView("ViewFit")
|
|
view.viewIsometric()
|
|
results.append("ViewFit + Isometric: OK")
|
|
|
|
# Screenshot
|
|
view.saveImage("/tmp/screenshot.png", 800, 600, "Current")
|
|
results.append("Screenshot: /tmp/screenshot.png")
|
|
|
|
# ViewObject manipulation
|
|
box_view = FreeCADGui.ActiveDocument.getObject("TestBox")
|
|
if box_view:
|
|
box_view.ShapeColor = (1.0, 0.0, 0.0)
|
|
results.append("ShapeColor set to red: OK")
|
|
|
|
# Save document
|
|
doc.saveAs("/tmp/gui_test.FCStd")
|
|
results.append("Document saved: /tmp/gui_test.FCStd")
|
|
|
|
results.append("ALL TESTS PASSED")
|
|
|
|
except Exception as e:
|
|
results.append(f"ERROR: {e}")
|
|
import traceback
|
|
results.append(traceback.format_exc())
|
|
|
|
with open("/tmp/full_gui_result.txt", "w") as f:
|
|
f.write("\n".join(results))
|
|
|
|
sys.exit(0)
|
|
PYEOF
|
|
|
|
echo "Running full GUI test..."
|
|
run_freecad_gui /tmp/full_gui_test.py 45 || true # FreeCAD may crash on exit, check result file
|
|
echo ""
|
|
echo "=== Full GUI Test Result ==="
|
|
if [ -f /tmp/full_gui_result.txt ]; then
|
|
cat /tmp/full_gui_result.txt
|
|
# Check if test passed
|
|
if grep -q "ALL TESTS PASSED" /tmp/full_gui_result.txt; then
|
|
echo "Full GUI test PASSED"
|
|
else
|
|
echo "Full GUI test FAILED"
|
|
fi
|
|
else
|
|
echo "No result file (test failed to run)"
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Files Created ==="
|
|
ls -la /tmp/screenshot.png /tmp/gui_test.FCStd 2>/dev/null || echo "Files not created"
|
|
|
|
echo ""
|
|
echo "=== Test 5: Start FreeCAD GUI with MCP bridge ==="
|
|
if [ -f "/workspace/addon/FreecadRobustMCP/freecad_mcp_bridge/blocking_bridge.py" ]; then
|
|
echo "Starting FreeCAD with MCP bridge..."
|
|
|
|
freecad /workspace/addon/FreecadRobustMCP/freecad_mcp_bridge/blocking_bridge.py > /tmp/freecad_bridge.log 2>&1 &
|
|
FREECAD_PID=$!
|
|
echo "FreeCAD PID: $FREECAD_PID"
|
|
|
|
# Start xdotool events in background for this test
|
|
(
|
|
for j in {1..120}; do
|
|
xdotool mousemove $((400 + j*3)) $((300 + j*3)) click 1 key Escape 2>/dev/null
|
|
sleep 0.5
|
|
done
|
|
) &
|
|
XDOT_PID=$!
|
|
|
|
# Wait for bridge to start
|
|
BRIDGE_READY=0
|
|
for i in {1..60}; do
|
|
if ! ps -p "$FREECAD_PID" > /dev/null 2>&1; then
|
|
echo "FreeCAD process died after ${i}s"
|
|
break
|
|
fi
|
|
|
|
# Check for bridge
|
|
if curl -s --max-time 1 -X POST \
|
|
-H "Content-Type: text/xml" \
|
|
-d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>' \
|
|
http://localhost:9875 > /dev/null 2>&1; then
|
|
echo "MCP bridge is ready after ${i}s!"
|
|
BRIDGE_READY=1
|
|
break
|
|
fi
|
|
|
|
if [ $((i % 10)) -eq 0 ]; then
|
|
echo "Waiting... (${i}s)"
|
|
fi
|
|
|
|
sleep 1
|
|
done
|
|
|
|
if [ "$BRIDGE_READY" -eq 1 ]; then
|
|
echo "=== Bridge Test: Execute Python ==="
|
|
curl -s -X POST \
|
|
-H "Content-Type: text/xml" \
|
|
-d '<?xml version="1.0"?><methodCall><methodName>execute</methodName><params><param><value><string>_result_ = {"version": str(FreeCAD.Version()), "gui_up": FreeCAD.GuiUp}</string></value></param></params></methodCall>' \
|
|
http://localhost:9875 | head -50
|
|
echo ""
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Bridge Log ==="
|
|
cat /tmp/freecad_bridge.log 2>/dev/null | tail -30
|
|
|
|
# Cleanup
|
|
kill "$XDOT_PID" 2>/dev/null || true
|
|
kill "$FREECAD_PID" 2>/dev/null || true
|
|
else
|
|
echo "Bridge script not found - mount workspace with: docker run -v \$(pwd):/workspace ..."
|
|
fi
|
|
|
|
echo ""
|
|
echo "========================================"
|
|
echo "Tests complete"
|
|
echo "========================================"
|