#!/usr/bin/env bash # next/tests/log_disk.sh — Step 3b on-disk log acceptance test. # # Exercises log:open_disk/2, append/2 (write-through), and the # read-segment-on-reopen path. Uses next/kernel/term_codec.erl for # the entry encoding and a 4-byte big-endian length prefix per frame. set -uo pipefail cd "$(git rev-parse --show-toplevel)" SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}" if [ ! -x "$SX_SERVER" ]; then SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe" fi if [ ! -x "$SX_SERVER" ]; then echo "ERROR: sx_server.exe not found." >&2 exit 1 fi # Fixed tmp dir so we can refer to it as an Erlang binary literal. DISK_BASE=/tmp/fed_sx_m1_log_disk rm -rf "$DISK_BASE" mkdir -p "$DISK_BASE" # Pre-write a corrupted segment file for the corrupt-detect test # (just a truncated 4-byte length header with no payload). printf '\x00\x00\x00\x05XX' > "$DISK_BASE/corrupted.log" VERBOSE="${1:-}" PASS=0; FAIL=0; ERRORS="" TMPFILE=$(mktemp); trap "rm -f $TMPFILE; rm -rf $DISK_BASE" EXIT cat > "$TMPFILE" <<'EPOCHS' (epoch 1) (load "lib/erlang/tokenizer.sx") (load "lib/erlang/parser.sx") (load "lib/erlang/parser-core.sx") (load "lib/erlang/parser-expr.sx") (load "lib/erlang/parser-module.sx") (load "lib/erlang/transpile.sx") (load "lib/erlang/runtime.sx") (load "lib/erlang/vm/dispatcher.sx") (epoch 2) (eval "(get (erlang-load-module (file-read \"next/kernel/term_codec.erl\")) :name)") (epoch 3) (eval "(get (erlang-load-module (file-read \"next/kernel/log.erl\")) :name)") ;; Base path: /tmp/fed_sx_m1_log_disk constructed as an Erlang binary ;; via list_to_binary of the char codes. (`<<"...">>` literals don't ;; carry through in this port — see Step 3b substrate fix #2.) ;; --- 3a in-memory open/2 still works unchanged --- (epoch 10) (eval "(get (erlang-eval-ast \"{ok, L} = log:open(alice, base), log:tip(L) =:= 0\") :name)") ;; --- open_disk on missing file returns empty fresh state --- (epoch 20) (eval "(get (erlang-eval-ast \"Base = list_to_binary([$/, $t, $m, $p, $/, $f, $e, $d, $_, $s, $x, $_, $m, $1, $_, $l, $o, $g, $_, $d, $i, $s, $k]), {ok, L} = log:open_disk(alice, Base), log:tip(L) =:= 0\") :name)") (epoch 21) (eval "(get (erlang-eval-ast \"Base = list_to_binary([$/, $t, $m, $p, $/, $f, $e, $d, $_, $s, $x, $_, $m, $1, $_, $l, $o, $g, $_, $d, $i, $s, $k]), {ok, L} = log:open_disk(alice, Base), log:entries(L) =:= []\") :name)") ;; --- append + re-open: entries match --- (epoch 30) (eval "(get (erlang-eval-ast \"Base = list_to_binary([$/, $t, $m, $p, $/, $f, $e, $d, $_, $s, $x, $_, $m, $1, $_, $l, $o, $g, $_, $d, $i, $s, $k]), {ok, L0} = log:open_disk(bob, Base), {ok, L1, _} = log:append(L0, hello), {ok, L2, _} = log:append(L1, world), {ok, L3} = log:open_disk(bob, Base), log:entries(L3) =:= [hello, world]\") :name)") ;; --- tip resumes correctly across restart --- (epoch 31) (eval "(get (erlang-eval-ast \"Base = list_to_binary([$/, $t, $m, $p, $/, $f, $e, $d, $_, $s, $x, $_, $m, $1, $_, $l, $o, $g, $_, $d, $i, $s, $k]), {ok, L0} = log:open_disk(carol, Base), {ok, L1, _} = log:append(L0, a), {ok, L2, _} = log:append(L1, b), {ok, L3, _} = log:append(L2, c), {ok, L4} = log:open_disk(carol, Base), log:tip(L4) =:= 3\") :name)") ;; --- replay/3 over re-opened state visits append order --- (epoch 32) (eval "(get (erlang-eval-ast \"Base = list_to_binary([$/, $t, $m, $p, $/, $f, $e, $d, $_, $s, $x, $_, $m, $1, $_, $l, $o, $g, $_, $d, $i, $s, $k]), {ok, L0} = log:open_disk(dave, Base), {ok, L1, _} = log:append(L0, a), {ok, L2, _} = log:append(L1, b), {ok, L3, _} = log:append(L2, c), {ok, L4} = log:open_disk(dave, Base), log:replay(L4, [], fun (X, S, Acc) -> [{S, X} | Acc] end) =:= [{2,c},{1,b},{0,a}]\") :name)") ;; --- mixed types round-trip (atom, int, binary, tuple, list) --- (epoch 33) (eval "(get (erlang-eval-ast \"Base = list_to_binary([$/, $t, $m, $p, $/, $f, $e, $d, $_, $s, $x, $_, $m, $1, $_, $l, $o, $g, $_, $d, $i, $s, $k]), {ok, L0} = log:open_disk(eve, Base), {ok, L1, _} = log:append(L0, foo), {ok, L2, _} = log:append(L1, 42), {ok, L3, _} = log:append(L2, <<1,2,3>>), {ok, L4, _} = log:append(L3, {pair, alice, bob}), {ok, L5, _} = log:append(L4, [1, two, <<3>>]), {ok, L6} = log:open_disk(eve, Base), log:entries(L6) =:= [foo, 42, <<1,2,3>>, {pair, alice, bob}, [1, two, <<3>>]]\") :name)") ;; --- continuing to append after reopen preserves chronology --- (epoch 34) (eval "(get (erlang-eval-ast \"Base = list_to_binary([$/, $t, $m, $p, $/, $f, $e, $d, $_, $s, $x, $_, $m, $1, $_, $l, $o, $g, $_, $d, $i, $s, $k]), {ok, L0} = log:open_disk(frank, Base), {ok, L1, _} = log:append(L0, a), {ok, L2} = log:open_disk(frank, Base), {ok, L3, S} = log:append(L2, b), {S, log:tip(L3)} =:= {1, 2}\") :name)") ;; --- corrupted segment returns {error, _} not crash --- (epoch 40) (eval "(get (erlang-eval-ast \"Base = list_to_binary([$/, $t, $m, $p, $/, $f, $e, $d, $_, $s, $x, $_, $m, $1, $_, $l, $o, $g, $_, $d, $i, $s, $k]), element(1, log:open_disk(corrupted, Base))\") :name)") ;; --- per-actor isolation: two disk-backed logs are independent --- (epoch 41) (eval "(get (erlang-eval-ast \"Base = list_to_binary([$/, $t, $m, $p, $/, $f, $e, $d, $_, $s, $x, $_, $m, $1, $_, $l, $o, $g, $_, $d, $i, $s, $k]), {ok, LA0} = log:open_disk(g1, Base), {ok, LB0} = log:open_disk(g2, Base), {ok, LA1, _} = log:append(LA0, x), {ok, LB1, _} = log:append(LB0, y1), {ok, LB2, _} = log:append(LB1, y2), {ok, LAr} = log:open_disk(g1, Base), {ok, LBr} = log:open_disk(g2, Base), {log:entries(LAr), log:entries(LBr)} =:= {[x], [y1, y2]}\") :name)") EPOCHS OUTPUT=$(timeout 90 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) check() { local epoch="$1" desc="$2" expected="$3" local actual actual=$(echo "$OUTPUT" | grep -A1 "^(ok-len $epoch " | tail -1 || true) if echo "$actual" | grep -q "^(ok-len"; then actual=""; fi if [ -z "$actual" ]; then actual=$(echo "$OUTPUT" | grep "^(ok $epoch " | head -1 || true) fi if [ -z "$actual" ]; then actual=$(echo "$OUTPUT" | grep "^(error $epoch " | head -1 || true) fi [ -z "$actual" ] && actual="" if echo "$actual" | grep -qF -- "$expected"; then PASS=$((PASS+1)) [ "$VERBOSE" = "-v" ] && echo " ok $desc" else FAIL=$((FAIL+1)) ERRORS+=" FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual " fi } check 2 "term_codec loads" "term_codec" check 3 "log module loads" "log" check 10 "3a in-memory open/2 compat" "true" check 20 "open_disk missing -> tip 0" "true" check 21 "open_disk missing -> []" "true" check 30 "append+reopen entries match" "true" check 31 "tip resumes after restart" "true" check 32 "replay chronological" "true" check 33 "mixed types round-trip" "true" check 34 "append after reopen" "true" check 40 "corrupted segment -> error" "error" check 41 "per-actor isolation" "true" TOTAL=$((PASS+FAIL)) if [ $FAIL -eq 0 ]; then echo "ok $PASS/$TOTAL log_disk tests passed" else echo "FAIL $PASS/$TOTAL passed, $FAIL failed:" echo "$ERRORS" fi [ $FAIL -eq 0 ]