fix: close demo lifecycle races
This commit is contained in:
@@ -106,17 +106,17 @@ forge test --match-test testLegacyParallelActorManifestIsRejected -vv
|
||||
# DemoScript.InvalidActorConfiguration
|
||||
forge test --match-test testAnvilManifestRequiresOwnerAtActorZero -vv
|
||||
|
||||
# DemoScript.UnexpectedState example payload: label, expected, actual
|
||||
cast calldata 'UnexpectedState(string,uint256,uint256)' 'reserves' 1400000000 1399000000
|
||||
# DemoScript.UnexpectedState, including label and expected/actual values
|
||||
forge test --match-test testUnexpectedStateExerciseRevertsThroughAssertionBranch -vv
|
||||
|
||||
# DemoScript.UnexpectedAddress, including expected/actual implementation addresses
|
||||
forge test --match-test testCheckStateRejectsManifestImplementationMismatch -vv
|
||||
|
||||
# CheckState.Insolvent example payload: reserves below ledger liabilities
|
||||
cast calldata 'Insolvent(uint256,uint256)' 1399000000 1400000000
|
||||
# CheckState.Insolvent with reserves below ledger liabilities
|
||||
forge test --match-test testCheckStateExercisesRevertThroughInsolventAndUnknownStageBranches -vv
|
||||
|
||||
# CheckState.UnknownStage example payload
|
||||
cast calldata 'UnknownStage(string)' 'mystery'
|
||||
# CheckState.UnknownStage through the real state checker
|
||||
forge test --match-test testCheckStateExercisesRevertThroughInsolventAndUnknownStageBranches -vv
|
||||
```
|
||||
|
||||
Finish by running `make verify`; it combines unit, fuzz, invariant, script, process-safety, scanner, and web gates.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.35;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
||||
import {BankV1} from "../src/BankV1.sol";
|
||||
import {MockUSDC} from "../src/MockUSDC.sol";
|
||||
import {CheckState} from "../script/CheckState.s.sol";
|
||||
import {DemoScript} from "../script/lib/DemoScript.sol";
|
||||
|
||||
contract NamedErrorHarness is DemoScript {
|
||||
function assertUint(string calldata label, uint256 expected, uint256 actual) external pure {
|
||||
_assertUint(label, expected, actual);
|
||||
}
|
||||
|
||||
function writeManifest(string calldata path, Manifest calldata manifest) external {
|
||||
_writeManifest(path, manifest);
|
||||
}
|
||||
}
|
||||
|
||||
contract NamedErrorExercisesTest is Test {
|
||||
address internal constant OWNER = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
|
||||
address internal constant ALICE = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;
|
||||
address internal constant BOB = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;
|
||||
|
||||
NamedErrorHarness internal harness;
|
||||
|
||||
function setUp() public {
|
||||
vm.chainId(31337);
|
||||
harness = new NamedErrorHarness();
|
||||
}
|
||||
|
||||
function testUnexpectedStateExerciseRevertsThroughAssertionBranch() public {
|
||||
vm.expectRevert(
|
||||
abi.encodeWithSelector(DemoScript.UnexpectedState.selector, "reserves", uint256(1_400e6), uint256(1_399e6))
|
||||
);
|
||||
harness.assertUint("reserves", 1_400e6, 1_399e6);
|
||||
}
|
||||
|
||||
function testCheckStateExercisesRevertThroughInsolventAndUnknownStageBranches() public {
|
||||
(MockUSDC token, BankV1 bank) = _deployFixture("insolvent.json");
|
||||
token.mint(ALICE, 1_400e6);
|
||||
vm.startPrank(ALICE);
|
||||
token.approve(address(bank), 1_400e6);
|
||||
bank.deposit(1_400e6);
|
||||
vm.stopPrank();
|
||||
deal(address(token), address(bank), 1_399e6);
|
||||
vm.setEnv("DEMO_EXPECTED_STAGE", "invariants");
|
||||
CheckState checker = new CheckState();
|
||||
|
||||
vm.expectRevert(abi.encodeWithSelector(CheckState.Insolvent.selector, uint256(1_399e6), uint256(1_400e6)));
|
||||
checker.run();
|
||||
|
||||
_deployFixture("unknown-stage.json");
|
||||
vm.setEnv("DEMO_EXPECTED_STAGE", "mystery");
|
||||
checker = new CheckState();
|
||||
|
||||
vm.expectRevert(abi.encodeWithSelector(CheckState.UnknownStage.selector, "mystery"));
|
||||
checker.run();
|
||||
}
|
||||
|
||||
function _deployFixture(string memory fixtureName) internal returns (MockUSDC token, BankV1 bank) {
|
||||
token = new MockUSDC(address(this));
|
||||
BankV1 implementation = new BankV1();
|
||||
ERC1967Proxy proxy =
|
||||
new ERC1967Proxy(address(implementation), abi.encodeCall(BankV1.initialize, (address(token), OWNER)));
|
||||
bank = BankV1(address(proxy));
|
||||
|
||||
DemoScript.Actor[] memory actors = new DemoScript.Actor[](3);
|
||||
actors[0] = DemoScript.Actor({label: "owner", address_: OWNER});
|
||||
actors[1] = DemoScript.Actor({label: "Alice", address_: ALICE});
|
||||
actors[2] = DemoScript.Actor({label: "Bob", address_: BOB});
|
||||
DemoScript.Manifest memory manifest = DemoScript.Manifest({
|
||||
schemaVersion: 1,
|
||||
network: "anvil",
|
||||
chainId: 31337,
|
||||
deploymentBlock: 1,
|
||||
rpcUrl: "http://127.0.0.1:8545",
|
||||
explorerBaseUrl: "",
|
||||
token: address(token),
|
||||
proxy: address(proxy),
|
||||
implementation: address(implementation),
|
||||
owner: OWNER,
|
||||
actors: actors
|
||||
});
|
||||
string memory fixtureDir = string.concat(vm.projectRoot(), "/deployments/test-named-error-exercises");
|
||||
vm.createDir(fixtureDir, true);
|
||||
string memory manifestPath = string.concat(fixtureDir, "/", fixtureName);
|
||||
harness.writeManifest(manifestPath, manifest);
|
||||
vm.setEnv("DEPLOYMENT_MANIFEST_PATH", manifestPath);
|
||||
}
|
||||
}
|
||||
+19
-3
@@ -8,6 +8,7 @@ ANVIL_DEV_WORDS=(test test test test test test test test test test test junk)
|
||||
CLEANING=0
|
||||
ANVIL_PID='' ANVIL_START='' ANVIL_PGID=''
|
||||
VITE_PID='' VITE_START='' VITE_PGID=''
|
||||
ANVIL_VALIDATED=0 VITE_VALIDATED=0
|
||||
|
||||
cleanup() {
|
||||
local status=$? cleanup_status=0
|
||||
@@ -15,12 +16,14 @@ cleanup() {
|
||||
CLEANING=1
|
||||
trap - INT TERM EXIT
|
||||
if [[ -n "$VITE_PID" && -n "$VITE_START" && -n "$VITE_PGID" ]]; then
|
||||
demo_stop_launch "$ROOT" vite "$VITE_PID" "$VITE_START" "$VITE_PGID" || cleanup_status=1
|
||||
if ((VITE_VALIDATED == 1)); then demo_stop_launch "$ROOT" vite "$VITE_PID" "$VITE_START" "$VITE_PGID" || cleanup_status=1
|
||||
else demo_stop_raw_launch "$ROOT" vite "$VITE_PID" "$VITE_START" "$VITE_PGID" || cleanup_status=1; fi
|
||||
else
|
||||
demo_stop_recorded "$ROOT" vite || cleanup_status=1
|
||||
fi
|
||||
if [[ -n "$ANVIL_PID" && -n "$ANVIL_START" && -n "$ANVIL_PGID" ]]; then
|
||||
demo_stop_launch "$ROOT" anvil "$ANVIL_PID" "$ANVIL_START" "$ANVIL_PGID" || cleanup_status=1
|
||||
if ((ANVIL_VALIDATED == 1)); then demo_stop_launch "$ROOT" anvil "$ANVIL_PID" "$ANVIL_START" "$ANVIL_PGID" || cleanup_status=1
|
||||
else demo_stop_raw_launch "$ROOT" anvil "$ANVIL_PID" "$ANVIL_START" "$ANVIL_PGID" || cleanup_status=1; fi
|
||||
else
|
||||
demo_stop_recorded "$ROOT" anvil || cleanup_status=1
|
||||
fi
|
||||
@@ -34,10 +37,23 @@ trap cleanup EXIT
|
||||
capture_and_publish() {
|
||||
local kind=$1 pid=$2 start pgid
|
||||
for _ in {1..50}; do
|
||||
if demo_capture_process "$ROOT" "$kind" "$pid" start pgid; then
|
||||
if demo_capture_raw_launch "$pid" start pgid; then
|
||||
if [[ "$kind" == anvil ]]; then ANVIL_START=$start; ANVIL_PGID=$pgid
|
||||
else VITE_START=$start; VITE_PGID=$pgid; fi
|
||||
break
|
||||
fi
|
||||
demo_pid_is_running "$pid" || break
|
||||
sleep 0.02
|
||||
done
|
||||
[[ -n ${start:-} && -n ${pgid:-} ]] || {
|
||||
printf 'Could not capture raw ownership for launched %s PID %s; no signal will be guessed.\n' "$kind" "$pid" >&2
|
||||
return 1
|
||||
}
|
||||
for _ in {1..50}; do
|
||||
if demo_identity_matches "$ROOT" "$kind" "$pid" "$start" "$pgid"; then
|
||||
demo_publish_process "$ROOT" "$kind" "$pid" "$start" "$pgid"
|
||||
if [[ "$kind" == anvil ]]; then ANVIL_VALIDATED=1
|
||||
else VITE_VALIDATED=1; fi
|
||||
return
|
||||
fi
|
||||
demo_pid_is_running "$pid" || break
|
||||
|
||||
+49
-29
@@ -89,28 +89,25 @@ demo_interpreter_for_entry() {
|
||||
else demo_resolve_command "${interpreter[0]}"; fi
|
||||
}
|
||||
|
||||
demo_has_sequence_from() {
|
||||
demo_has_exact_tail() {
|
||||
local array_name=$1 begin=$2
|
||||
local -n argv_ref=$array_name
|
||||
shift 2
|
||||
local -a wanted=("$@")
|
||||
local i j
|
||||
for ((i = begin; i + ${#wanted[@]} <= ${#argv_ref[@]}; i++)); do
|
||||
for ((j = 0; j < ${#wanted[@]}; j++)); do
|
||||
[[ ${argv_ref[i+j]} == "${wanted[j]}" ]] || break
|
||||
done
|
||||
((j == ${#wanted[@]})) && return 0
|
||||
local index
|
||||
((${#argv_ref[@]} - begin == ${#wanted[@]})) || return 1
|
||||
for index in "${!wanted[@]}"; do
|
||||
[[ ${argv_ref[begin+index]} == "${wanted[index]}" ]] || return 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
demo_command_matches() {
|
||||
local root=$1 pid=$2 kind=$3 expected executable interpreter entry_index
|
||||
local -a arguments=()
|
||||
[[ -r "/proc/$pid/cmdline" && -e "/proc/$pid/exe" ]] || return 1
|
||||
mapfile -d '' -t arguments <"/proc/$pid/cmdline"
|
||||
{ mapfile -d '' -t arguments <"/proc/$pid/cmdline"; } 2>/dev/null || return 1
|
||||
((${#arguments[@]} > 0)) || return 1
|
||||
executable=$(readlink -f -- "/proc/$pid/exe") || return 1
|
||||
executable=$(readlink -f -- "/proc/$pid/exe" 2>/dev/null) || return 1
|
||||
|
||||
if [[ "$kind" == anvil ]]; then
|
||||
expected=$(demo_resolve_command anvil) || return 1
|
||||
@@ -128,13 +125,11 @@ demo_command_matches() {
|
||||
fi
|
||||
|
||||
if [[ "$kind" == anvil ]]; then
|
||||
demo_has_sequence_from arguments "$((entry_index + 1))" --host 127.0.0.1 || return 1
|
||||
demo_has_sequence_from arguments "$((entry_index + 1))" --port 8545 || return 1
|
||||
demo_has_sequence_from arguments "$((entry_index + 1))" --chain-id 31337 || return 1
|
||||
local dev_phrase='test test test test test test test test test test test junk'
|
||||
demo_has_exact_tail arguments "$((entry_index + 1))" \
|
||||
--host 127.0.0.1 --port 8545 --chain-id 31337 --mnemonic "$dev_phrase"
|
||||
else
|
||||
local -a expected_tail=(web --host 127.0.0.1 --port 5173)
|
||||
local -a actual_tail=("${arguments[@]:entry_index+1}")
|
||||
[[ "${actual_tail[*]}" == "${expected_tail[*]}" && ${#actual_tail[@]} -eq ${#expected_tail[@]} ]] || return 1
|
||||
demo_has_exact_tail arguments "$((entry_index + 1))" web --host 127.0.0.1 --port 5173
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -146,10 +141,17 @@ demo_identity_matches() {
|
||||
demo_command_matches "$root" "$pid" "$kind"
|
||||
}
|
||||
|
||||
demo_capture_raw_launch() {
|
||||
local pid=$1 start_name=$2 pgid_name=$3 state raw_actual_start raw_actual_pgid sid
|
||||
demo_read_process_identity "$pid" state raw_actual_start raw_actual_pgid sid || return 1
|
||||
[[ "$state" != Z && "$pid" == "$raw_actual_pgid" && "$pid" == "$sid" ]] || return 1
|
||||
printf -v "$start_name" '%s' "$raw_actual_start"
|
||||
printf -v "$pgid_name" '%s' "$raw_actual_pgid"
|
||||
}
|
||||
|
||||
demo_capture_process() {
|
||||
local root=$1 kind=$2 pid=$3 start_name=$4 pgid_name=$5 state actual_start actual_pgid sid
|
||||
demo_read_process_identity "$pid" state actual_start actual_pgid sid || return 1
|
||||
[[ "$state" != Z && "$pid" == "$actual_pgid" && "$pid" == "$sid" ]] || return 1
|
||||
local root=$1 kind=$2 pid=$3 start_name=$4 pgid_name=$5 actual_start actual_pgid
|
||||
demo_capture_raw_launch "$pid" actual_start actual_pgid || return 1
|
||||
demo_command_matches "$root" "$pid" "$kind" || return 1
|
||||
printf -v "$start_name" '%s' "$actual_start"
|
||||
printf -v "$pgid_name" '%s' "$actual_pgid"
|
||||
@@ -201,20 +203,29 @@ demo_collect_group_members() {
|
||||
}
|
||||
|
||||
demo_anchor_allows_signal() {
|
||||
local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 state start pgid sid
|
||||
local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 identity_mode=${6:-validated} state start pgid sid
|
||||
if ! demo_read_process_identity "$leader" state start pgid sid; then return 0; fi
|
||||
[[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_pgid" ]] || return 1
|
||||
[[ "$state" == Z ]] && return 0
|
||||
[[ "$identity_mode" == raw ]] && return 0
|
||||
demo_process_has_command_line "$leader" || return 0
|
||||
demo_command_matches "$root" "$leader" "$kind"
|
||||
if demo_command_matches "$root" "$leader" "$kind"; then return 0; fi
|
||||
if ! demo_read_process_identity "$leader" state start pgid sid; then return 0; fi
|
||||
[[ "$state" == Z ]] && return 0
|
||||
[[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_pgid" ]] || return 1
|
||||
return 1
|
||||
}
|
||||
|
||||
demo_signal_member() {
|
||||
local pid=$1 expected_start=$2 expected_pgid=$3 expected_sid=$4 signal=$5 state start pgid sid
|
||||
local pid=$1 expected_start=$2 expected_pgid=$3 expected_sid=$4 signal=$5 state start pgid sid signal_status
|
||||
if ! demo_read_process_identity "$pid" state start pgid sid; then return 0; fi
|
||||
[[ "$state" != Z ]] || return 0
|
||||
[[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" ]] || return 2
|
||||
kill -"$signal" -- "$pid"
|
||||
if kill -"$signal" -- "$pid"; then return 0; else signal_status=$?; fi
|
||||
if ! demo_read_process_identity "$pid" state start pgid sid; then return 0; fi
|
||||
[[ "$state" != Z ]] || return 0
|
||||
[[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" ]] || return 0
|
||||
return "$signal_status"
|
||||
}
|
||||
|
||||
demo_group_has_live_members() {
|
||||
@@ -224,12 +235,13 @@ demo_group_has_live_members() {
|
||||
((${#members[@]} > 0))
|
||||
}
|
||||
|
||||
demo_stop_launch() {
|
||||
local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 member member_pid member_start result
|
||||
demo_stop_owned_session() {
|
||||
local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 identity_mode=$6 member member_pid member_start result
|
||||
local -a members
|
||||
[[ "$identity_mode" == validated || "$identity_mode" == raw ]] || return 1
|
||||
demo_is_process_id "$leader" && demo_is_uint "$expected_start" && demo_is_process_id "$expected_pgid" \
|
||||
&& [[ "$leader" == "$expected_pgid" ]] || return 1
|
||||
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || {
|
||||
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" "$identity_mode" || {
|
||||
printf 'Refusing %s PID %s: process identity changed\n' "$kind" "$leader" >&2
|
||||
return 1
|
||||
}
|
||||
@@ -243,13 +255,13 @@ demo_stop_launch() {
|
||||
for member in "${members[@]}"; do
|
||||
IFS=: read -r member_pid member_start <<<"$member"
|
||||
[[ "$member_pid" != "$leader" ]] || continue
|
||||
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || return 1
|
||||
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" "$identity_mode" || return 1
|
||||
demo_signal_member "$member_pid" "$member_start" "$expected_pgid" "$expected_pgid" TERM || return 1
|
||||
done
|
||||
for member in "${members[@]}"; do
|
||||
IFS=: read -r member_pid member_start <<<"$member"
|
||||
[[ "$member_pid" == "$leader" ]] || continue
|
||||
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || return 1
|
||||
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" "$identity_mode" || return 1
|
||||
demo_signal_member "$member_pid" "$member_start" "$expected_pgid" "$expected_pgid" TERM || return 1
|
||||
done
|
||||
for ((result = 0; result < ${DEMO_TERM_WAIT_ATTEMPTS:-50}; result++)); do
|
||||
@@ -264,7 +276,7 @@ demo_stop_launch() {
|
||||
((${#members[@]} > 0)) || break
|
||||
for member in "${members[@]}"; do
|
||||
IFS=: read -r member_pid member_start <<<"$member"
|
||||
if ! demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid"; then
|
||||
if ! demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" "$identity_mode"; then
|
||||
printf 'Refusing KILL escalation: %s leader identity changed\n' "$kind" >&2
|
||||
return 1
|
||||
fi
|
||||
@@ -287,6 +299,14 @@ demo_stop_launch() {
|
||||
demo_remove_record "$root" "$kind"
|
||||
}
|
||||
|
||||
demo_stop_launch() {
|
||||
demo_stop_owned_session "$1" "$2" "$3" "$4" "$5" validated
|
||||
}
|
||||
|
||||
demo_stop_raw_launch() {
|
||||
demo_stop_owned_session "$1" "$2" "$3" "$4" "$5" raw
|
||||
}
|
||||
|
||||
demo_stop_recorded() {
|
||||
local root=$1 kind=$2 pid start pgid
|
||||
local pid_path="$root/.demo/$kind.pid" start_path="$root/.demo/$kind.start" pgid_path="$root/.demo/$kind.pgid"
|
||||
|
||||
@@ -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