fix: close demo lifecycle races
This commit is contained in:
@@ -8,6 +8,8 @@ TEST_ROOT=$(mktemp -d /tmp/uups-bank-process-test.XXXXXX)
|
||||
PATH="$TEST_ROOT/bin:$PATH"
|
||||
export DEMO_TERM_WAIT_ATTEMPTS=5
|
||||
export DEMO_KILL_WAIT_ATTEMPTS=10
|
||||
ANVIL_DEV_PHRASE='test test test test test test test test test test test junk'
|
||||
ANVIL_EXACT_ARGS=(--host 127.0.0.1 --port 8545 --chain-id 31337 --mnemonic "$ANVIL_DEV_PHRASE")
|
||||
declare -a TEST_IDENTITIES=()
|
||||
PASSED=0
|
||||
STARTED_PID=
|
||||
@@ -15,7 +17,7 @@ STARTED_PID=
|
||||
cleanup() {
|
||||
local identity root relative
|
||||
for identity in "${TEST_IDENTITIES[@]}"; do test_safe_stop "$identity"; done
|
||||
for root in absent stale nonnumeric wrong-command false-anvil false-vite reused atomic matching group mutated partial local-reset base-canonical base-active sentinel; do
|
||||
for root in absent stale nonnumeric wrong-command false-anvil false-vite exact-anvil raw-capture reused atomic matching group mutated partial local-reset base-canonical base-active sentinel; do
|
||||
for relative in \
|
||||
.demo/anvil.pid .demo/anvil.start .demo/anvil.pgid .demo/anvil.log \
|
||||
.demo/vite.pid .demo/vite.start .demo/vite.pgid .demo/vite.log \
|
||||
@@ -29,7 +31,8 @@ cleanup() {
|
||||
"$TEST_ROOT/$root/web" "$TEST_ROOT/$root/deployments" "$TEST_ROOT/$root/tools" "$TEST_ROOT/$root/.demo" \
|
||||
"$TEST_ROOT/$root" 2>/dev/null || true
|
||||
done
|
||||
rm -f -- "$TEST_ROOT/bin/anvil" "$TEST_ROOT/bin/vite" "$TEST_ROOT/vite-child.pid"
|
||||
rm -f -- "$TEST_ROOT/bin/anvil" "$TEST_ROOT/bin/vite" "$TEST_ROOT/bin/leaderless" \
|
||||
"$TEST_ROOT/vite-child.pid" "$TEST_ROOT/leaderless-child.pid"
|
||||
rmdir -- "$TEST_ROOT/bin" "$TEST_ROOT" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
@@ -75,15 +78,17 @@ test_safe_stop() {
|
||||
local identity=$1 leader expected_start expected_pgid expected_sid state start pgid sid signal line member member_start
|
||||
local current_state current_start current_pgid current_sid
|
||||
IFS=: read -r leader expected_start expected_pgid expected_sid <<<"$identity"
|
||||
if test_process_identity "$leader" state start pgid sid && [[ "$start" != "$expected_start" ]]; then return 0; fi
|
||||
if test_process_identity "$leader" state start pgid sid \
|
||||
&& [[ "$start" != "$expected_start" || "$pgid" != "$expected_pgid" || "$sid" != "$expected_sid" ]]; then return 0; fi
|
||||
for signal in TERM KILL; do
|
||||
while read -r member pgid sid state; do
|
||||
[[ "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" && "$state" != Z ]] || continue
|
||||
test_process_identity "$member" state member_start pgid sid || continue
|
||||
[[ "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" ]] || continue
|
||||
test_process_identity "$leader" current_state current_start current_pgid current_sid || continue
|
||||
[[ "$current_start" == "$expected_start" && "$current_pgid" == "$expected_pgid" \
|
||||
&& "$current_sid" == "$expected_sid" ]] || continue
|
||||
if test_process_identity "$leader" current_state current_start current_pgid current_sid; then
|
||||
[[ "$current_start" == "$expected_start" && "$current_pgid" == "$expected_pgid" \
|
||||
&& "$current_sid" == "$expected_sid" ]] || continue
|
||||
fi
|
||||
test_process_identity "$member" current_state current_start current_pgid current_sid || continue
|
||||
[[ "$current_state" != Z && "$current_start" == "$member_start" \
|
||||
&& "$current_pgid" == "$expected_pgid" && "$current_sid" == "$expected_sid" ]] || continue
|
||||
@@ -138,7 +143,7 @@ wait
|
||||
HELPER
|
||||
chmod +x "$helper"
|
||||
if [[ "$kind" == anvil ]]; then
|
||||
setsid "$helper" --host 127.0.0.1 --port 8545 --chain-id 31337 >/dev/null 2>&1 &
|
||||
setsid "$helper" "${ANVIL_EXACT_ARGS[@]}" >/dev/null 2>&1 &
|
||||
else
|
||||
setsid "$helper" web --host 127.0.0.1 --port 5173 >/dev/null 2>&1 &
|
||||
fi
|
||||
@@ -152,7 +157,17 @@ HELPER
|
||||
STARTED_PID=$pid
|
||||
}
|
||||
|
||||
printf '1..16\n'
|
||||
assert_anvil_args_rejected() {
|
||||
local root=$1 label=$2 state start pgid sid pid
|
||||
shift 2
|
||||
setsid "$TEST_ROOT/bin/anvil" "$@" & pid=$!
|
||||
track_process "$pid"
|
||||
test_process_identity "$pid" state start pgid sid
|
||||
if demo_command_matches "$root" "$pid" anvil; then fail "$label Anvil arguments were accepted"; fi
|
||||
demo_stop_raw_launch "$root" anvil "$pid" "$start" "$pgid"
|
||||
}
|
||||
|
||||
printf '1..21\n'
|
||||
|
||||
# Catches cleanup treating a missing record as an error or signaling an inferred PID.
|
||||
root=$(make_root absent)
|
||||
@@ -205,6 +220,81 @@ if demo_stop_recorded "$root" vite 2>/dev/null; then fail 'argument-only Vite si
|
||||
assert_alive "$false_vite_pid"
|
||||
pass 'Vite identity requires the exact project-local entry and interpreter'
|
||||
|
||||
# Catches accepting extra or overriding Anvil arguments after a valid-looking prefix.
|
||||
root=$(make_root exact-anvil)
|
||||
mkdir -p "$TEST_ROOT/bin"
|
||||
cat >"$TEST_ROOT/bin/anvil" <<'HELPER'
|
||||
#!/usr/bin/env bash
|
||||
sleep 120 &
|
||||
wait
|
||||
HELPER
|
||||
chmod +x "$TEST_ROOT/bin/anvil"
|
||||
assert_anvil_args_rejected "$root" extra "${ANVIL_EXACT_ARGS[@]}" --silent
|
||||
assert_anvil_args_rejected "$root" duplicate "${ANVIL_EXACT_ARGS[@]}" --port 9999
|
||||
assert_anvil_args_rejected "$root" reordered \
|
||||
--port 8545 --host 127.0.0.1 --chain-id 31337 --mnemonic "$ANVIL_DEV_PHRASE"
|
||||
pass 'Anvil identity requires the exact complete launch argument tail'
|
||||
|
||||
# Catches treating an ESRCH-like signal race as fatal after the captured member has exited.
|
||||
setsid /bin/bash -c 'trap "exit 0" TERM; while :; do sleep 0.01; done' & vanished_pid=$!
|
||||
track_process "$vanished_pid"
|
||||
vanished_state='' vanished_start='' vanished_pgid='' vanished_sid=''
|
||||
test_process_identity "$vanished_pid" vanished_state vanished_start vanished_pgid vanished_sid
|
||||
[[ "$vanished_state" != Z ]] || fail 'vanishing race fixture exited before injection'
|
||||
kill() {
|
||||
builtin kill -TERM -- "$vanished_pid" 2>/dev/null || true
|
||||
for _ in {1..50}; do
|
||||
vanished_state_after=''
|
||||
if ! test_process_identity "$vanished_pid" vanished_state_after vanished_start_after vanished_pgid_after vanished_sid_after \
|
||||
|| [[ "$vanished_state_after" == Z ]]; then break; fi
|
||||
sleep 0.01
|
||||
done
|
||||
return 1
|
||||
}
|
||||
if demo_signal_member "$vanished_pid" "$vanished_start" "$vanished_pgid" "$vanished_sid" TERM; then
|
||||
vanished_result=0
|
||||
else
|
||||
vanished_result=$?
|
||||
fi
|
||||
unset -f kill
|
||||
wait "$vanished_pid" 2>/dev/null || true
|
||||
((vanished_result == 0)) || fail 'a vanished member made its raced signal fatal'
|
||||
pass 'a failed signal is idempotent after the captured member vanishes'
|
||||
|
||||
# Catches swallowing a real signal failure while the exact captured member remains live.
|
||||
setsid /bin/sleep 120 & unsignaled_pid=$!
|
||||
track_process "$unsignaled_pid"
|
||||
unsignaled_state='' unsignaled_start='' unsignaled_pgid='' unsignaled_sid=''
|
||||
test_process_identity "$unsignaled_pid" unsignaled_state unsignaled_start unsignaled_pgid unsignaled_sid
|
||||
[[ "$unsignaled_state" != Z ]] || fail 'unchanged signal-failure fixture exited early'
|
||||
kill() { return 1; }
|
||||
if demo_signal_member "$unsignaled_pid" "$unsignaled_start" "$unsignaled_pgid" "$unsignaled_sid" TERM; then
|
||||
unsignaled_result=0
|
||||
else
|
||||
unsignaled_result=$?
|
||||
fi
|
||||
unset -f kill
|
||||
((unsignaled_result != 0)) || fail 'a failed signal to the unchanged live member was accepted'
|
||||
assert_alive "$unsignaled_pid"
|
||||
pass 'a failed signal remains fatal for the same live member tuple'
|
||||
|
||||
# Catches command validation failure occurring before launch ownership is retained for cleanup.
|
||||
root=$(make_root raw-capture)
|
||||
setsid /bin/bash -c 'sleep 120 & wait' unexpected-entry --not-the-demo & raw_pid=$!
|
||||
track_process "$raw_pid"
|
||||
raw_start='' raw_pgid=''
|
||||
declare -F demo_capture_raw_launch >/dev/null || fail 'missing raw launch identity capture'
|
||||
declare -F demo_stop_raw_launch >/dev/null || fail 'missing command-independent raw launch cleanup'
|
||||
for _ in {1..50}; do
|
||||
if demo_capture_raw_launch "$raw_pid" raw_start raw_pgid; then break; fi
|
||||
sleep 0.02
|
||||
done
|
||||
[[ -n ${raw_start:-} && "$raw_pgid" == "$raw_pid" ]] || fail 'raw launch identity was not captured'
|
||||
if demo_command_matches "$root" "$raw_pid" anvil; then fail 'raw test command unexpectedly passed Anvil validation'; fi
|
||||
demo_stop_raw_launch "$root" anvil "$raw_pid" "$raw_start" "$raw_pgid"
|
||||
assert_dead "$raw_pid"
|
||||
pass 'raw launch ownership safely cleans a command-validation failure'
|
||||
|
||||
# Catches recycled PID ownership being inferred from PID and command alone.
|
||||
root=$(make_root reused)
|
||||
start_owned "$root" anvil
|
||||
@@ -267,7 +357,7 @@ trap 'exec -a changed-after-term /bin/sleep 120' TERM
|
||||
while :; do sleep 0.05; done
|
||||
HELPER
|
||||
chmod +x "$mutating_helper"
|
||||
setsid "$mutating_helper" --host 127.0.0.1 --port 8545 --chain-id 31337 & mutated_pid=$!
|
||||
setsid "$mutating_helper" "${ANVIL_EXACT_ARGS[@]}" & mutated_pid=$!
|
||||
track_process "$mutated_pid"
|
||||
write_record "$root" anvil "$mutated_pid" "$(start_tick "$mutated_pid")" "$mutated_pid"
|
||||
if demo_stop_recorded "$root" anvil 2>/dev/null; then fail 'changed identity was escalated instead of rejected'; fi
|
||||
@@ -288,6 +378,36 @@ assert_absent "$root/.demo/anvil.start"
|
||||
assert_absent "$root/.demo/anvil.pgid"
|
||||
pass 'in-memory launch identity safely cleans a partial unpublished record'
|
||||
|
||||
# Catches test cleanup leaking a TERM-resistant session member after its original leader is reaped.
|
||||
leaderless_helper="$TEST_ROOT/bin/leaderless"
|
||||
leaderless_child_file="$TEST_ROOT/leaderless-child.pid"
|
||||
cat >"$leaderless_helper" <<'HELPER'
|
||||
#!/usr/bin/env bash
|
||||
bash -c 'trap "" TERM; exec sleep 120' &
|
||||
printf '%s\n' "$!" >"$DEMO_CHILD_PID_FILE"
|
||||
sleep 1
|
||||
HELPER
|
||||
chmod +x "$leaderless_helper"
|
||||
DEMO_CHILD_PID_FILE="$leaderless_child_file" setsid "$leaderless_helper" & leaderless_pid=$!
|
||||
track_process "$leaderless_pid"
|
||||
leaderless_identity=${TEST_IDENTITIES[${#TEST_IDENTITIES[@]}-1]}
|
||||
for _ in {1..50}; do [[ -s "$leaderless_child_file" ]] && break; sleep 0.02; done
|
||||
[[ -s "$leaderless_child_file" ]] || fail 'leaderless cleanup child did not start'
|
||||
leaderless_child=$(<"$leaderless_child_file")
|
||||
child_state='' child_start='' child_pgid='' child_sid=''
|
||||
test_process_identity "$leaderless_child" child_state child_start child_pgid child_sid
|
||||
[[ "$child_state" != Z ]] || fail 'leaderless cleanup child exited early'
|
||||
wait "$leaderless_pid"
|
||||
test_safe_stop "$leaderless_identity"
|
||||
if test_process_identity "$leaderless_child" current_state current_start current_pgid current_sid \
|
||||
&& [[ "$current_state" != Z ]]; then
|
||||
if [[ "$current_start" == "$child_start" && "$current_pgid" == "$child_pgid" && "$current_sid" == "$child_sid" ]]; then
|
||||
builtin kill -KILL -- "$leaderless_child" 2>/dev/null || true
|
||||
fi
|
||||
fail 'test cleanup leaked a session member after the leader was reaped'
|
||||
fi
|
||||
pass 'test cleanup safely stops surviving members after leader reaping'
|
||||
|
||||
# Catches reset deleting arbitrary neighbors or leaving known reproducible local artifacts.
|
||||
root=$(make_root local-reset)
|
||||
for kind in anvil vite; do printf 'log\n' >"$root/.demo/$kind.log"; printf '1\n' >"$root/.demo/$kind.start"; printf '1\n' >"$root/.demo/$kind.pgid"; done
|
||||
|
||||
Reference in New Issue
Block a user