#!/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 [--json ] # # 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 [--json ]" >&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 — 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