diff --git a/tests/README.md b/tests/README.md index 3b78f14..7183d0f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -24,6 +24,12 @@ the build does. The stage either passes or the build fails; nothing is tagged. - `test-render-config.sh` — the Suramadu config renders with no `@PLACEHOLDER@` left behind. An unsubstituted player name means a client that logs in as `@PLAYER_NAME@` and sits in the lobby forever. +- `test-perf-shim.sh` — `suramadu/web/perf.js`, the latency instrument served to + every player. That it parses (bracket balance; `node --check` too if the + machine has node), that nothing is installed above its `?perf=1` check, that + its ring buffer is bounded, that `window.__arenaPerf` still exposes what a + headless pilot calls, and that `index.html` loads it after + `suramadu-init.js` under an unchanged CSP. - `test-deploy.sh` — what `scripts/deploy.sh` refuses. It writes the release id production is pinned to, so the guards in front of it are the whole point: a dirty tree, and a main that has drifted from `origin/main` either way. Builds diff --git a/tests/shell/test-perf-shim.sh b/tests/shell/test-perf-shim.sh new file mode 100755 index 0000000..f50ac08 --- /dev/null +++ b/tests/shell/test-perf-shim.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# suramadu/web/perf.js is served to every player, and the two ways it can go +# wrong are both silent: +# +# - it costs production something. It must do nothing at all without ?perf=1, +# so the opt-in check has to come before every listener, wrapper and global. +# - the page stops loading. It is a same-origin file under script-src 'self', +# and it hooks the client through window.webswingInstance0, which +# suramadu-init.js assigns - so tag order in index.html is load-bearing. +# +# The test stage has no JavaScript engine, so "it parses" is as far as this can +# go without a browser: `node --check` when a laptop has node, and otherwise a +# scanner that balances brackets outside strings and comments. Everything the +# shim actually measures is unverified until someone opens a match with it. +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +JS="$ROOT/suramadu/web/perf.js" +HTML="$ROOT/suramadu/web/index.html" + +pass=0; fail=0 +check() { if "${@:2}"; then echo "ok $1"; pass=$((pass+1)); else echo "FAIL $1"; fail=$((fail+1)); fi; } + +[ -s "$JS" ] || { echo "FAIL perf.js is missing or empty"; exit 1; } +[ -s "$HTML" ] || { echo "FAIL index.html is missing or empty"; exit 1; } + +# --- it parses --------------------------------------------------------------- + +# The scanner below treats a backtick as a plain string delimiter, so a +# template literal containing ${ } would confuse its brace count. perf.js says +# it uses none; this is what holds it to that. +check "no template literals, so the scanner's assumptions hold" \ + bash -c "! grep -q '\`' '$JS'" + +# Brackets, parentheses and braces balance outside strings and comments. Not a +# parse - it would accept plenty of invalid JavaScript - but it catches the +# edit that lost a closing brace, which is the failure this file is exposed to. +check "brackets balance" python3 - "$JS" <<'PY' +import sys + +src = open(sys.argv[1], encoding="utf-8").read() +pairs = {")": "(", "]": "[", "}": "{"} +stack = [] +i, n, line = 0, len(src), 1 +while i < n: + c = src[i] + if c == "\n": + line += 1 + i += 1 + elif c == "/" and src[i + 1:i + 2] == "/": + while i < n and src[i] != "\n": + i += 1 + elif c == "/" and src[i + 1:i + 2] == "*": + i += 2 + while i + 1 < n and not (src[i] == "*" and src[i + 1] == "/"): + line += src[i] == "\n" + i += 1 + i += 2 + elif c in "\"'`": + quote, i = c, i + 1 + while i < n and src[i] != quote: + if src[i] == "\\": + i += 2 + continue + line += src[i] == "\n" + i += 1 + i += 1 + elif c in "([{": + stack.append((c, line)) + i += 1 + elif c in ")]}": + if not stack or stack[-1][0] != pairs[c]: + sys.exit("unbalanced '%s' at line %d" % (c, line)) + stack.pop() + i += 1 + else: + i += 1 +if stack: + sys.exit("unclosed '%s' opened at line %d" % (stack[-1][0], stack[-1][1])) +PY + +if command -v node >/dev/null; then + check "node --check" node --check "$JS" +else + echo "skip node --check (no node in the test stage)" +fi + +# --- production pays nothing ------------------------------------------------- + +# Everything that costs a page anything must sit below the ?perf=1 check. +guard="$(grep -n 'search.get("perf")' "$JS" | head -1 | cut -d: -f1)" +check "the opt-in check is on ?perf=1" test -n "$guard" + +for pattern in 'addEventListener' 'window.WebSocket =' 'window.__arenaPerf ='; do + first="$(grep -n -F "$pattern" "$JS" | head -1 | cut -d: -f1)" + check "nothing installs '$pattern' before the opt-in check" \ + bash -c '[ -n "$1" ] && [ -n "$2" ] && [ "$1" -gt "$2" ]' _ "$first" "$guard" +done + +# A match runs for an hour. The buffer has to wrap rather than grow. +check "the ring buffer is bounded by a capacity constant" \ + grep -q 'written % CAPACITY' "$JS" + +# The pilot calls these over CDP, one Runtime.evaluate per collection. Renaming +# one breaks a consumer this repo cannot see. +for fn in dump summary reset mark; do + check "__arenaPerf exposes $fn" grep -qE "^ *$fn: $fn,?$" "$JS" +done +check "dump carries the summary, so one call is enough" \ + grep -q 'summary: summary(recs)' "$JS" + +# --- the page loads it ------------------------------------------------------- + +check "index.html loads perf.js" grep -q '' "$HTML" + +# It hooks window.webswingInstance0.options, which suramadu-init.js assigns. +# Loaded first, the hook silently does nothing and paint timing falls back to +# requestAnimationFrame. +check "perf.js is loaded after suramadu-init.js" bash -c ' + i=$(grep -n "src=\"javascript/suramadu-init.js\"" "$1" | head -1 | cut -d: -f1) + p=$(grep -n "src=\"perf.js\"" "$1" | head -1 | cut -d: -f1) + [ -n "$i" ] && [ -n "$p" ] && [ "$p" -gt "$i" ]' _ "$HTML" + +# Same-origin, so upstream's script-src covers it with no CSP change. If that +# directive ever loosens to include a host or 'unsafe-inline', it did not +# happen here. +check "the CSP still allows only same-origin scripts" \ + grep -q "script-src 'self';" "$HTML" + +echo +echo "$pass passed, $fail failed" +[ "$fail" -eq 0 ]