gui/docs/pkg-qa-requests-m1.md R3, filed by the previous session: three GUI M1 DoD items (10k rows at 60 fps, flat RSS over 10 minutes, --slow/--flaky/--drop-connection recovery) had nowhere to run in CI. This is the harness — `gui/tests/dod/run.sh <gate> [--json <path>]`, exactly the path/invocation contract tests/integration/README.md already specified — plus `gui/tests/dod/dod_harness.cpp`, the Qt/RpcClient-driven binary that actually runs each gate against a real mockd run.sh starts and tears down itself. - scroll-60fps: an eased scripted scroll over the whole loaded table, timing each step's synchronous repaint; p99 against a 16.6 ms budget (auto-scaled 4x under a sanitized build — ASan/UBSan overhead, not a loosened bar, see the harness's isSanitizedBuild()). - rss-flat: samples this process's own VmRSS at 1 Hz across the run, discards a warm-up window, checks post-warm-up growth against a stated 20 MiB slack. - unhappy-path: three phases (slow/flaky/drop-connection), each its own mockd instance; passes when the client reaches and holds Connected with no crash or hang. A watchdog (the harness's own QTimer, backstopped by run.sh's external `timeout`) turns a genuine hang into a bounded non-zero exit rather than needing the CI caller to timeout(1) around it. Every gate honours the exit-code and --json contract PKG/QA's pre-drafted CI job expects unchanged (one addition needed: the build step must also build the `gui-dod-harness` target, noted in the R3 update). No leaked mockd processes on any exit path (`trap cleanup EXIT INT TERM`); no writes outside a tempdir except the caller's own --json path. Verified live end-to-end (not just unit-level): all three gates run against a real mockd under the exact `ASAN_OPTIONS=detect_leaks=1:halt_on_error=1` `.github/workflows/ci.yml`'s sanitizers job already sets, all pass, and scroll-60fps was forced red once on purpose (VELOX_DOD_FRAME_BUDGET_MS=1) to prove the fail path and exit code actually work. Building this is also what surfaced the two RpcClient bugs fixed in the previous commit, and one real gap in mockd itself — --drop-connection never worked over the Unix socket transport (only WebSocket) — filed as gui/docs/proto-requests-m1.md since tools/mockd is PROTO's file. gui/docs/pkg-qa-requests-m1.md R3 and R4 (an unrelated, non-blocking Qt6::DBus CMake hygiene note filed while wiring the clipboard global-shortcut path) are updated with the concrete findings above. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
198 lines
6.5 KiB
Bash
Executable File
198 lines
6.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# GUI M1 DoD gate driver. Lane GUI.
|
|
#
|
|
# gui/docs/pkg-qa-requests-m1.md R3 / tests/integration/README.md's invocation contract:
|
|
#
|
|
# gui/tests/dod/run.sh <gate> [--json <path>]
|
|
#
|
|
# <gate> is one of: scroll-60fps | rss-flat | unhappy-path
|
|
#
|
|
# Headless-capable: works under offscreen QT_QPA_PLATFORM (the default here) or under
|
|
# Xvfb (xvfb-run -a gui/tests/dod/run.sh ...) if DISPLAY is already set. Exit 0 pass,
|
|
# non-zero fail. Spawns and tears down its own mockd; no network, no writes outside a
|
|
# tempdir except the caller's --json path; never leaves a child process running, on
|
|
# either exit path (see cleanup() / the EXIT trap).
|
|
#
|
|
# Env overrides, for local iteration — CI's real run uses none of these:
|
|
# VELOX_BUILD_DIR build directory holding bin/gui-dod-harness (default: the
|
|
# first of build/ci, build/dev that has the binary)
|
|
# VELOX_DOD_RSS_DURATION_SEC shorten the 10-minute rss-flat soak
|
|
# VELOX_DOD_FRAME_BUDGET_MS override the scroll-60fps per-step budget (default 16.6,
|
|
# x4 under a sanitized build — see dod_harness.cpp)
|
|
# VELOX_DOD_RSS_SLACK_KIB override the rss-flat growth slack (default 20*1024)
|
|
|
|
set -u -o pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
MOCKD_DIR="$REPO_ROOT/tools/mockd"
|
|
|
|
usage() {
|
|
echo "usage: $0 <scroll-60fps|rss-flat|unhappy-path> [--json <path>]" >&2
|
|
exit 2
|
|
}
|
|
|
|
GATE="${1:-}"
|
|
[ -n "$GATE" ] || usage
|
|
shift || true
|
|
|
|
JSON_PATH=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--json) JSON_PATH="$2"; shift 2 ;;
|
|
*) echo "unknown argument: $1" >&2; usage ;;
|
|
esac
|
|
done
|
|
case "$GATE" in
|
|
scroll-60fps|rss-flat|unhappy-path) ;;
|
|
*) usage ;;
|
|
esac
|
|
|
|
# --- locate the harness binary -----------------------------------------------------------
|
|
HARNESS=""
|
|
for d in "${VELOX_BUILD_DIR:-}" "$REPO_ROOT/build/ci" "$REPO_ROOT/build/dev"; do
|
|
[ -n "$d" ] || continue
|
|
if [ -x "$d/bin/gui-dod-harness" ]; then
|
|
HARNESS="$d/bin/gui-dod-harness"
|
|
break
|
|
fi
|
|
done
|
|
if [ -z "$HARNESS" ]; then
|
|
echo "FAIL: gui-dod-harness not found. Build it first:" >&2
|
|
echo " cmake --preset dev && cmake --build --preset dev --target gui-dod-harness" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# --- locate mockd's runner ----------------------------------------------------------------
|
|
MOCKD_RUNNER=""
|
|
if [ -x "$MOCKD_DIR/node_modules/.bin/tsx" ]; then
|
|
MOCKD_RUNNER=("$MOCKD_DIR/node_modules/.bin/tsx" "$MOCKD_DIR/src/index.ts")
|
|
elif command -v npx >/dev/null 2>&1; then
|
|
MOCKD_RUNNER=(npx --prefix "$MOCKD_DIR" tsx "$MOCKD_DIR/src/index.ts")
|
|
else
|
|
echo "FAIL: no tsx runner for mockd found. Run: (cd tools/mockd && npm ci)" >&2
|
|
exit 2
|
|
fi
|
|
|
|
: "${QT_QPA_PLATFORM:=offscreen}"
|
|
export QT_QPA_PLATFORM
|
|
|
|
# --- isolated runtime dir, and the socket mockd/the harness will use -----------------------
|
|
WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}/velox-gui-dod.XXXXXX")"
|
|
export XDG_RUNTIME_DIR="$WORKDIR/xdg"
|
|
mkdir -p "$XDG_RUNTIME_DIR/velox"
|
|
SOCK="$XDG_RUNTIME_DIR/velox/velox.sock"
|
|
|
|
MOCKD_PID=""
|
|
cleanup() {
|
|
if [ -n "$MOCKD_PID" ] && kill -0 "$MOCKD_PID" 2>/dev/null; then
|
|
kill "$MOCKD_PID" 2>/dev/null
|
|
wait "$MOCKD_PID" 2>/dev/null
|
|
fi
|
|
rm -rf "$WORKDIR"
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
start_mockd() {
|
|
# "$@" are extra mockd flags for this phase.
|
|
rm -f "$SOCK"
|
|
( cd "$MOCKD_DIR" && exec "${MOCKD_RUNNER[@]}" --no-ws "$@" ) \
|
|
>"$WORKDIR/mockd.log" 2>&1 &
|
|
MOCKD_PID=$!
|
|
local waited=0
|
|
while [ ! -S "$SOCK" ]; do
|
|
if ! kill -0 "$MOCKD_PID" 2>/dev/null; then
|
|
echo "FAIL: mockd exited before listening. Log:" >&2
|
|
cat "$WORKDIR/mockd.log" >&2
|
|
return 1
|
|
fi
|
|
sleep 0.2
|
|
waited=$((waited + 1))
|
|
if [ "$waited" -gt 100 ]; then
|
|
echo "FAIL: mockd never created its socket within 20s" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
return 0
|
|
}
|
|
|
|
stop_mockd() {
|
|
if [ -n "$MOCKD_PID" ] && kill -0 "$MOCKD_PID" 2>/dev/null; then
|
|
kill "$MOCKD_PID" 2>/dev/null
|
|
wait "$MOCKD_PID" 2>/dev/null
|
|
fi
|
|
MOCKD_PID=""
|
|
}
|
|
|
|
# run_harness <extra harness args...> — belt-and-braces external timeout on top of the
|
|
# harness's own internal watchdog: if the Qt event loop itself ever wedges, its QTimer
|
|
# watchdog can't fire either, and this is what still turns that into a bounded failure
|
|
# instead of a wait forever. Either way this script's own caller never needs timeout(1).
|
|
run_harness() {
|
|
timeout --signal=KILL "$1" "$HARNESS" --sock "$SOCK" "${@:2}"
|
|
}
|
|
|
|
JSON_ARGS=()
|
|
[ -n "$JSON_PATH" ] && JSON_ARGS=(--json "$JSON_PATH")
|
|
|
|
case "$GATE" in
|
|
scroll-60fps)
|
|
start_mockd --tasks 10000 --seed 1 || exit 1
|
|
run_harness 90 scroll-60fps "${JSON_ARGS[@]}"
|
|
RC=$?
|
|
stop_mockd
|
|
exit "$RC"
|
|
;;
|
|
|
|
rss-flat)
|
|
DURATION="${VELOX_DOD_RSS_DURATION_SEC:-600}"
|
|
start_mockd --tasks 10000 --seed 1 || exit 1
|
|
run_harness "$((DURATION + 120))" rss-flat --duration-sec "$DURATION" "${JSON_ARGS[@]}"
|
|
RC=$?
|
|
stop_mockd
|
|
exit "$RC"
|
|
;;
|
|
|
|
unhappy-path)
|
|
# Three phases, one mockd flag each; every phase must pass. mockd's own README
|
|
# documents these flags (--slow/--flaky/--drop-connection).
|
|
OVERALL_RC=0
|
|
declare -A PHASE_FLAGS=(
|
|
[slow]="--slow 900"
|
|
[flaky]="--flaky 0.3"
|
|
[drop-connection]="--drop-connection 5"
|
|
)
|
|
MERGED="{}"
|
|
for phase in slow flaky drop-connection; do
|
|
# shellcheck disable=SC2206
|
|
flags=(${PHASE_FLAGS[$phase]})
|
|
start_mockd "${flags[@]}" || { OVERALL_RC=1; continue; }
|
|
PHASE_JSON="$WORKDIR/phase-$phase.json"
|
|
run_harness 75 unhappy-path --phase "$phase" --json "$PHASE_JSON"
|
|
RC=$?
|
|
stop_mockd
|
|
[ "$RC" -eq 0 ] || OVERALL_RC=1
|
|
if [ -n "$JSON_PATH" ] && [ -f "$PHASE_JSON" ]; then
|
|
MERGED="$(python3 -c "
|
|
import json, sys
|
|
merged = json.loads(sys.argv[1])
|
|
phase = json.load(open(sys.argv[2]))
|
|
merged.setdefault('gate', 'unhappy-path')
|
|
merged.setdefault('phases', {})
|
|
merged['phases'][sys.argv[3]] = phase
|
|
print(json.dumps(merged))
|
|
" "$MERGED" "$PHASE_JSON" "$phase")"
|
|
fi
|
|
done
|
|
if [ -n "$JSON_PATH" ]; then
|
|
python3 -c "
|
|
import json, sys
|
|
merged = json.loads(sys.argv[1])
|
|
merged['pass'] = sys.argv[2] == '0'
|
|
json.dump(merged, open(sys.argv[3], 'w'), indent=2)
|
|
" "$MERGED" "$OVERALL_RC" "$JSON_PATH"
|
|
fi
|
|
exit "$OVERALL_RC"
|
|
;;
|
|
esac
|