P0.4: wire the logic verification gate (sx assert helper + snapshot runner)
Add a real logic-test gate so future pure-sx game logic fails the build on a bad assertion: - tests/test.sx: `expect(cond, msg)` assert helper — prints a greppable `FAIL <file>:<line>: <msg>` and exits non-zero via process.exit on failure. - tools/run_tests.sh: snapshot runner mirroring sx/tests/run_examples.sh; runs each tests/<name>.sx and diffs stdout + exit code against tests/expected/. Exits 0 iff all tests pass. - tests/arith.sx (+ expected snapshots): seed passing sanity test. - README.md: document both halves of the gate — logic runner and the reproducible ios-sim build/launch sequence (with device discovery).
This commit is contained in:
50
README.md
50
README.md
@@ -7,3 +7,53 @@ A match-3 game written entirely in the **sx** language, targeting **iOS** first.
|
||||
- Verification gate: sx logic tests pass AND the iOS app builds & launches in the Simulator.
|
||||
|
||||
Development is driven by the multi-agent `flow` (Product Owner → Worker → Reviewer → Observer).
|
||||
|
||||
## Verification gate
|
||||
|
||||
The gate has two halves. Both must pass. The sx compiler used below lives at
|
||||
`/Users/agra/projects/sx/zig-out/bin/sx` (override the runner's binary with the
|
||||
`SX` env var). Run everything from the repo root.
|
||||
|
||||
### 1. Logic tests
|
||||
|
||||
Pure-sx logic tests run under `sx` and have their stdout + exit code diffed
|
||||
against committed snapshots in `tests/expected/`. A failed assertion exits the
|
||||
process non-zero, so it fails the runner (and the gate).
|
||||
|
||||
```bash
|
||||
bash tools/run_tests.sh
|
||||
```
|
||||
|
||||
- A test is any `tests/<name>.sx` that has a `tests/expected/<name>.exit`
|
||||
marker; `tests/test.sx` (the `expect` assert helper) has no marker, so it is
|
||||
not itself run.
|
||||
- Regenerate snapshots after an intentional change: `bash tools/run_tests.sh --update`.
|
||||
|
||||
### 2. iOS Simulator build + launch
|
||||
|
||||
Build the app for the simulator, then install/launch it on an available device
|
||||
and screenshot the rendered scene (blue background + a centered orange quad).
|
||||
|
||||
```bash
|
||||
# Build the .app bundle (sx-out/ios/M3te.app):
|
||||
/Users/agra/projects/sx/zig-out/bin/sx build --target ios-sim main.sx
|
||||
|
||||
# Discover an available simulator — do NOT hardcode a udid:
|
||||
xcrun simctl list devices available
|
||||
# e.g. capture the first available device's UDID into $udid:
|
||||
udid=$(xcrun simctl list devices available | grep -Eo '[0-9A-Fa-f-]{36}' | head -1)
|
||||
|
||||
# Boot it (skip if already "Booted") and bring the Simulator window up:
|
||||
xcrun simctl boot "$udid" || true
|
||||
open -a Simulator
|
||||
|
||||
# Install, launch (bundle id co.swipelab.m3te), and screenshot:
|
||||
xcrun simctl install booted sx-out/ios/M3te.app
|
||||
xcrun simctl launch booted co.swipelab.m3te
|
||||
xcrun simctl io booted screenshot /tmp/m3te.png
|
||||
```
|
||||
|
||||
The screenshot should match `goldens/p0_quad.png` (a centered orange quad over a
|
||||
blue clear), modulo the status-bar clock — pixel-exact equality is not required.
|
||||
A tap on the quad flips its color (orange ↔ green); see
|
||||
`goldens/p0_input_before.png` / `goldens/p0_input_after.png`.
|
||||
|
||||
13
tests/arith.sx
Normal file
13
tests/arith.sx
Normal file
@@ -0,0 +1,13 @@
|
||||
// Trivial logic-gate sanity check: exercises the `expect` helper on passing
|
||||
// assertions and prints a stable summary line for the snapshot runner to diff.
|
||||
// Real board-state tests arrive in Phase 1.
|
||||
#import "modules/std.sx";
|
||||
t :: #import "test.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
t.expect(2 + 2 == 4, "two plus two is four");
|
||||
t.expect(7 % 3 == 1, "seven mod three is one");
|
||||
t.expect(10 - 4 == 6, "ten minus four is six");
|
||||
print("ok: arithmetic\n");
|
||||
return 0;
|
||||
}
|
||||
1
tests/expected/arith.exit
Normal file
1
tests/expected/arith.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
tests/expected/arith.stdout
Normal file
1
tests/expected/arith.stdout
Normal file
@@ -0,0 +1 @@
|
||||
ok: arithmetic
|
||||
15
tests/test.sx
Normal file
15
tests/test.sx
Normal file
@@ -0,0 +1,15 @@
|
||||
// m3te logic-test assert helper.
|
||||
//
|
||||
// `expect(cond, msg)` is a no-op when `cond` holds. On a false condition it
|
||||
// prints a single greppable `FAIL <file>:<line>: <msg>` line to stdout and
|
||||
// terminates the process NON-ZERO (exit 1) via process.exit, so a broken
|
||||
// assertion fails `tools/run_tests.sh` and the build gate.
|
||||
#import "modules/std.sx";
|
||||
proc :: #import "modules/process.sx";
|
||||
|
||||
expect :: (cond: bool, msg: string, loc: Source_Location = #caller_location) {
|
||||
if !cond {
|
||||
print("FAIL {}:{}: {}\n", loc.file, loc.line, msg);
|
||||
proc.exit(1);
|
||||
}
|
||||
}
|
||||
112
tools/run_tests.sh
Executable file
112
tools/run_tests.sh
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
# m3te logic-test gate. Mirrors sx/tests/run_examples.sh: run each sx logic
|
||||
# test, diff its stdout + exit code against committed expectations. Exits 0 iff
|
||||
# ALL tests pass, NON-ZERO otherwise (so a broken assertion fails the build gate).
|
||||
#
|
||||
# Usage: bash tools/run_tests.sh [--update]
|
||||
# --update: regenerate the expected .stdout/.exit from current output.
|
||||
#
|
||||
# Layout (a test is any tests/<name>.sx with a tests/expected/<name>.exit marker;
|
||||
# helper modules such as tests/test.sx have no marker, so they are not run):
|
||||
# tests/<name>.sx
|
||||
# tests/expected/<name>.stdout # normalized stdout
|
||||
# tests/expected/<name>.exit # process exit code
|
||||
#
|
||||
# SX binary: override with the SX env var; defaults to the dev build.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SX="${SX:-/Users/agra/projects/sx/zig-out/bin/sx}"
|
||||
TESTS_DIR="$ROOT_DIR/tests"
|
||||
EXPECTED_DIR="$TESTS_DIR/expected"
|
||||
TIMEOUT=30
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
SKIP=0
|
||||
TIMEOUT_COUNT=0
|
||||
UPDATE=0
|
||||
|
||||
if [[ "${1:-}" == "--update" ]]; then
|
||||
UPDATE=1
|
||||
fi
|
||||
|
||||
if [[ ! -x "$SX" ]]; then
|
||||
echo "sx binary not found/executable at: $SX (override with SX=/path/to/sx)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Collapse the absolute test-file path back to its repo-relative form so the
|
||||
# `FAIL <file>:<line>` lines printed by tests/test.sx are checkout-location
|
||||
# independent. Applied identically to expected and actual, so it can only
|
||||
# reconcile location noise, never desync an otherwise-matching pair.
|
||||
normalize() {
|
||||
sed -E -e "s#${ROOT_DIR}/##g"
|
||||
}
|
||||
|
||||
TMP_ERR="$(mktemp)"
|
||||
trap 'rm -f "$TMP_ERR"' EXIT
|
||||
|
||||
shopt -s nullglob
|
||||
markers=("$EXPECTED_DIR"/*.exit)
|
||||
if [[ ${#markers[@]} -eq 0 ]]; then
|
||||
echo "no tests found under $EXPECTED_DIR (expected <name>.exit markers)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for exit_file in "${markers[@]}"; do
|
||||
name=$(basename "$exit_file" .exit)
|
||||
sx_file="$TESTS_DIR/${name}.sx"
|
||||
out_file="$EXPECTED_DIR/${name}.stdout"
|
||||
|
||||
if [[ ! -f "$sx_file" ]]; then
|
||||
printf " %-40s SKIP (missing %s)\n" "$name" "tests/${name}.sx"
|
||||
SKIP=$((SKIP + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
printf " %-40s" "$name"
|
||||
actual_out=$(timeout "$TIMEOUT" "$SX" run "$sx_file" 2>"$TMP_ERR" | normalize)
|
||||
actual_exit=${PIPESTATUS[0]}
|
||||
|
||||
if [[ $actual_exit -eq 124 ]]; then
|
||||
TIMEOUT_COUNT=$((TIMEOUT_COUNT + 1))
|
||||
echo "TIMEOUT (>${TIMEOUT}s)"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ $UPDATE -eq 1 ]]; then
|
||||
printf '%s\n' "$actual_out" > "$out_file"
|
||||
printf '%s\n' "$actual_exit" > "$exit_file"
|
||||
echo "updated (exit=$actual_exit)"
|
||||
continue
|
||||
fi
|
||||
|
||||
expected_out=$(normalize < "$out_file" 2>/dev/null)
|
||||
expected_exit=$(cat "$exit_file")
|
||||
|
||||
out_ok=true; exit_ok=true
|
||||
[[ "$actual_out" == "$expected_out" ]] || out_ok=false
|
||||
[[ "$actual_exit" == "$expected_exit" ]] || exit_ok=false
|
||||
|
||||
if $out_ok && $exit_ok; then
|
||||
PASS=$((PASS + 1))
|
||||
echo "ok"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
echo "FAIL"
|
||||
$out_ok || { echo " --- stdout diff (expected vs actual) ---"; diff <(printf '%s\n' "$expected_out") <(printf '%s\n' "$actual_out") || true; }
|
||||
$exit_ok || echo " exit code: expected=$expected_exit actual=$actual_exit"
|
||||
[[ -s "$TMP_ERR" ]] && { echo " --- stderr ---"; cat "$TMP_ERR"; }
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $UPDATE -eq 1 ]]; then
|
||||
echo "Updated all expected output files."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "$PASS passed, $FAIL failed, $SKIP skipped, $TIMEOUT_COUNT timed out"
|
||||
[[ $FAIL -eq 0 && $TIMEOUT_COUNT -eq 0 ]]
|
||||
Reference in New Issue
Block a user