Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 40s
blob/put|get|has? backed by <root>/blobs/<cid>, CIDv1 (raw codec,
sha2-256 via Sx_cid/Sx_sha2). put idempotent; persist stores only the
{:cid :size :mime} ref. persist_durable_test.sh extended (8/8): blob
round-trip + content-address idempotency + bytes/ref surviving real
restart. Mock blob suite 14/0 on worktree binary. Durable-storage
Blocker now CLOSED.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
145 lines
5.6 KiB
Bash
Executable File
145 lines
5.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# hosts/ocaml/test/persist_durable_test.sh
|
|
# Acceptance test for the host durable-storage adapter (Sx_persist_store).
|
|
#
|
|
# Exercises `persist/durable-backend` (REAL `perform`, not the mock) under the
|
|
# WORKTREE-built sx_server.exe, and asserts:
|
|
# 1. durable: writes land on disk and read back (the silent-data-loss repro
|
|
# from plans/persist-on-sx.md now returns correct values).
|
|
# 2. last-seq is monotonic across truncate (compaction never reassigns a seq).
|
|
# 3. kv ops round-trip and delete.
|
|
# 4. recovery: a REAL process restart (write, exit, fresh process, replay)
|
|
# recovers state from disk.
|
|
#
|
|
# Run from repo root or anywhere; locates the worktree binary relative to itself.
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT="$(cd "$HERE/../../.." && pwd)" # repo/worktree root
|
|
cd "$ROOT"
|
|
|
|
SX="hosts/ocaml/_build/default/bin/sx_server.exe"
|
|
if [ ! -x "$SX" ]; then
|
|
echo "ERROR: worktree binary not found at $SX — build it first:" >&2
|
|
echo " (cd hosts/ocaml && dune build bin/sx_server.exe)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
DATADIR="$(mktemp -d)"
|
|
trap 'rm -rf "$DATADIR"' EXIT
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
check() { # check <label> <got> <expected>
|
|
if [ "$2" = "$3" ]; then
|
|
PASS=$((PASS + 1)); printf ' ok %-40s => %s\n' "$1" "$2"
|
|
else
|
|
FAIL=$((FAIL + 1)); printf ' FAIL %-40s got [%s] want [%s]\n' "$1" "$2" "$3"
|
|
fi
|
|
}
|
|
|
|
PRELUDE='(epoch 1)
|
|
(load "spec/stdlib.sx")
|
|
(load "lib/r7rs.sx")
|
|
(load "lib/persist/event.sx")
|
|
(load "lib/persist/backend.sx")
|
|
(load "lib/persist/log.sx")
|
|
(load "lib/persist/kv.sx")
|
|
(load "lib/persist/durable.sx")
|
|
(load "lib/persist/blob.sx")
|
|
(epoch 2)'
|
|
|
|
# run_eval <sx-expr-string>: prints the final (ok-len 2 ...) payload line.
|
|
run_eval() {
|
|
local expr="$1"
|
|
printf '%s\n(eval %s)\n' "$PRELUDE" "$expr" \
|
|
| SX_PERSIST_DIR="$DATADIR" timeout 60 "$SX" 2>/dev/null \
|
|
| awk '/^\(ok-len 2 / {getline; print; exit}'
|
|
}
|
|
|
|
# escape an SX program into a single-line double-quoted SX string literal for
|
|
# (eval "..."). The REPL reads one command per physical line, so newlines in the
|
|
# program are collapsed to spaces.
|
|
q() { printf '"%s"' "$(printf '%s' "$1" | tr '\n' ' ' | sed 's/\\/\\\\/g; s/"/\\"/g')"; }
|
|
|
|
echo "== durable: append/read/last-seq round-trip on disk =="
|
|
GOT=$(run_eval "$(q '(let ((b (persist/durable-backend)))
|
|
(begin
|
|
(persist/append b "s" "x" 0 {:v 1})
|
|
(persist/append b "s" "x" 0 {:v 2})
|
|
(list (persist/event-seq (persist/append b "s" "x" 0 {:v 3}))
|
|
(persist/count b "s")
|
|
(len (persist/read b "s")))))')")
|
|
check "append/count/read" "$GOT" "(3 3 3)"
|
|
|
|
echo "== last-seq monotonic across truncate =="
|
|
GOT=$(run_eval "$(q '(let ((b (persist/durable-backend)))
|
|
(begin
|
|
(persist/append b "t" "x" 0 {})
|
|
(persist/append b "t" "x" 0 {})
|
|
(persist/append b "t" "x" 0 {})
|
|
(persist/truncate b "t" 2)
|
|
(list (persist/last-seq b "t") (persist/count b "t"))))')")
|
|
check "last-seq survives truncate" "$GOT" "(3 1)"
|
|
|
|
echo "== streams set survives compaction =="
|
|
GOT=$(run_eval "$(q '(let ((b (persist/durable-backend)))
|
|
(sort ((get b "streams"))))')")
|
|
check "streams" "$GOT" '("s" "t")'
|
|
|
|
echo "== kv round-trip + delete =="
|
|
GOT=$(run_eval "$(q '(let ((b (persist/durable-backend)))
|
|
(begin
|
|
(persist/kv-put b "k" {:a 1 :b "two"})
|
|
(persist/kv-put b "gone" 9)
|
|
(persist/kv-delete b "gone")
|
|
(list (get (persist/kv-get b "k") :b)
|
|
(persist/kv-has? b "k")
|
|
(persist/kv-has? b "gone"))))')")
|
|
check "kv get/has/delete" "$GOT" '("two" true false)'
|
|
|
|
echo "== recovery: state survives a REAL process restart =="
|
|
# write in process A then let it exit; the next run is a brand-new process.
|
|
run_eval "$(q '(let ((b (persist/durable-backend)))
|
|
(begin
|
|
(persist/append b "r" "ev" 0 {:n 1})
|
|
(persist/append b "r" "ev" 0 {:n 2})
|
|
(persist/kv-put b "survive" "yes")
|
|
(persist/count b "r")))')" >/dev/null
|
|
# fresh process, same SX_PERSIST_DIR — must replay from disk.
|
|
GOT=$(run_eval "$(q '(let ((b (persist/durable-backend)))
|
|
(list (persist/count b "r")
|
|
(persist/last-seq b "r")
|
|
(get (get (nth (persist/read b "r") 1) :data) :n)
|
|
(persist/kv-get b "survive")))')")
|
|
check "recovered after restart" "$GOT" '(2 2 2 "yes")'
|
|
|
|
echo "== blob: content-addressed put/get/has? round-trip =="
|
|
GOT=$(run_eval "$(q '(let ((bs (persist/blob-store-backend)))
|
|
(let ((r (persist/blob-store bs "hello world" "text/plain")))
|
|
(list (persist/blob-size r)
|
|
(persist/blob-mime r)
|
|
(persist/blob-fetch bs r)
|
|
(persist/blob-exists? bs r))))')")
|
|
check "blob size/mime/fetch/exists" "$GOT" '(11 "text/plain" "hello world" true)'
|
|
|
|
echo "== blob: put is content-addressed (idempotent cid) =="
|
|
GOT=$(run_eval "$(q '(let ((bs (persist/blob-store-backend)))
|
|
(equal? (persist/blob-cid (persist/blob-store bs "same bytes" "x"))
|
|
(persist/blob-cid (persist/blob-store bs "same bytes" "x"))))')")
|
|
check "same bytes -> same cid" "$GOT" "true"
|
|
|
|
echo "== blob: bytes + ref-in-kv survive a REAL restart =="
|
|
# process A: store a blob, keep only its ref in the durable kv.
|
|
run_eval "$(q '(let ((b (persist/durable-backend)) (bs (persist/blob-store-backend)))
|
|
(begin (persist/kv-put b "logo" (persist/blob-store bs "PNGDATA" "image/png")) nil))')" >/dev/null
|
|
# fresh process: read the ref from kv, fetch the bytes from the blob store.
|
|
GOT=$(run_eval "$(q '(let ((b (persist/durable-backend)) (bs (persist/blob-store-backend)))
|
|
(let ((r (persist/kv-get b "logo")))
|
|
(list (persist/blob-fetch bs r) (persist/blob-exists? bs r) (persist/blob-mime r))))')")
|
|
check "blob recovered via ref after restart" "$GOT" '("PNGDATA" true "image/png")'
|
|
|
|
echo
|
|
echo "durable adapter: $PASS passed, $FAIL failed"
|
|
[ "$FAIL" -eq 0 ]
|