Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
66094fd
test: add UI smoke tests for axis, touchy, gmoccapy, qtdragon
grandixximo May 4, 2026
62dfb23
test: mark ui-smoke scripts executable
grandixximo May 4, 2026
0090a72
test: skip ui-smoke when GUI python deps absent; gentler shutdown
grandixximo May 4, 2026
2697cd7
test: fix bash -c syntax error in launch.sh and add cairo preflight
grandixximo May 4, 2026
3b6fe75
test: add runtests skip files for ui-smoke
grandixximo May 4, 2026
be52d39
debian: add GUI runtime python deps to Build-Depends with !nocheck
grandixximo May 4, 2026
9e16829
test: add more GUI deps + check gi typelibs in skip predicate
grandixximo May 4, 2026
004ec5f
test: trim driver to true smoke + harden shutdown cleanup
grandixximo May 4, 2026
754cafd
test: kill leftover linuxcnc processes before each ui-smoke test
grandixximo May 4, 2026
afb18f1
test: address ui-smoke review feedback (BsAtHome, hdiethelm)
grandixximo May 4, 2026
a065f41
test: retry NML connection during linuxcnc startup window
grandixximo May 5, 2026
456e90a
test: widen ui-smoke timeouts to fit CI startup + shutdown
grandixximo May 5, 2026
7eba717
test: recreate stat object on NML errors + force software GL
grandixximo May 5, 2026
3b74eb6
test: pre-create $HOME/linuxcnc/nc_files + more Qt software-GL knobs
grandixximo May 5, 2026
2733262
debian: add pyqt5-dev-tools to !nocheck Build-Depends
grandixximo May 5, 2026
73b9aa2
test: scope ui-smoke crash check to pre-UI_SMOKE_OK output only
grandixximo May 5, 2026
37867f6
test: drop crash-marker grep in checkresult; trust driver signal
grandixximo May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions debian/control.top.in
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ Build-Depends:
tk@TCLTK_VERSION@-dev,
xvfb <!nocheck>,
x11-xserver-utils <!nocheck>,
python3-opengl <!nocheck>,
python3-pyqt5 <!nocheck>,
python3-pyqt5.qsci <!nocheck>,
python3-pyqt5.qtsvg <!nocheck>,
python3-pyqt5.qtopengl <!nocheck>,
python3-pyqt5.qtwebengine <!nocheck>,
pyqt5-dev-tools <!nocheck>,
python3-dbus <!nocheck>,
python3-dbus.mainloop.pyqt5 <!nocheck>,
python3-qtpy <!nocheck>,
python3-cairo <!nocheck>,
python3-gi <!nocheck>,
python3-gi-cairo <!nocheck>,
gir1.2-gtk-3.0 <!nocheck>,
gir1.2-gtksource-4 <!nocheck>,
python3-numpy <!nocheck>,
python3-configobj <!nocheck>,
libfmt-dev,
yapps2
Build-Depends-Indep:
Expand Down
9 changes: 9 additions & 0 deletions tests/ui-smoke/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Runtime artifacts left by launch.sh; per Bertho's clean-tree rule,
# generated build/test outputs must be gitignored or committed.
linuxcnc.out
linuxcnc.err
linuxcnc.pid
ui-smoke.out
ui-smoke.err
result
stderr
26 changes: 26 additions & 0 deletions tests/ui-smoke/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
UI smoke tests
~~~~~~~~~~~~~~

These tests launch each GUI (axis, touchy, gmoccapy, qtdragon) under
xvfb-run against an existing sim config and verify the GUI starts and
the NML task is reachable. Phase 1 ("does it start"); functional
checks (load G-code, jog, MDI) belong in tests/ui-functional/.

Each test directory contains:
test.sh launches the GUI under xvfb-run, runs drive.py
checkresult examines the captured output for crash markers
skip skips this test only when xvfb-run is not on the host

Shared helpers live in _lib/:
drive.py common NML driver, prints UI_SMOKE_OK on success
launch.sh xvfb-run wrapper, signal escalation for shutdown
cleanup-runtime.sh belt-and-suspenders: kill stray daemons, ipcrm
shared memory, drop /tmp/linuxcnc.lock
checkresult.sh shared pass/fail predicate
skip-if-missing.sh shared skip predicate

Skip vs fail policy: the only condition we skip on is xvfb-run absence
(rare local dev env). Python and gi typelib deps the GUIs need are
declared in debian/control under !nocheck so apt-get build-dep
installs them on CI; if they are missing the test should fail loudly
rather than silently skip, so missing deps surface during review.
28 changes: 28 additions & 0 deletions tests/ui-smoke/_lib/checkresult.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash
# Shared result check for UI smoke tests.
#
# Pass if the driver printed UI_SMOKE_OK and did not print UI_SMOKE_FAIL.
# The driver only emits UI_SMOKE_OK after a successful NML round-trip
# (linuxcnc task ready and stat.poll() still alive after settle), so
# this is sufficient evidence that the GUI booted. We do not grep for
# generic crash markers like "Segmentation fault" or "Traceback":
# - linuxcnc's own scripts/linuxcnc Cleanup may emit shutdown-side
# segfaults (Qt/GTK teardown races) that are out of scope for a
# startup smoke test;
# - launch.sh interleaves linuxcnc.{out,err} before ui-smoke.{out,err}
# so an "in-log ordering" check would still catch shutdown noise.
set -u
LOG="${1:-/dev/stdin}"
content=$(cat "$LOG")

if grep -q '^UI_SMOKE_FAIL' <<< "$content"; then
echo "FAIL: driver reported UI_SMOKE_FAIL" >&2
exit 1
fi

if ! grep -q '^UI_SMOKE_OK$' <<< "$content"; then
echo "FAIL: driver did not report UI_SMOKE_OK" >&2
exit 1
fi

exit 0
28 changes: 28 additions & 0 deletions tests/ui-smoke/_lib/cleanup-runtime.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash
# Reset linuxcnc-related runtime state so the next ui-smoke test starts
# from a clean environment. Used as both a pre-launch belt-and-braces
# cleanup and as a post-shutdown last-resort if scripts/linuxcnc's own
# SIGTERM trap could not reap everything in time.
#
# Single source of truth for the daemon list and shared memory keys
# (BsAtHome review, PR #3999) so they are not duplicated across
# launch.sh.

set -u

DAEMONS=(linuxcncsvr milltask halui rtapi_app)
SHM_KEYS=(0x48414c32 0x48484c34 0x00000064)

for proc in "${DAEMONS[@]}"; do
pkill -KILL -x "$proc" 2>/dev/null || true
done

rm -f /tmp/linuxcnc.lock
halrun -U 2>/dev/null || true

for key in "${SHM_KEYS[@]}"; do
shmid=$(ipcs -m | awk -v k="$key" 'tolower($1)==k {print $2}')
[ -n "$shmid" ] && ipcrm -m "$shmid" 2>/dev/null || true
done

exit 0
62 changes: 62 additions & 0 deletions tests/ui-smoke/_lib/drive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
# Minimal UI smoke driver: confirm linuxcnc task came up and the GUI
# did not crash. The smoke layer answers Bertho's "does it start"
# question only; functional behaviour (home, run a file, verify
# position) belongs in tests/ui-functional/ (Phase 2).

import linuxcnc
import sys
import time

CONNECT_TIMEOUT_S = 60.0
SETTLE_S = 3.0


def connect_and_wait_ready(timeout):
"""Wait until linuxcnc.stat().poll() returns without error and
reports a non-negative echo_serial_number. The NML status buffer
can be 'invalid err=3' for the first ~30s while linuxcncsvr is
still initialising; recreate the stat object on every iteration so
a stale invalid buffer does not stick after linuxcncsvr is ready."""
deadline = time.monotonic() + timeout
last_err = None
while time.monotonic() < deadline:
try:
stat = linuxcnc.stat()
stat.poll()
if stat.echo_serial_number >= 0:
return linuxcnc.command(), stat
except linuxcnc.error as e:
last_err = e
time.sleep(0.5)
sys.stderr.write(
f"UI_SMOKE_FAIL: task not ready within {timeout}s "
f"(last NML error: {last_err})\n")
return None, None


def main():
cmd, stat = connect_and_wait_ready(CONNECT_TIMEOUT_S)
if cmd is None:
return 1

# Give the GUI process enough time to finish constructing itself
# (load .ui files, compile resources.py if needed, etc.) and
# settle. If the GUI was going to crash on startup it has crashed
# by now.
time.sleep(SETTLE_S)

# Re-check task is still alive; a GUI crash may have torn linuxcnc
# down via Cleanup.
try:
stat.poll()
except linuxcnc.error as e:
sys.stderr.write(f"UI_SMOKE_FAIL: task disappeared after GUI startup: {e}\n")
return 1

print("UI_SMOKE_OK")
return 0


if __name__ == "__main__":
sys.exit(main())
105 changes: 105 additions & 0 deletions tests/ui-smoke/_lib/launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/bin/bash
# Shared launcher for UI smoke tests.
# Usage: launch.sh <sim-config-ini>
#
# Spawns linuxcnc -r <ini> under xvfb-run, then runs the common driver
# script against it via NML. Captures stdout/stderr to per-test files.
#
# Skip vs fail (BsAtHome / hdiethelm review, PR #3999): the only thing
# we skip on is xvfb-run absence, which can happen on a developer's
# minimal machine. CI is expected to have all required deps; if any
# python module the GUI needs is missing the test should fail loudly
# rather than silently skip, so missing deps surface during review.
# Per-GUI deps are declared in debian/control under !nocheck, so
# apt-get build-dep installs them on CI.

set -u

CONFIG_INI="$1"
TEST_DIR="${TEST_DIR:-$(cd "$(dirname "$0")" && pwd)}"
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

if ! command -v xvfb-run >/dev/null 2>&1; then
echo "SKIP: xvfb-run not installed" >&2
exit 77
fi

cd "$TEST_DIR" || exit 1
rm -f ui-smoke.out ui-smoke.err linuxcnc.pid

# Pre-launch cleanup: a previous ui-smoke test in the same job may
# have left a daemon listening on the NML TCP port or HAL shared
# memory still attached. Run the shared cleanup once before we start.
bash "$LIB_DIR/cleanup-runtime.sh"

# Launch linuxcnc inside xvfb-run. The outer timeout is a safety net
# so a wedged GUI cannot hang CI.
LINUXCNC_TIMEOUT=240
DRIVER_TIMEOUT=90

# Force software OpenGL (Mesa llvmpipe). CI runners have no GPU and
# Qt/GL widgets segfault under hardware GL with no display. The Qt-
# specific knobs cover qtdragon's QtQuick + RHI paths.
export LIBGL_ALWAYS_SOFTWARE=1
export GALLIUM_DRIVER=llvmpipe
export QT_QUICK_BACKEND=software
export QSG_RHI_BACKEND=software
export QT_OPENGL=software

# touchy's filechooser reads $HOME/linuxcnc/nc_files at startup and
# crashes if the dir is missing (no try/except in filechooser.py).
# CI runners have a clean $HOME with no linuxcnc/ tree; create the
# minimum set of dirs the GUIs poke at on boot.
mkdir -p "$HOME/linuxcnc/nc_files"

xvfb-run -a --server-args="-screen 0 1024x768x24" \
timeout "$LINUXCNC_TIMEOUT" \
bash -c "
# Run linuxcnc in its own process group so we can signal the
# whole group cleanly (linuxcnc forks task, motion, GUI, halrun).
setsid linuxcnc -r '$CONFIG_INI' >linuxcnc.out 2>linuxcnc.err &
LINUXCNC_PGID=\$!
echo \$LINUXCNC_PGID >linuxcnc.pid

# The driver polls NML readiness itself (BsAtHome review:
# avoid real-clock waits where status polling will do).
timeout $DRIVER_TIMEOUT python3 '$LIB_DIR/drive.py' >ui-smoke.out 2>ui-smoke.err
DRIVE_RC=\$?

# Clean shutdown: GUI-specific quit first (lets linuxcnc's
# own SIGTERM trap run Cleanup which unloads halrun and reaps
# shared memory). axis-remote works only for axis but is
# harmless otherwise. Then SIGTERM to the script PID so the
# trap runs in-process. Wait up to 60s for Cleanup to finish
# before falling back to SIGKILL + cleanup-runtime.sh.
if command -v axis-remote >/dev/null 2>&1; then
axis-remote --quit 2>/dev/null || true
fi

kill -TERM \$LINUXCNC_PGID 2>/dev/null || true
for i in \$(seq 60); do
kill -0 \$LINUXCNC_PGID 2>/dev/null || break
sleep 1
done
if kill -0 \$LINUXCNC_PGID 2>/dev/null; then
echo 'WARN: linuxcnc did not exit on SIGTERM, escalating to KILL' >&2
kill -KILL -\$LINUXCNC_PGID 2>/dev/null || true
sleep 2
bash '$LIB_DIR/cleanup-runtime.sh'
fi

exit \$DRIVE_RC
"
RC=$?

# Surface logs so checkresult and CI artifact upload can see them.
echo "=== linuxcnc.out ==="
[ -f linuxcnc.out ] && cat linuxcnc.out
echo "=== linuxcnc.err ==="
[ -f linuxcnc.err ] && cat linuxcnc.err
echo "=== ui-smoke.out ==="
[ -f ui-smoke.out ] && cat ui-smoke.out
echo "=== ui-smoke.err ==="
[ -f ui-smoke.err ] && cat ui-smoke.err

exit "$RC"
17 changes: 17 additions & 0 deletions tests/ui-smoke/_lib/skip-if-missing.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
# Shared "skip" predicate for ui-smoke tests.
# runtests semantics: a `skip` script that returns non-zero causes the
# test to be skipped. Per-test skip files invoke this.
#
# We only skip on xvfb-run absence (rare local dev env). Python /
# typelib deps are declared in debian/control under !nocheck so CI
# always has them; missing deps should fail the test loudly rather
# than silently skip (BsAtHome / hdiethelm review, PR #3999).
set -u

if ! command -v xvfb-run >/dev/null 2>&1; then
echo "skip: xvfb-run not installed" >&2
exit 1
fi

exit 0
2 changes: 2 additions & 0 deletions tests/ui-smoke/axis/checkresult
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/checkresult.sh" "$@"
2 changes: 2 additions & 0 deletions tests/ui-smoke/axis/skip
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/skip-if-missing.sh"
4 changes: 4 additions & 0 deletions tests/ui-smoke/axis/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
TEST_DIR="$(cd "$(dirname "$0")" && pwd)" \
exec "$(dirname "$0")/../_lib/launch.sh" \
"$(cd "$(dirname "$0")"/../../../configs/sim/axis && pwd)/axis.ini"
2 changes: 2 additions & 0 deletions tests/ui-smoke/gmoccapy/checkresult
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/checkresult.sh" "$@"
2 changes: 2 additions & 0 deletions tests/ui-smoke/gmoccapy/skip
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/skip-if-missing.sh"
4 changes: 4 additions & 0 deletions tests/ui-smoke/gmoccapy/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
TEST_DIR="$(cd "$(dirname "$0")" && pwd)" \
exec "$(dirname "$0")/../_lib/launch.sh" \
"$(cd "$(dirname "$0")"/../../../configs/sim/gmoccapy && pwd)/gmoccapy.ini"
2 changes: 2 additions & 0 deletions tests/ui-smoke/qtdragon/checkresult
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/checkresult.sh" "$@"
2 changes: 2 additions & 0 deletions tests/ui-smoke/qtdragon/skip
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/skip-if-missing.sh"
4 changes: 4 additions & 0 deletions tests/ui-smoke/qtdragon/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
TEST_DIR="$(cd "$(dirname "$0")" && pwd)" \
exec "$(dirname "$0")/../_lib/launch.sh" \
"$(cd "$(dirname "$0")"/../../../configs/sim/qtdragon/qtdragon_xyz && pwd)/qtdragon_metric.ini"
2 changes: 2 additions & 0 deletions tests/ui-smoke/touchy/checkresult
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/checkresult.sh" "$@"
2 changes: 2 additions & 0 deletions tests/ui-smoke/touchy/skip
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/skip-if-missing.sh"
4 changes: 4 additions & 0 deletions tests/ui-smoke/touchy/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
TEST_DIR="$(cd "$(dirname "$0")" && pwd)" \
exec "$(dirname "$0")/../_lib/launch.sh" \
"$(cd "$(dirname "$0")"/../../../configs/sim/touchy && pwd)/touchy.ini"
Loading