Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions tests/ui-smoke/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 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
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.
32 changes: 32 additions & 0 deletions tests/ui-smoke/_lib/checkresult.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/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, and the driver-signal approach catches the
# startup-time failures we actually care about.
set -u

if [ $# -lt 1 ]; then
echo "FAIL: checkresult requires the result-log path as argument" >&2
exit 1
fi

LOG="$1"

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

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

exit 0
30 changes: 30 additions & 0 deletions tests/ui-smoke/_lib/cleanup-runtime.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/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.
#
# SHM_KEYS mirrors SHMEM_KEY in scripts/runtests:157. If a ui-smoke
# crash leaks any of these, the next runtests invocation aborts in
# test_shmem(); we must clean the full set.

set -u

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

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=$(LC_ALL=C ipcs -m | awk -v k="$key" 'tolower($1)==k {print $2}')
if [ -n "$shmid" ]; then
ipcrm -m "$shmid" 2>/dev/null || true
fi
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): xvfb-run absence
# is handled by the per-test skip files (skip-if-missing.sh), which
# runtests gates on before invoking test.sh. 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. 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)"

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

# Export the per-invocation values so the inner bash -c receives them
# as proper env vars (avoids embedding paths into the inner script
# via quoting, which breaks on apostrophes / spaces).
export CONFIG_INI LIB_DIR DRIVER_TIMEOUT

# Single quotes around the inner script are intentional: CONFIG_INI,
# LIB_DIR and DRIVER_TIMEOUT are expanded by the inner bash (which sees
# them via the exported env), not by the outer shell.
# shellcheck disable=SC2016
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 makes the child a session leader, so its PID equals
# the PGID and we can group-signal via "kill -- -$PID".
setsid linuxcnc -r "$CONFIG_INI" >linuxcnc.out 2>linuxcnc.err &
LINUXCNC_PID=$!
echo "$LINUXCNC_PID" >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 end
# its own SIGTERM trap run Cleanup which unloads halrun and
# reaps shared memory). axis-remote works only for axis but is
# harmless otherwise. Then group-SIGTERM 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_PID" 2>/dev/null || true
for _ in $(seq 60); do
kill -0 "$LINUXCNC_PID" 2>/dev/null || break
sleep 1
done
if kill -0 "$LINUXCNC_PID" 2>/dev/null; then
echo "WARN: linuxcnc did not exit on SIGTERM, escalating to KILL" >&2
kill -KILL -- -"$LINUXCNC_PID" 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"
15 changes: 15 additions & 0 deletions tests/ui-smoke/_lib/run-gui.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash
# Dispatcher invoked from each per-GUI test.sh. Resolves an INI path
# under configs/sim/ and execs launch.sh in the caller's test dir.
# Usage: run-gui.sh <relpath-under-configs/sim>
# e.g. run-gui.sh axis/axis.ini
# run-gui.sh qtdragon/qtdragon_xyz/qtdragon_metric.ini

set -u

LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_DIR="${TEST_DIR:-$(cd "$(dirname "$0")" && pwd)}"
CONFIGS_DIR="$(cd "$LIB_DIR/../../../configs/sim" && pwd)"

export TEST_DIR
exec "$LIB_DIR/launch.sh" "$CONFIGS_DIR/$1"
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"
2 changes: 2 additions & 0 deletions tests/ui-smoke/axis/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/run-gui.sh" axis/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"
2 changes: 2 additions & 0 deletions tests/ui-smoke/gmoccapy/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/run-gui.sh" gmoccapy/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"
2 changes: 2 additions & 0 deletions tests/ui-smoke/qtdragon/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/run-gui.sh" qtdragon/qtdragon_xyz/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"
2 changes: 2 additions & 0 deletions tests/ui-smoke/touchy/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../_lib/run-gui.sh" touchy/touchy.ini
Loading