#!/usr/bin/env bash # next/tests/peer_actors.sh — m2 Step 5c test. # # Peer-actors cache for the federation inbox handler. Tracks # {PeerActorId, PeerActorState} pairs so signature verification # can be done against a peer's :public_keys without re-fetching # their actor doc on every inbound. lookup_or_fetch/3 is the # load-bearing entry point: cache hit returns cached AS, miss # invokes the caller-supplied FetchFn and stores its result. 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 VERBOSE="${1:-}" PASS=0; FAIL=0; ERRORS="" TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT SETUP='K1 = <<1,2,3,4>>, BobAS = [{public_keys,[[{id,k1},{created,0},{value,K1}]]}], K2 = <<5,6,7,8>>, CarolAS = [{public_keys,[[{id,k1},{created,0},{value,K2}]]}], OkFetch = fun(bob) -> {ok, BobAS}; (carol) -> {ok, CarolAS}; (_) -> {error, not_found} end,' cat > "$TMPFILE" < ok; _ -> bad end\") :name)") ;; lookup_or_fetch hit returns cached value without invoking FetchFn (epoch 17) (eval "(get (erlang-eval-ast \"${SETUP} TombstoneFetch = fun(_) -> {error, should_not_be_called} end, S = peer_actors:store(bob, BobAS, peer_actors:new()), case peer_actors:lookup_or_fetch(bob, TombstoneFetch, S) of {ok, BobAS, S} -> ok; _ -> bad end\") :name)") ;; lookup_or_fetch error from FetchFn does NOT store anything (epoch 18) (eval "(get (erlang-eval-ast \"${SETUP} BadFetch = fun(_) -> {error, http_404} end, case peer_actors:lookup_or_fetch(ghost, BadFetch, peer_actors:new()) of {error, http_404, []} -> ok; _ -> bad end\") :name)") ;; lookup_or_fetch bad return shape is captured (epoch 19) (eval "(get (erlang-eval-ast \"${SETUP} JunkFetch = fun(_) -> garbage end, case peer_actors:lookup_or_fetch(ghost, JunkFetch, peer_actors:new()) of {error, {bad_fetch_return, garbage}, []} -> ok; _ -> bad end\") :name)") ;; gen_server: start_link + lookup_srv miss returns not_found (epoch 20) (eval "(get (erlang-eval-ast \"peer_actors:start_link(), peer_actors:lookup_srv(bob) =:= not_found\") :name)") ;; gen_server: store_srv + lookup_srv round-trip (epoch 21) (eval "(get (erlang-eval-ast \"${SETUP} peer_actors:start_link(), peer_actors:store_srv(bob, BobAS), peer_actors:lookup_srv(bob) =:= {ok, BobAS}\") :name)") ;; gen_server: peers_srv reflects stored entries (epoch 22) (eval "(get (erlang-eval-ast \"${SETUP} peer_actors:start_link(), peer_actors:store_srv(bob, BobAS), peer_actors:store_srv(carol, CarolAS), peer_actors:peers_srv() =:= [bob, carol]\") :name)") ;; gen_server: lookup_or_fetch_srv miss invokes FetchFn + caches (epoch 23) (eval "(get (erlang-eval-ast \"${SETUP} peer_actors:start_link(), R = peer_actors:lookup_or_fetch_srv(bob, OkFetch), R =:= {ok, BobAS} andalso peer_actors:peers_srv() =:= [bob]\") :name)") ;; gen_server: subsequent lookup uses cached value (FetchFn would error) (epoch 24) (eval "(get (erlang-eval-ast \"${SETUP} TombstoneFetch = fun(_) -> {error, should_not_be_called} end, peer_actors:start_link(), peer_actors:store_srv(bob, BobAS), R = peer_actors:lookup_or_fetch_srv(bob, TombstoneFetch), R =:= {ok, BobAS}\") :name)") ;; gen_server: fetch error doesn't poison cache (epoch 25) (eval "(get (erlang-eval-ast \"${SETUP} BadFetch = fun(_) -> {error, http_404} end, peer_actors:start_link(), R = peer_actors:lookup_or_fetch_srv(ghost, BadFetch), R =:= {error, http_404} andalso peer_actors:peers_srv() =:= []\") :name)") ;; gen_server: evict_srv removes the entry (epoch 26) (eval "(get (erlang-eval-ast \"${SETUP} peer_actors:start_link(), peer_actors:store_srv(bob, BobAS), peer_actors:evict_srv(bob), peer_actors:lookup_srv(bob) =:= not_found\") :name)") ;; Initial-state argument: start_link/1 pre-populates the cache (epoch 27) (eval "(get (erlang-eval-ast \"${SETUP} peer_actors:start_link([{bob, BobAS}]), peer_actors:lookup_srv(bob) =:= {ok, BobAS}\") :name)") EPOCHS OUTPUT=$(timeout 240 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) check() { local epoch="$1" desc="$2" expected="$3" local actual actual=$(echo "$OUTPUT" | awk -v e="$epoch" ' $0 ~ "^\\(ok-len " e " " { getline; print; exit } $0 ~ "^\\(ok " e " " { print; exit } $0 ~ "^\\(error " e " " { print; exit } ') [ -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 3 "peer_actors module loaded" "peer_actors" check 10 "new/0 -> []" "true" check 11 "lookup on empty -> not_found" "true" check 12 "store + lookup round-trip" "true" check 13 "peers/1 lists in insertion order" "true" check 14 "evict removes entry" "true" check 15 "evict unknown -> no-op" "true" check 16 "lookup_or_fetch miss fetches" "ok" check 17 "lookup_or_fetch hit skips fetch" "ok" check 18 "fetch error doesn't store" "ok" check 19 "bad fetch return shape captured" "ok" check 20 "gen_server lookup miss" "true" check 21 "gen_server store + lookup" "true" check 22 "gen_server peers_srv lists" "true" check 23 "gen_server fetch + cache" "true" check 24 "gen_server cached skips fetch" "true" check 25 "gen_server fetch error pristine" "true" check 26 "gen_server evict removes" "true" check 27 "start_link/1 pre-populates" "true" TOTAL=$((PASS+FAIL)) if [ $FAIL -eq 0 ]; then echo "ok $PASS/$TOTAL next/tests/peer_actors.sh passed" else echo "FAIL $PASS/$TOTAL passed, $FAIL failed:" echo "$ERRORS" fi [ $FAIL -eq 0 ]