writeonce/scripts/single-binary-smoke.sh
shoney.arickathil 37d3ba1543 fix(compiler): woc build -o creates the output's parent directory — a fresh checkout has no target/
- `woc build <dir> -o <example>/target/<name>` wrote its temp file beside the
  output and failed with "No such file or directory" when target/ was absent;
  target/ is gitignored, so every fresh checkout hit it — the db-actor and
  db-bench gates went red on a clean master worktree while passing on dev,
  where the directories exist from history
- the driver now creates the output's parent (mkdir -p shape) before the
  temp write; project-mode builds and existing directories are unchanged
- single-binary-smoke.sh gains `build-into-missing-dir` (4 checks, was 3),
  red on the old driver, green on this one; woc-test clean

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit e9213bb949e1c26cb3a29d6f9babf2322a82d839)
2026-09-15 01:34:13 +02:00

133 lines
5.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# scripts/single-binary-smoke.sh — Task 6 (plan 3): proves `woc build`
# produces a genuinely self-contained executable, and that a corrupted
# trailer (docs/plan/oop-vm/00-wob-format.md, "single-binary trailer"
# section) fails clearly instead of crashing or misbehaving silently.
#
# Steps: build the hello fixture into one file; copy it to a temp
# directory OUTSIDE the repo (hardcoded under /tmp, not $TMPDIR — the
# whole point is proving nothing repo-relative is needed at run time);
# run it there with no arguments; diff stdout byte-for-byte. Then corrupt
# the trailer's payload_len field and confirm a clear exit-2 error, not a
# crash or a hang.
#
# Called by scripts/oop-e2e.sh (its ok/FAIL lines fold into that
# harness's tally); also runnable standalone. Same output shape as
# oop-e2e.sh: "ok NAME" / "FAIL NAME -- reason", one line per check.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WOC="$ROOT/compiler/_build/default/bin/woc"
WOVM="$ROOT/runtime/wovm"
FIXTURE="$ROOT/tests/corpus/run/hello/fixture.wo"
EXPECTED="$ROOT/tests/corpus/run/hello/fixture.out"
TIMEOUT="${OOP_E2E_TIMEOUT:-10}"
if [[ ! -x "$WOC" ]]; then
echo "single-binary-smoke: woc is not built ($WOC) — run: just woc-build" >&2
exit 1
fi
if [[ ! -x "$WOVM" ]]; then
echo "single-binary-smoke: wovm is not built ($WOVM) — run: make -C runtime wovm" >&2
exit 1
fi
pass=0
fail=0
ok() { echo "ok $1"; pass=$((pass + 1)); }
bad() { echo "FAIL $1 -- $2"; fail=$((fail + 1)); }
WORK="$(mktemp -d "${TMPDIR:-/tmp}/wo-single-binary-smoke.XXXXXX")"
# Deliberately NOT under $WORK / $TMPDIR: this directory is the actual
# proof of self-containment, so it must be unambiguously outside the repo
# regardless of how TMPDIR is set in the environment running the harness.
ELSEWHERE="$(mktemp -d /tmp/wo-single-binary-elsewhere.XXXXXX)"
trap 'rm -rf "$WORK" "$ELSEWHERE"' EXIT
echo "single-binary-smoke: relocation dir = $ELSEWHERE"
# ---- build the hello fixture into one file --------------------------------
SRC="$WORK/src"
mkdir -p "$SRC"
cp "$FIXTURE" "$SRC/fixture.wo"
APP="$WORK/app"
if ! timeout "$TIMEOUT" "$WOC" build "$SRC" -o "$APP" --runtime "$WOVM" \
>"$WORK/build.out" 2>"$WORK/build.err"; then
rc=$?
bad "single-binary/build" "woc build exited $rc: $(head -1 "$WORK/build.err")"
elif [[ ! -x "$APP" ]]; then
bad "single-binary/build" "output missing or not executable: $APP"
else
ok "single-binary/build"
fi
# ---- -o into a directory that does not exist yet ---------------------------
# A fresh checkout has no target/ anywhere (gitignored), and every example
# gate builds with `-o <example>/target/<name>`: `woc build` must create the
# output's parent rather than fail on its own temp file there.
FRESH="$WORK/fresh/target/app"
if ! timeout "$TIMEOUT" "$WOC" build "$SRC" -o "$FRESH" --runtime "$WOVM" \
>"$WORK/fresh.out" 2>"$WORK/fresh.err"; then
rc=$?
bad "single-binary/build-into-missing-dir" "woc build exited $rc: $(head -1 "$WORK/fresh.err")"
elif [[ ! -x "$FRESH" ]]; then
bad "single-binary/build-into-missing-dir" "output missing or not executable: $FRESH"
else
ok "single-binary/build-into-missing-dir"
fi
# ---- relocate outside the repo, run with no args, diff --------------------
if [[ -x "$APP" ]]; then
cp "$APP" "$ELSEWHERE/app"
chmod +x "$ELSEWHERE/app"
( cd / && timeout "$TIMEOUT" "$ELSEWHERE/app" ) >"$WORK/relocated.out" 2>"$WORK/relocated.err"
rc=$?
if [[ $rc -ne 0 ]]; then
bad "single-binary/relocated-run" "exit $rc: $(head -1 "$WORK/relocated.err")"
elif ! diff -q "$EXPECTED" "$WORK/relocated.out" >/dev/null 2>&1; then
bad "single-binary/relocated-run" "stdout mismatch"
diff -u "$EXPECTED" "$WORK/relocated.out" | sed 's/^/ /'
else
ok "single-binary/relocated-run"
fi
else
bad "single-binary/relocated-run" "skipped: no app to relocate"
fi
# ---- corrupt the trailer's payload_len field, confirm a clear failure -----
# Trailer is the last 20 bytes (payload_off u64, payload_len u64, magic
# u32): payload_len sits 12 bytes from EOF. Overwriting it with all-0xFF
# breaks the reader's "payload_off + payload_len == file_size - 20" check
# without touching the magic, so this exercises the "recognized trailer,
# bad bounds" path specifically (not the "no trailer at all" fallback).
if [[ -f "$ELSEWHERE/app" ]]; then
CORRUPT="$WORK/corrupt-app"
cp "$ELSEWHERE/app" "$CORRUPT"
chmod +x "$CORRUPT"
size=$(stat -c%s "$CORRUPT")
printf '\xff\xff\xff\xff\xff\xff\xff\xff' \
| dd of="$CORRUPT" bs=1 seek=$((size - 12)) count=8 conv=notrunc status=none
timeout "$TIMEOUT" "$CORRUPT" >"$WORK/corrupt.out" 2>"$WORK/corrupt.err"
rc=$?
if [[ $rc -eq 124 ]]; then
bad "single-binary/corrupt-trailer" "timed out after ${TIMEOUT}s instead of failing cleanly"
elif [[ $rc -gt 128 ]]; then
bad "single-binary/corrupt-trailer" "crashed (signal $((rc - 128))) instead of failing cleanly"
elif [[ $rc -ne 2 ]]; then
bad "single-binary/corrupt-trailer" "expected exit 2, got $rc: $(head -1 "$WORK/corrupt.err")"
elif [[ -s "$WORK/corrupt.out" ]]; then
bad "single-binary/corrupt-trailer" "exit 2 but stdout was not empty (partial run before failing?)"
elif ! grep -qi "corrupt trailer" "$WORK/corrupt.err"; then
bad "single-binary/corrupt-trailer" "exit 2 but no clear message: $(head -1 "$WORK/corrupt.err")"
else
ok "single-binary/corrupt-trailer"
fi
else
bad "single-binary/corrupt-trailer" "skipped: no relocated app to corrupt"
fi
total=$((pass + fail))
printf 'single-binary-smoke: %d checks, %d failures\n' "$total" "$fail"
[[ $fail -eq 0 ]]