Skip to content

Commit 0ad17aa

Browse files
committed
tests: add ui-smoke quit-path coverage (SIGTERM)
Adds a quit-path smoke test per GUI that boots the GUI, waits for the NML task to come up, sends SIGTERM to the GUI process alone, and asserts the GUI exits on its own within a short grace. This guards the clean-shutdown handlers: a GUI that absorbs SIGTERM and has to be SIGKILLed fails the test. The new _lib/quit-launch.sh shares the headless environment (software GL + audio silencing) with launch.sh by sourcing a new _lib/launch-env.sh rather than copying it, so the two launchers cannot drift apart. Results go through _lib/checkresult-quit.sh (pass on UI_SMOKE_QUIT_OK). The GUI process is identified by matching a python argv[0], so the linuxcnc launcher and xvfb-run wrappers that also carry the GUI name on their command line are not mistaken for it. Per-GUI dirs: touchy-quit, gmoccapy-quit, qtdragon-quit. The qtdragon quit test needs the same CI workarounds the qtdragon smoke test already carries (writable config mirror with a patched LOG_FILE, the offscreen Qt platform, and the QtWebEngine import shim). Those move out of qtdragon/test.sh into _lib/qtdragon-prepare.sh, sourced by both qtdragon test.sh files, so the quit test reuses them instead of leaving qtvcp to crash on startup. Requires the SIGTERM handlers in #4076 (gmoccapy), #4077 (touchy) and #4078 (qtvcp); without them the GUIs ignore SIGTERM and these tests fail by design. (cherry picked from commit aff5991)
1 parent 121fe44 commit 0ad17aa

15 files changed

Lines changed: 282 additions & 86 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/bash
2+
# Shared result check for UI smoke quit-path tests.
3+
#
4+
# Pass if the quit launcher printed UI_SMOKE_QUIT_OK (the GUI exited on
5+
# its own SIGTERM within the grace) and did not print UI_SMOKE_QUIT_FAIL.
6+
set -u
7+
8+
if [ $# -lt 1 ]; then
9+
echo "FAIL: checkresult-quit requires the result-log path as argument" >&2
10+
exit 1
11+
fi
12+
13+
LOG="$1"
14+
15+
if grep -q '^UI_SMOKE_QUIT_FAIL' "$LOG"; then
16+
echo "FAIL: $(grep -m1 '^UI_SMOKE_QUIT_FAIL' "$LOG")" >&2
17+
exit 1
18+
fi
19+
20+
if ! grep -q '^UI_SMOKE_QUIT_OK' "$LOG"; then
21+
echo "FAIL: GUI did not report a clean SIGTERM exit (no UI_SMOKE_QUIT_OK)" >&2
22+
exit 1
23+
fi
24+
25+
exit 0

tests/ui-smoke/_lib/launch-env.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/bash
2+
# Shared headless environment for the UI smoke launchers. Sourced by
3+
# launch.sh and quit-launch.sh so the two stay in lockstep; a knob added
4+
# here reaches both. The caller must set LIB_DIR before sourcing (it
5+
# locates asound.conf). This file only exports; it runs no commands.
6+
7+
# Force software OpenGL (Mesa llvmpipe). CI runners have no GPU and
8+
# Qt/GL widgets segfault under hardware GL with no display. The Qt-
9+
# specific knobs cover qtdragon's QtQuick + RHI paths.
10+
export LIBGL_ALWAYS_SOFTWARE=1
11+
export GALLIUM_DRIVER=llvmpipe
12+
export QT_QUICK_BACKEND=software
13+
export QSG_RHI_BACKEND=software
14+
export QT_OPENGL=software
15+
# Dodge a long-known xcb_glx integration crash that hits QtWebEngine
16+
# and related Qt5 widgets under xvfb (Launchpad #1761708, QTBUG-67537).
17+
# Forces the egl path which is what software-GL stacks expect anyway.
18+
export QT_XCB_GL_INTEGRATION=xcb_egl
19+
20+
# Silence audio: xvfb covers X but not sound. Demote every Gst
21+
# Audio/Sink and disable canberra/SDL/pulse/ALSA-default paths.
22+
export ALSA_CONFIG_PATH="$LIB_DIR/asound.conf"
23+
export CANBERRA_DRIVER=null
24+
export GST_PLUGIN_FEATURE_RANK="pulsesink:NONE,alsasink:NONE,osssink:NONE,oss4sink:NONE,jackaudiosink:NONE,pipewiresink:NONE,openalsink:NONE"
25+
export PULSE_SERVER=/dev/null
26+
export SDL_AUDIODRIVER=dummy

tests/ui-smoke/_lib/launch.sh

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,9 @@ bash "$LIB_DIR/cleanup-runtime.sh"
3535
LINUXCNC_TIMEOUT=300
3636
DRIVER_TIMEOUT=180
3737

38-
# Force software OpenGL (Mesa llvmpipe). CI runners have no GPU and
39-
# Qt/GL widgets segfault under hardware GL with no display. The Qt-
40-
# specific knobs cover qtdragon's QtQuick + RHI paths.
41-
export LIBGL_ALWAYS_SOFTWARE=1
42-
export GALLIUM_DRIVER=llvmpipe
43-
export QT_QUICK_BACKEND=software
44-
export QSG_RHI_BACKEND=software
45-
export QT_OPENGL=software
46-
# Dodge a long-known xcb_glx integration crash that hits QtWebEngine
47-
# and related Qt5 widgets under xvfb (Launchpad #1761708, QTBUG-67537).
48-
# Forces the egl path which is what software-GL stacks expect anyway.
49-
export QT_XCB_GL_INTEGRATION=xcb_egl
50-
51-
# Silence audio: xvfb covers X but not sound. Demote every Gst
52-
# Audio/Sink and disable canberra/SDL/pulse/ALSA-default paths.
53-
export ALSA_CONFIG_PATH="$LIB_DIR/asound.conf"
54-
export CANBERRA_DRIVER=null
55-
export GST_PLUGIN_FEATURE_RANK="pulsesink:NONE,alsasink:NONE,osssink:NONE,oss4sink:NONE,jackaudiosink:NONE,pipewiresink:NONE,openalsink:NONE"
56-
export PULSE_SERVER=/dev/null
57-
export SDL_AUDIODRIVER=dummy
38+
# Shared headless environment (software GL + audio silencing), kept in
39+
# launch-env.sh so launch.sh and quit-launch.sh cannot drift apart.
40+
. "$LIB_DIR/launch-env.sh"
5841

5942
# Export the per-invocation values so the inner bash -c receives them
6043
# as proper env vars (avoids embedding paths into the inner script
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/bin/bash
2+
# Sourced by the qtdragon ui-smoke tests (smoke and quit) to build a
3+
# config qtvcp can actually run under CI. Sets QTDRAGON_INI to the
4+
# patched ini path and exports the headless env; the caller then execs
5+
# run-gui.sh or quit-launch.sh with "$QTDRAGON_INI". Must be sourced
6+
# with LIB_DIR already set.
7+
#
8+
# qtdragon's qtvcp logger writes its log file (path from INI [DISPLAY]
9+
# LOG_FILE) into the config directory. CI mounts the workspace read-
10+
# only for the runtime user, so a relative LOG_FILE like 'qtdragon.log'
11+
# resolves to a path qtvcp cannot create, hal_bridge then exits, and
12+
# linuxcnc tears down before our driver can do anything. Mirror the
13+
# config dir to a writable tmp location and patch LOG_FILE to be
14+
# rooted at $HOME so the log lands in a directory we can write to.
15+
#
16+
# Force the Qt offscreen platform plugin. qtvcp under xvfb + xcb on
17+
# Ubuntu 24.04 segfaults during widget construction (no backtrace);
18+
# Debian containers in the same CI matrix do not. Offscreen renders
19+
# entirely in memory, no X server needed (xvfb-run still wraps the
20+
# call so the rest of scripts/linuxcnc's X-display assumptions hold).
21+
# scripts/linuxcnc itself forces QT_QPA_PLATFORM=xcb unless
22+
# LINUXCNC_OPENGL_PLATFORM is set to something other than glx, so we
23+
# pin both env vars.
24+
#
25+
# qtdragon embeds a QWebEngineView (Chromium). Under offscreen + xvfb
26+
# with no GPU and no user namespaces in the CI runner sandbox,
27+
# QtWebEngine browser-process init segfaults even with --no-sandbox
28+
# --single-process --disable-gpu (Chromium logs "Sandboxing disabled
29+
# by user." then crashes inside the same qtvcp PID). Rather than keep
30+
# tuning Chromium flags for a widget the smoke test never touches,
31+
# we shim qtpy.QtWebEngineWidgets to raise ImportError; web_widget.py
32+
# already has a fallback path that swaps the QWebEngineView for a
33+
# plain QWidget when the import fails (its "fail safe - mostly for
34+
# designer" branch). No Chromium spawn = no crash.
35+
36+
: "${LIB_DIR:?qtdragon-prepare.sh must be sourced with LIB_DIR set}"
37+
38+
SRC_DIR="$(cd "$LIB_DIR/../../../configs/sim/qtdragon/qtdragon_xyz" && pwd)"
39+
40+
WORK_DIR="$(mktemp -d -t ui-smoke-qtdragon.XXXXXX)"
41+
trap 'rm -rf "$WORK_DIR"' EXIT
42+
cp -r "$SRC_DIR/." "$WORK_DIR/"
43+
sed -i 's|^LOG_FILE = qtdragon\.log$|LOG_FILE = ~/qtdragon.log|' \
44+
"$WORK_DIR/qtdragon_metric.ini"
45+
46+
export LINUXCNC_OPENGL_PLATFORM=offscreen
47+
export QT_QPA_PLATFORM=offscreen
48+
49+
# sitecustomize.py is auto-imported by Python from any sys.path entry
50+
# at interpreter startup. Drop a meta_path finder that blocks the
51+
# qtpy.QtWebEngineWidgets import so WebWidget falls back to QWidget.
52+
SHIM_DIR="$WORK_DIR/_pyshim"
53+
mkdir -p "$SHIM_DIR"
54+
cat >"$SHIM_DIR/sitecustomize.py" <<'PY'
55+
import sys
56+
from importlib.abc import MetaPathFinder, Loader
57+
from importlib.util import spec_from_loader
58+
59+
_BLOCK = {'qtpy.QtWebEngineWidgets', 'PyQt5.QtWebEngineWidgets'}
60+
61+
class _BlockLoader(Loader):
62+
def create_module(self, spec):
63+
raise ImportError('QtWebEngineWidgets blocked for ui-smoke CI')
64+
def exec_module(self, module):
65+
pass
66+
67+
class _BlockFinder(MetaPathFinder):
68+
def find_spec(self, name, path, target=None):
69+
if name in _BLOCK:
70+
return spec_from_loader(name, _BlockLoader())
71+
return None
72+
73+
sys.meta_path.insert(0, _BlockFinder())
74+
PY
75+
export PYTHONPATH="$SHIM_DIR${PYTHONPATH:+:$PYTHONPATH}"
76+
77+
# Consumed by the sourcing test.sh, which execs the launcher with it.
78+
# shellcheck disable=SC2034
79+
QTDRAGON_INI="$WORK_DIR/qtdragon_metric.ini"

tests/ui-smoke/_lib/quit-launch.sh

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/bin/bash
2+
# Quit-path launcher for UI smoke tests.
3+
# Usage: quit-launch.sh <sim-config-ini> <gui-process-match>
4+
#
5+
# Boots linuxcnc + GUI under xvfb-run exactly like launch.sh, waits for
6+
# the NML task to come up (via drive.py), then sends SIGTERM to the GUI
7+
# process *alone* and asserts the GUI exits on its own within a short
8+
# grace. This is the regression guard for the SIGTERM clean-shutdown
9+
# handlers: a GUI that absorbs SIGTERM and has to be SIGKILLed fails.
10+
#
11+
# <gui-process-match> is a pgrep -f pattern identifying the GUI process
12+
# (e.g. "bin/touchy", "bin/gmoccapy"). It must not match the linuxcnc
13+
# launcher or task/motion helpers.
14+
#
15+
# Markers (consumed by checkresult-quit.sh):
16+
# UI_SMOKE_QUIT_OK GUI exited on SIGTERM within QUIT_GRACE
17+
# UI_SMOKE_QUIT_FAIL GUI never started, was not found, or ignored TERM
18+
19+
set -u
20+
21+
CONFIG_INI="$1"
22+
GUI_MATCH="$2"
23+
TEST_DIR="${TEST_DIR:-$(cd "$(dirname "$0")" && pwd)}"
24+
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25+
26+
cd "$TEST_DIR" || exit 1
27+
rm -f ui-smoke.out ui-smoke.err linuxcnc.pid
28+
29+
bash "$LIB_DIR/cleanup-runtime.sh"
30+
31+
LINUXCNC_TIMEOUT=240
32+
DRIVER_TIMEOUT=90
33+
# Seconds to wait for the GUI to exit after SIGTERM before declaring it
34+
# stuck. A GUI honouring SIGTERM exits in well under a second; the
35+
# margin covers Cleanup of task/motion on slow CI.
36+
QUIT_GRACE=15
37+
38+
# Shared headless environment (software GL + audio silencing), kept in
39+
# launch-env.sh so launch.sh and quit-launch.sh cannot drift apart.
40+
. "$LIB_DIR/launch-env.sh"
41+
42+
export CONFIG_INI LIB_DIR DRIVER_TIMEOUT GUI_MATCH QUIT_GRACE
43+
44+
# shellcheck disable=SC2016
45+
xvfb-run -a --server-args="-screen 0 1024x768x24" \
46+
timeout "$LINUXCNC_TIMEOUT" \
47+
bash -c '
48+
setsid linuxcnc -r "$CONFIG_INI" >linuxcnc.out 2>linuxcnc.err &
49+
LINUXCNC_PID=$!
50+
echo "$LINUXCNC_PID" >linuxcnc.pid
51+
52+
# Wait until the task is reachable (GUI has constructed and the
53+
# NML round-trip works). Reuse the phase-1 driver for readiness.
54+
timeout "$DRIVER_TIMEOUT" python3 "$LIB_DIR/drive.py" >ui-smoke.out 2>ui-smoke.err
55+
if ! grep -q "^UI_SMOKE_OK$" ui-smoke.out; then
56+
echo "UI_SMOKE_QUIT_FAIL: GUI did not come up; cannot test quit"
57+
kill -KILL -- -"$LINUXCNC_PID" 2>/dev/null || true
58+
bash "$LIB_DIR/cleanup-runtime.sh"
59+
exit 1
60+
fi
61+
62+
# Identify the GUI process. pgrep -f matches against the whole
63+
# command line, so wrapper processes (the linuxcnc launcher, the
64+
# xvfb-run shell, this bash -c) also match because the GUI name
65+
# appears in the config path or the embedded script text. Every
66+
# such wrapper has a shell or xvfb-run as argv[0]; the real GUI
67+
# is a python interpreter. Pick the first match whose argv[0]
68+
# basename is a python binary.
69+
GUI_PID=""
70+
for p in $(pgrep -f "$GUI_MATCH"); do
71+
arg0=$(tr "\0" "\n" <"/proc/$p/cmdline" 2>/dev/null | head -1)
72+
case "$(basename "$arg0" 2>/dev/null)" in
73+
python*) GUI_PID="$p"; break ;;
74+
esac
75+
done
76+
if [ -z "$GUI_PID" ]; then
77+
echo "UI_SMOKE_QUIT_FAIL: GUI process matching \"$GUI_MATCH\" not found"
78+
kill -KILL -- -"$LINUXCNC_PID" 2>/dev/null || true
79+
bash "$LIB_DIR/cleanup-runtime.sh"
80+
exit 1
81+
fi
82+
83+
# Send SIGTERM to the GUI alone and time how long it takes to go.
84+
kill -TERM "$GUI_PID" 2>/dev/null || true
85+
waited=0
86+
while [ "$waited" -lt "$QUIT_GRACE" ]; do
87+
kill -0 "$GUI_PID" 2>/dev/null || break
88+
sleep 1
89+
waited=$((waited + 1))
90+
done
91+
92+
if kill -0 "$GUI_PID" 2>/dev/null; then
93+
echo "UI_SMOKE_QUIT_FAIL: GUI (pid $GUI_PID) still alive ${QUIT_GRACE}s after SIGTERM"
94+
RC=1
95+
else
96+
echo "UI_SMOKE_QUIT_OK: GUI exited ${waited}s after SIGTERM"
97+
RC=0
98+
fi
99+
100+
# Tear down whatever is left (task/motion, or the GUI on failure).
101+
kill -TERM -- -"$LINUXCNC_PID" 2>/dev/null || true
102+
for _ in $(seq 30); do
103+
kill -0 "$LINUXCNC_PID" 2>/dev/null || break
104+
sleep 1
105+
done
106+
if kill -0 "$LINUXCNC_PID" 2>/dev/null; then
107+
kill -KILL -- -"$LINUXCNC_PID" 2>/dev/null || true
108+
sleep 2
109+
bash "$LIB_DIR/cleanup-runtime.sh"
110+
fi
111+
exit "$RC"
112+
'
113+
RC=$?
114+
115+
echo "=== linuxcnc.err ==="
116+
[ -f linuxcnc.err ] && cat linuxcnc.err
117+
echo "=== ui-smoke.out ==="
118+
[ -f ui-smoke.out ] && cat ui-smoke.out
119+
120+
exit "$RC"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash
2+
exec "$(dirname "$0")/../_lib/checkresult-quit.sh" "$@"

tests/ui-smoke/gmoccapy-quit/skip

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash
2+
exec "$(dirname "$0")/../_lib/skip-if-missing.sh"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
exec "$(dirname "$0")/../_lib/quit-launch.sh" \
3+
"$(cd "$(dirname "$0")/../../../configs/sim" && pwd)/gmoccapy/gmoccapy.ini" \
4+
"bin/gmoccapy"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash
2+
exec "$(dirname "$0")/../_lib/checkresult-quit.sh" "$@"

tests/ui-smoke/qtdragon-quit/skip

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash
2+
exec "$(dirname "$0")/../_lib/skip-if-missing.sh"

0 commit comments

Comments
 (0)