mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
the readiness loops fell through silently after 30 tries, so a server that never started surfaced as an opaque driver failure minutes later. each now says what did not answer and on which port.
207 lines
8.1 KiB
Bash
Executable File
207 lines
8.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Runs examples/folio/sanderling/spec.ts against one platform and checks the
|
|
# exit code the job expects. Kept out of the workflow YAML so it can be run by
|
|
# hand, which is how it was calibrated:
|
|
#
|
|
# SEED=9 MAX_STEPS=200 .github/scripts/folio-run.sh android
|
|
#
|
|
# web and ios expect a conviction: folio's double-submit bug is still there, and
|
|
# a run that no longer finds it is a regression in the fuzzer, not a pass. The
|
|
# exit code alone does not say that much, so this script reads the trace: see
|
|
# GATED_PROPERTIES below. android is a health gate instead, because it convicts
|
|
# in four runs out of five rather than five; see docs/development/ci.md.
|
|
set -uo pipefail
|
|
|
|
platform="${1:?usage: folio-run.sh android|ios|web}"
|
|
seed="${SEED:-1}"
|
|
max_steps="${MAX_STEPS:-240}"
|
|
duration="${DURATION:-20m}"
|
|
sanderling="${SANDERLING:-./bin/sanderling}"
|
|
output="runs/folio-$platform"
|
|
spec="examples/folio/sanderling/spec.ts"
|
|
summary="${GITHUB_STEP_SUMMARY:-/dev/null}"
|
|
|
|
folio_args=(--bundle-id app.folio)
|
|
case "$platform" in
|
|
android)
|
|
folio_args+=(--android-app-path
|
|
examples/folio/app/androidApp/build/outputs/apk/debug/androidApp-debug.apk)
|
|
;;
|
|
ios)
|
|
# --clear-data=false because the caller has just installed a fresh build (a
|
|
# freshly installed app IS clear state). The in-run reinstall path is worth
|
|
# avoiding here: `simctl uninstall` + `install` immediately followed by the
|
|
# XCTest runner's own launch hits "app.folio is unknown to FrontBoard"
|
|
# perhaps half the time. The launch RPC is bounded now, so that surfaces
|
|
# as an error rather than an indefinite hang, but a failed leg is still a
|
|
# failed leg and a fresh install is already clear state.
|
|
folio_args+=(--platform ios
|
|
--clear-data=false
|
|
--ios-device "${IOS_DEVICE:-iPhone 16 Pro}")
|
|
;;
|
|
web)
|
|
dist="examples/folio/app/webApp/build/dist/wasmJs/developmentExecutable"
|
|
port="${PORT:-8791}"
|
|
# The stock static servers do not set COOP/COEP, and without cross-origin
|
|
# isolation the app's sqlite worker never starts, so folio loads to a blank
|
|
# canvas and every step observes an empty accessibility tree.
|
|
python3 - "$dist" "$port" <<'PY' &
|
|
import functools, http.server, sys
|
|
|
|
class Isolated(http.server.SimpleHTTPRequestHandler):
|
|
def end_headers(self):
|
|
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
|
|
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
|
|
self.send_header("Cross-Origin-Resource-Policy", "cross-origin")
|
|
super().end_headers()
|
|
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
directory, port = sys.argv[1], int(sys.argv[2])
|
|
handler = functools.partial(Isolated, directory=directory)
|
|
http.server.HTTPServer(("127.0.0.1", port), handler).serve_forever()
|
|
PY
|
|
server_pid=$!
|
|
trap 'kill "$server_pid" 2>/dev/null' EXIT
|
|
ready=""
|
|
for _ in $(seq 1 30); do
|
|
curl -sf "http://127.0.0.1:$port/index.html" >/dev/null && { ready=1; break; }
|
|
sleep 1
|
|
done
|
|
if [ -z "$ready" ]; then
|
|
echo "folio/web: the app server never served index.html on 127.0.0.1:$port from $dist" >&2
|
|
exit 1
|
|
fi
|
|
folio_args=(--platform web --bundle-id "http://127.0.0.1:$port/index.html")
|
|
;;
|
|
*)
|
|
echo "unknown platform: $platform" >&2
|
|
exit 64
|
|
;;
|
|
esac
|
|
|
|
"$sanderling" test \
|
|
--spec "$spec" \
|
|
"${folio_args[@]}" \
|
|
--duration "$duration" \
|
|
--max-steps "$max_steps" \
|
|
--seed "$seed" \
|
|
--exit-on-violation \
|
|
--output "$output"
|
|
code=$?
|
|
|
|
run_dir="$(ls -d "$output"/*/ 2>/dev/null | tail -1)"
|
|
trace="${run_dir:-.}/trace.jsonl"
|
|
steps=0
|
|
[ -f "$trace" ] && steps=$(wc -l < "$trace" | tr -d ' ')
|
|
|
|
# The two properties that state folio's double-submit. Anything else the spec
|
|
# proves false is a different finding, and this leg has nothing to say about it.
|
|
GATED_PROPERTIES="submitMovesBalanceByTypedAmount,submitCommitsOneTransactionPerAction"
|
|
|
|
# Exit 2 means "the run recorded a violation", and that is NOT the same as "the
|
|
# run convicted folio". A predicate that THROWS is recorded as a violation too,
|
|
# with is_error set and the thrown text as its reason, and it reaches exit 2 by
|
|
# the identical path. So does a violation of newAccountBalanceIsZero, a real but
|
|
# unrelated property in the same spec, which one android seed produces. Reading
|
|
# the exit code alone leaves the gate green with detection dead.
|
|
#
|
|
# So sort the trace's violations three ways, by name and by is_error:
|
|
# line 1 convictions: a gated property that was proved false
|
|
# line 2 thrown: a predicate that blew up, with the reason it gave
|
|
# line 3 other: a real violation of some property this leg does not gate on
|
|
classified=$(TRACE="$trace" GATED="$GATED_PROPERTIES" python3 - <<'PY'
|
|
import json, os
|
|
|
|
gated = set(os.environ["GATED"].split(","))
|
|
convictions, thrown, other = [], [], []
|
|
try:
|
|
lines = open(os.environ["TRACE"], encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
lines = []
|
|
for line in lines:
|
|
try:
|
|
step = json.loads(line)
|
|
except ValueError:
|
|
continue
|
|
witnesses = step.get("witnesses") or {}
|
|
for name in step.get("violations") or []:
|
|
witness = witnesses.get(name) or {}
|
|
if witness.get("is_error"):
|
|
reason = " ".join(str(witness.get("reason") or "").split())
|
|
thrown.append("%s: %s" % (name, reason) if reason else name)
|
|
elif name in gated:
|
|
convictions.append(name)
|
|
else:
|
|
other.append(name)
|
|
for names in (convictions, thrown, other):
|
|
print(", ".join(names))
|
|
PY
|
|
)
|
|
convicted=$(printf '%s\n' "$classified" | sed -n '1p')
|
|
thrown=$(printf '%s\n' "$classified" | sed -n '2p')
|
|
other=$(printf '%s\n' "$classified" | sed -n '3p')
|
|
|
|
{
|
|
echo "### folio on $platform"
|
|
echo
|
|
echo "- seed \`$seed\`, budget $max_steps steps / $duration"
|
|
echo "- $steps steps recorded, exit $code"
|
|
[ -n "$convicted" ] && echo "- convicted on: $convicted"
|
|
[ -n "$other" ] && echo "- also violated: $other"
|
|
[ -n "$thrown" ] && echo "- **a predicate threw**: $thrown"
|
|
} >> "$summary"
|
|
|
|
# A thrown predicate fails every leg, android included. It is not evidence about
|
|
# folio: the property that threw is violated from that step on whatever the app
|
|
# does, and --exit-on-violation ends the run there, so nothing past it was
|
|
# checked. Reporting that as a conviction, or as android's bonus, is the hole
|
|
# this check exists to close.
|
|
if [ -n "$thrown" ]; then
|
|
echo "folio/$platform: a predicate threw, so exit $code is not a verdict about folio: $thrown" >&2
|
|
echo "folio/$platform: fix the spec (examples/folio/sanderling/) and run again" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$platform" = "android" ]; then
|
|
# A health gate, not a conviction gate: android convicts in four runs out of
|
|
# five, and a gate that fails the fifth would report a regression it had not
|
|
# found. Reaching the transaction screen is what this leg proves: the app
|
|
# built, installed, launched and drove.
|
|
case "$code" in
|
|
0) ;;
|
|
2)
|
|
if [ -n "$convicted" ]; then
|
|
echo "folio/android: found the submit bug in $steps steps (a bonus, not required)"
|
|
else
|
|
echo "folio/android: violated $other, which is not the double-submit; judging health only"
|
|
fi
|
|
;;
|
|
*) echo "folio/android: the harness failed with exit $code" >&2; exit "$code" ;;
|
|
esac
|
|
if ! grep -q '"AddTransactionScreen"' "$trace" 2>/dev/null; then
|
|
echo "folio/android: the run never reached AddTransactionScreen, so it never got past login" >&2
|
|
exit 1
|
|
fi
|
|
echo "folio/android: healthy run over $steps steps, reached the transaction screen"
|
|
exit 0
|
|
fi
|
|
|
|
case "$code" in
|
|
2)
|
|
if [ -z "$convicted" ]; then
|
|
echo "folio/$platform: exit 2, but the violation was ${other:-nothing this trace records}, not the double-submit this leg gates on" >&2
|
|
exit 1
|
|
fi
|
|
echo "folio/$platform: found the submit bug in $steps steps ($convicted)"
|
|
exit 0
|
|
;;
|
|
0)
|
|
echo "folio/$platform: the run finished clean; the double-submit bug was NOT found in $steps steps (seed $seed)" >&2
|
|
echo "folio/$platform: the spec ran without throwing, so this is the fuzzer no longer reaching the bug, not a broken spec" >&2
|
|
exit 1
|
|
;;
|
|
*) echo "folio/$platform: the harness failed with exit $code" >&2; exit "$code" ;;
|
|
esac
|