Closes tickets 0001 (portal-rng) and 0002 (os-entropy-seed). Ticket 0001 goal 4 called for proof that seeding with k, drawing N, saving, clearing, resuming in any impl, and drawing M more produces a full stream matching a single-process Python baseline bit-for-bit. Prior tests/portal-cross-test.sh exercised producer-consumer agreement but used a producer-side self-computed baseline; it did not compare against an independent Python run that never saves or resumes. tests/portal-rng-cross-test.sh computes a single-process Python baseline once (seed=42, N+M=10 draws, no portal), then runs all 9 producer x consumer cells (Python, C, asm each side) and checks that producer's first N plus consumer's M equals the independent baseline. All 12 assertions pass. Wired into make test-all. Ticket status updated to resolved on both 0001 and 0002 with dated one-line resolution notes.
146 lines
5.1 KiB
Bash
Executable file
146 lines
5.1 KiB
Bash
Executable file
#!/bin/bash
|
||
# portal-rng-cross-test.sh — ticket 0001 deliverable (goal 4):
|
||
#
|
||
# "seed k, draw N values, save, clear, resume in any impl, draw M more —
|
||
# full stream matches a single-process Python baseline."
|
||
#
|
||
# This script proves that every (producer, consumer) pair across
|
||
# Python / C / asm reproduces the exact same N+M stream that a single
|
||
# Python process produces with no save/resume at all. That is the
|
||
# stronger claim than portal-cross-test.sh's producer-side self-
|
||
# consistency: here the truth is computed independently, once, upfront,
|
||
# by a Python run that never touches the portal. Every cell compares
|
||
# against the same independent baseline.
|
||
#
|
||
# Matrix: 3 producers × 3 consumers = 9 cells. K=42, N=5, M=5.
|
||
|
||
set -e
|
||
cd "$(dirname "$0")/.."
|
||
|
||
PY="python3 lumbda.py --fast"
|
||
C="./c/lumbda"
|
||
ASM="./asm/lumbda"
|
||
|
||
K=42 # seed
|
||
N=5 # draws before save
|
||
M=5 # draws after resume
|
||
RANGE=1000000 # random-int upper bound
|
||
|
||
# ─── Compute the independent single-process Python baseline (N+M draws) ───
|
||
BASELINE_LSP=$(mktemp --suffix=.lsp)
|
||
trap 'rm -f "$BASELINE_LSP" /tmp/rng-xstream.sexp' EXIT INT TERM
|
||
|
||
cat > "$BASELINE_LSP" <<EOF
|
||
(random-seed! $K)
|
||
;; for-each returns a non-printing value across Python/C/asm, so no
|
||
;; trailing token leaks into stdout. Iteration count is carried by the
|
||
;; length of the dummy list below (shell-expanded once).
|
||
(for-each (lambda (_) (display (random-int $RANGE)) (newline))
|
||
'($(seq 1 $((N + M)) | tr '\n' ' ')))
|
||
EOF
|
||
|
||
# shellcheck disable=SC2086
|
||
BASELINE=$($PY "$BASELINE_LSP")
|
||
BASELINE_COUNT=$(printf "%s\n" "$BASELINE" | wc -l)
|
||
|
||
if [ "$BASELINE_COUNT" -ne $((N + M)) ]; then
|
||
echo "FAIL: baseline produced $BASELINE_COUNT draws, expected $((N + M))"
|
||
exit 1
|
||
fi
|
||
|
||
# Split baseline into first-N and next-M.
|
||
BASELINE_N=$(printf "%s\n" "$BASELINE" | head -n $N)
|
||
BASELINE_M=$(printf "%s\n" "$BASELINE" | tail -n $M)
|
||
|
||
echo "═══════════════════════════════════════════════════════"
|
||
echo "RNG portal cross-impl round-trip — independent Python baseline"
|
||
echo "Ticket: docs/tickets/0001-portal-rng.md (goal 4)"
|
||
echo " seed=$K, save after N=$N draws, resume for M=$M draws"
|
||
echo "═══════════════════════════════════════════════════════"
|
||
echo
|
||
echo "single-process Python baseline (N+M=$((N + M)) draws):"
|
||
printf "%s\n" "$BASELINE" | sed 's/^/ /'
|
||
echo
|
||
|
||
# ─── Producer script (generated): seed, draw N, emit them to stdout, save state ───
|
||
PROD_LSP=$(mktemp --suffix=.lsp)
|
||
cat > "$PROD_LSP" <<EOF
|
||
(random-seed! $K)
|
||
(for-each (lambda (_) (display (random-int $RANGE)) (newline))
|
||
'($(seq 1 $N | tr '\n' ' ')))
|
||
(define snapshot (random-state))
|
||
(define port (open-output-file "/tmp/rng-xstream.sexp"))
|
||
(display "(random-state! '" port) (write snapshot port) (display ")\n" port)
|
||
(close-port port)
|
||
EOF
|
||
|
||
# ─── Consumer script (generated): load state, draw M, emit them to stdout ───
|
||
CONS_LSP=$(mktemp --suffix=.lsp)
|
||
cat > "$CONS_LSP" <<EOF
|
||
(load "/tmp/rng-xstream.sexp")
|
||
(for-each (lambda (_) (display (random-int $RANGE)) (newline))
|
||
'($(seq 1 $M | tr '\n' ' ')))
|
||
EOF
|
||
|
||
# shellcheck disable=SC2064
|
||
trap "rm -f '$BASELINE_LSP' '$PROD_LSP' '$CONS_LSP' /tmp/rng-xstream.sexp" EXIT INT TERM
|
||
|
||
PASS=0
|
||
FAIL=0
|
||
|
||
run_producer() {
|
||
local impl="$1"
|
||
case "$impl" in
|
||
Python) $PY "$PROD_LSP" ;;
|
||
C) $C "$PROD_LSP" ;;
|
||
Asm) $ASM < "$PROD_LSP" ;;
|
||
esac
|
||
}
|
||
|
||
run_consumer() {
|
||
local impl="$1"
|
||
case "$impl" in
|
||
Python) $PY "$CONS_LSP" ;;
|
||
C) $C "$CONS_LSP" ;;
|
||
Asm) $ASM < "$CONS_LSP" ;;
|
||
esac
|
||
}
|
||
|
||
verify_stream() {
|
||
local label="$1" expected="$2" actual="$3"
|
||
if [ "$expected" = "$actual" ]; then
|
||
PASS=$((PASS + 1))
|
||
printf " %-22s OK\n" "$label"
|
||
else
|
||
FAIL=$((FAIL + 1))
|
||
printf " %-22s FAIL\n" "$label"
|
||
echo " expected:"
|
||
printf "%s\n" "$expected" | sed 's/^/ /'
|
||
echo " actual:"
|
||
printf "%s\n" "$actual" | sed 's/^/ /'
|
||
fi
|
||
}
|
||
|
||
for P_NAME in Python C Asm; do
|
||
echo "── producer: $P_NAME ──"
|
||
rm -f /tmp/rng-xstream.sexp
|
||
PROD_OUT=$(run_producer "$P_NAME")
|
||
|
||
# Producer's first N must equal the Python baseline's first N.
|
||
verify_stream "$P_NAME: first $N draws" "$BASELINE_N" "$PROD_OUT"
|
||
|
||
for C_NAME in Python C Asm; do
|
||
CONS_OUT=$(run_consumer "$C_NAME")
|
||
# Consumer's M draws after resume must equal the Python baseline's last M.
|
||
verify_stream "$P_NAME -> $C_NAME (next $M)" "$BASELINE_M" "$CONS_OUT"
|
||
done
|
||
done
|
||
|
||
echo
|
||
echo "═══════════════════════════════════════════════════════"
|
||
echo "Cross-impl RNG round-trip vs Python baseline: $PASS passed, $FAIL failed"
|
||
echo "═══════════════════════════════════════════════════════"
|
||
|
||
if [ $FAIL -ne 0 ]; then
|
||
exit 1
|
||
fi
|