mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(conformance): give the gate reproducible seeds
SEED defaulted to 0 and sanderling test reads --seed 0 as "derive a seed from the clock", so the tunable controlled nothing and a gate failure could not be re-run. SEEDS now takes one explicit non-zero seed per run, recorded in the results table so a failing row names its stream. The five runs stay on five different streams: a gate that scored one path five times would catch less than one that scores five. Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
This commit is contained in:
1 parent
d5bf743344
commit
b631601f5d
1 file changed
+71
-9
+71
-9
@@ -17,12 +17,20 @@
|
|||||||
# ./gates.sh run the simulator gates
|
# ./gates.sh run the simulator gates
|
||||||
# BACKEND=device IOS_DEVICE="iPhone" ./gates.sh
|
# BACKEND=device IOS_DEVICE="iPhone" ./gates.sh
|
||||||
# BACKEND=android ANDROID_DEVICE="663c91b1" ./gates.sh
|
# BACKEND=android ANDROID_DEVICE="663c91b1" ./gates.sh
|
||||||
|
# RUNS=1 SEEDS=303 ./gates.sh re-run just the seed that failed
|
||||||
# ./gates.sh --self-test run the offline analyzer tests only
|
# ./gates.sh --self-test run the offline analyzer tests only
|
||||||
#
|
#
|
||||||
# Tunables (environment):
|
# Tunables (environment):
|
||||||
# RUNS=5 number of serial runs
|
# RUNS=5 number of serial runs
|
||||||
# DURATION=3m per-run fuzz duration
|
# DURATION=3m per-run fuzz duration
|
||||||
# SEED=0 fuzz seed
|
# SEEDS="101 202 303 404 505"
|
||||||
|
# one fuzz seed per run, whitespace separated, consumed in
|
||||||
|
# order. The values differ so the runs explore different
|
||||||
|
# action streams, and they are fixed so a failing gate can
|
||||||
|
# be re-run exactly; the results table prints the seed each
|
||||||
|
# run used. A 0 is rejected: sanderling test reads --seed 0
|
||||||
|
# as "derive the seed from the clock", which is the one
|
||||||
|
# thing a gate cannot reproduce.
|
||||||
# P95_LIMIT_MS G5 p95 step-latency ceiling in ms (default 2500 for iOS,
|
# P95_LIMIT_MS G5 p95 step-latency ceiling in ms (default 2500 for iOS,
|
||||||
# 5500 for the android backend's higher and more variable
|
# 5500 for the android backend's higher and more variable
|
||||||
# per-step USB cost, especially cold right after a reboot)
|
# per-step USB cost, especially cold right after a reboot)
|
||||||
@@ -36,7 +44,7 @@ folio_directory="$(cd "${script_directory}/../examples/folio" && pwd)"
|
|||||||
BACKEND="${BACKEND:-simulator}"
|
BACKEND="${BACKEND:-simulator}"
|
||||||
RUNS="${RUNS:-5}"
|
RUNS="${RUNS:-5}"
|
||||||
DURATION="${DURATION:-3m}"
|
DURATION="${DURATION:-3m}"
|
||||||
SEED="${SEED:-0}"
|
SEEDS="${SEEDS:-101 202 303 404 505}"
|
||||||
# The p95 ceiling is backend-specific: a physical Android device drives every
|
# The p95 ceiling is backend-specific: a physical Android device drives every
|
||||||
# step over USB (snapshot + settle + adb round-trips), so its per-step floor is
|
# step over USB (snapshot + settle + adb round-trips), so its per-step floor is
|
||||||
# several times the iOS simulator's in-process cost. 2500ms was calibrated on
|
# several times the iOS simulator's in-process cost. 2500ms was calibrated on
|
||||||
@@ -51,6 +59,11 @@ SANDERLING="${SANDERLING:-sanderling}"
|
|||||||
IOS_DEVICE="${IOS_DEVICE:-iPhone 17 Pro}"
|
IOS_DEVICE="${IOS_DEVICE:-iPhone 17 Pro}"
|
||||||
ANDROID_DEVICE="${ANDROID_DEVICE:-}"
|
ANDROID_DEVICE="${ANDROID_DEVICE:-}"
|
||||||
|
|
||||||
|
if [[ -n "${SEED:-}" ]]; then
|
||||||
|
echo "gates.sh: SEED is not read any more; set SEEDS to one seed per run" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
bundle_id="app.folio"
|
bundle_id="app.folio"
|
||||||
spec_path="${folio_directory}/sanderling/spec.ts"
|
spec_path="${folio_directory}/sanderling/spec.ts"
|
||||||
android_apk="${folio_directory}/app/androidApp/build/outputs/apk/debug/androidApp-debug.apk"
|
android_apk="${folio_directory}/app/androidApp/build/outputs/apk/debug/androidApp-debug.apk"
|
||||||
@@ -246,10 +259,40 @@ orphan_processes() {
|
|||||||
|
|
||||||
# ---- run orchestration -----------------------------------------------------
|
# ---- run orchestration -----------------------------------------------------
|
||||||
|
|
||||||
|
# Split a seed list into the global seed_list, one entry per requested run.
|
||||||
|
# sanderling test reads --seed 0 as "derive the seed from the clock", so a 0
|
||||||
|
# here would make that run unreproducible and is refused.
|
||||||
|
seed_list=()
|
||||||
|
select_seeds() {
|
||||||
|
local requested="$1"
|
||||||
|
local -a candidates=()
|
||||||
|
read -r -a candidates <<<"$2"
|
||||||
|
if [[ "${#candidates[@]}" -lt "$requested" ]]; then
|
||||||
|
echo "gates.sh: SEEDS has ${#candidates[@]} value(s) but RUNS=${requested}; give one seed per run" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local index seed
|
||||||
|
for ((index = 0; index < ${#candidates[@]}; index++)); do
|
||||||
|
seed="${candidates[index]}"
|
||||||
|
case "$seed" in
|
||||||
|
'' | *[!0-9]*)
|
||||||
|
echo "gates.sh: SEEDS takes whitespace-separated positive integers; got \"${seed}\"" >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [[ "$((10#$seed))" -eq 0 ]]; then
|
||||||
|
echo "gates.sh: SEEDS may not contain 0; sanderling test derives a clock seed from 0, so a gate configured with it cannot be reproduced" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
seed_list=("${candidates[@]:0:requested}")
|
||||||
|
}
|
||||||
|
|
||||||
invoke_sanderling() {
|
invoke_sanderling() {
|
||||||
local output_directory="$1"
|
local output_directory="$1"
|
||||||
local output_log="$2"
|
local output_log="$2"
|
||||||
local exit_status_file="$3"
|
local exit_status_file="$3"
|
||||||
|
local seed="$4"
|
||||||
|
|
||||||
local platform target_flags=()
|
local platform target_flags=()
|
||||||
if [[ "$BACKEND" == "android" ]]; then
|
if [[ "$BACKEND" == "android" ]]; then
|
||||||
@@ -281,7 +324,7 @@ invoke_sanderling() {
|
|||||||
--bundle-id "$bundle_id" \
|
--bundle-id "$bundle_id" \
|
||||||
"${target_flags[@]}" \
|
"${target_flags[@]}" \
|
||||||
--duration "$DURATION" \
|
--duration "$DURATION" \
|
||||||
--seed "$SEED" \
|
--seed "$seed" \
|
||||||
--output "$output_directory" \
|
--output "$output_directory" \
|
||||||
>"$output_log" 2>&1 || status=$?
|
>"$output_log" 2>&1 || status=$?
|
||||||
printf '%s' "$status" >"$exit_status_file"
|
printf '%s' "$status" >"$exit_status_file"
|
||||||
@@ -300,6 +343,8 @@ collect_run_artifacts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run_gates() {
|
run_gates() {
|
||||||
|
select_seeds "$RUNS" "$SEEDS" || exit 2
|
||||||
|
|
||||||
if [[ "$BACKEND" == "android" ]]; then
|
if [[ "$BACKEND" == "android" ]]; then
|
||||||
# Build only; invoke_sanderling reinstalls per run via adb (gradle's ddmlib
|
# Build only; invoke_sanderling reinstalls per run via adb (gradle's ddmlib
|
||||||
# install is flaky on some physical devices).
|
# install is flaky on some physical devices).
|
||||||
@@ -326,7 +371,7 @@ run_gates() {
|
|||||||
local gate_root="${script_directory}/runs/${timestamp}"
|
local gate_root="${script_directory}/runs/${timestamp}"
|
||||||
mkdir -p "$gate_root"
|
mkdir -p "$gate_root"
|
||||||
echo "gate root: ${gate_root}"
|
echo "gate root: ${gate_root}"
|
||||||
echo "backend=${BACKEND} runs=${RUNS} duration=${DURATION} seed=${SEED} p95_limit_ms=${P95_LIMIT_MS}"
|
echo "backend=${BACKEND} runs=${RUNS} duration=${DURATION} seeds=${seed_list[*]} p95_limit_ms=${P95_LIMIT_MS}"
|
||||||
|
|
||||||
local all_latencies="${gate_root}/all-latencies.txt"
|
local all_latencies="${gate_root}/all-latencies.txt"
|
||||||
: >"$all_latencies"
|
: >"$all_latencies"
|
||||||
@@ -335,10 +380,12 @@ run_gates() {
|
|||||||
local run_index
|
local run_index
|
||||||
for ((run_index = 1; run_index <= RUNS; run_index++)); do
|
for ((run_index = 1; run_index <= RUNS; run_index++)); do
|
||||||
local run_directory="${gate_root}/run-${run_index}"
|
local run_directory="${gate_root}/run-${run_index}"
|
||||||
|
local seed="${seed_list[run_index - 1]}"
|
||||||
mkdir -p "$run_directory"
|
mkdir -p "$run_directory"
|
||||||
echo "run ${run_index}/${RUNS} -> ${run_directory}"
|
printf '%s' "$seed" >"${run_directory}/seed"
|
||||||
|
echo "run ${run_index}/${RUNS} seed=${seed} -> ${run_directory}"
|
||||||
|
|
||||||
invoke_sanderling "$run_directory" "${run_directory}/output.log" "${run_directory}/exit_status"
|
invoke_sanderling "$run_directory" "${run_directory}/output.log" "${run_directory}/exit_status" "$seed"
|
||||||
collect_run_artifacts "$run_directory"
|
collect_run_artifacts "$run_directory"
|
||||||
|
|
||||||
g1[run_index]=$(gate_exit_zero "$run_directory" && echo PASS || echo FAIL)
|
g1[run_index]=$(gate_exit_zero "$run_directory" && echo PASS || echo FAIL)
|
||||||
@@ -383,11 +430,12 @@ run_gates() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
printf 'run G1 G2 G3 G4 G5\n'
|
printf 'run seed G1 G2 G3 G4 G5\n'
|
||||||
local verdict="PASS"
|
local verdict="PASS"
|
||||||
for ((run_index = 1; run_index <= RUNS; run_index++)); do
|
for ((run_index = 1; run_index <= RUNS; run_index++)); do
|
||||||
printf '%-4s %-5s %-5s %-5s %-5s %-5s\n' \
|
printf '%-4s %-6s %-5s %-5s %-5s %-5s %-5s\n' \
|
||||||
"$run_index" "${g1[run_index]}" "${g2[run_index]}" "${g3[run_index]}" "${g4[run_index]}" "${g5[run_index]}"
|
"$run_index" "${seed_list[run_index - 1]}" \
|
||||||
|
"${g1[run_index]}" "${g2[run_index]}" "${g3[run_index]}" "${g4[run_index]}" "${g5[run_index]}"
|
||||||
for cell in "${g1[run_index]}" "${g2[run_index]}" "${g3[run_index]}" "${g4[run_index]}" "${g5[run_index]}"; do
|
for cell in "${g1[run_index]}" "${g2[run_index]}" "${g3[run_index]}" "${g4[run_index]}" "${g5[run_index]}"; do
|
||||||
[[ "$cell" == "PASS" ]] || verdict="FAIL"
|
[[ "$cell" == "PASS" ]] || verdict="FAIL"
|
||||||
done
|
done
|
||||||
@@ -399,6 +447,7 @@ run_gates() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
echo "GATES FAIL"
|
echo "GATES FAIL"
|
||||||
|
echo "reproduce one run with: RUNS=1 SEEDS=<seed from the table> BACKEND=${BACKEND} $0"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +496,19 @@ self_test() {
|
|||||||
assert "G4 doubled text caught" FAIL \
|
assert "G4 doubled text caught" FAIL \
|
||||||
"$(gate_no_doubled_text "${testdata}/g4-doubled-text" && echo PASS || echo FAIL)"
|
"$(gate_no_doubled_text "${testdata}/g4-doubled-text" && echo PASS || echo FAIL)"
|
||||||
|
|
||||||
|
assert "seeds default to one distinct value per run" "101 202 303 404 505" \
|
||||||
|
"$(select_seeds 5 "101 202 303 404 505" >/dev/null 2>&1 && echo "${seed_list[*]}")"
|
||||||
|
assert "seeds override honoured, extras ignored" "11 22" \
|
||||||
|
"$(select_seeds 2 "11 22 33" >/dev/null 2>&1 && echo "${seed_list[*]}")"
|
||||||
|
assert "seed 0 rejected" FAIL \
|
||||||
|
"$(select_seeds 1 "0" >/dev/null 2>&1 && echo PASS || echo FAIL)"
|
||||||
|
assert "padded seed 00 rejected" FAIL \
|
||||||
|
"$(select_seeds 1 "00" >/dev/null 2>&1 && echo PASS || echo FAIL)"
|
||||||
|
assert "non-numeric seed rejected" FAIL \
|
||||||
|
"$(select_seeds 1 "abc" >/dev/null 2>&1 && echo PASS || echo FAIL)"
|
||||||
|
assert "seed list shorter than RUNS rejected" FAIL \
|
||||||
|
"$(select_seeds 5 "101 202" >/dev/null 2>&1 && echo PASS || echo FAIL)"
|
||||||
|
|
||||||
local pass_p95 slow_p95
|
local pass_p95 slow_p95
|
||||||
pass_p95="$(emit_step_latencies "${testdata}/pass/trace.jsonl" | p95_of)"
|
pass_p95="$(emit_step_latencies "${testdata}/pass/trace.jsonl" | p95_of)"
|
||||||
slow_p95="$(emit_step_latencies "${testdata}/g5-slow-p95/trace.jsonl" | p95_of)"
|
slow_p95="$(emit_step_latencies "${testdata}/g5-slow-p95/trace.jsonl" | p95_of)"
|
||||||
|
|||||||
Reference in new issue
Block a user