diff --git a/tests/test-hup-parser.sh b/tests/test-hup-parser.sh new file mode 100644 index 0000000..26b8f43 --- /dev/null +++ b/tests/test-hup-parser.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail +# Inert parser controls only; no production code, network or real credentials. +unset GH_TOKEN GITHUB_TOKEN SSH_PRIVATE_KEY_B64 +root=$(mktemp -d) +cleanup() { rm -rf -- "$root"; } +trap cleanup EXIT +command -v timeout >/dev/null +printf "DIAG: parser controls bash=%s\n" "$BASH_VERSION" +cat > "$root/child.sh" <<\CHILD +#!/usr/bin/env bash +set -euo pipefail +handler() { : > "$ready.handled"; exit 129; } +mode=$1 +ready=$2 +trap handler HUP +: > "$ready" +end=$((SECONDS + 2)) +case "$mode" in + plain) while (( SECONDS < end )); do :; done ;; + command) while (( SECONDS < end )); do value=$(printf x); [[ $value == x ]]; done ;; + snapshot) while (( SECONDS < end )); do active_jobs=$(jobs -pr); while IFS= read -r value; do :; done <<< "$active_jobs"; done ;; + process) while (( SECONDS < end )); do while IFS= read -r value; do :; done < <(jobs -pr); done ;; + *) exit 90 ;; +esac +# Reaching this point means no HUP was handled; never count it as success. +exit 91 +CHILD +cat > "$root/controller.sh" <<\CONTROL +#!/usr/bin/env bash +set -euo pipefail +root=$1 mode=$2 round=$3 delay=$4 +ready="$root/ready-$mode-$round" +# Signal only the short-lived direct fixture child after its readiness marker. +env --default-signal=HUP,INT,TERM bash "$root/child.sh" "$mode" "$ready" & +child=$! +end=$((SECONDS + 2)) +while [[ ! -f $ready ]] && (( SECONDS < end )); do sleep 0.001; done +if [[ ! -f $ready ]]; then + wait "$child" || true + exit 92 +fi +sleep "$delay" +kill -HUP "$child" +status=0 +wait "$child" || status=$? +printf "DIAG: mode=%s round=%s delay=%s exit=%s\n" "$mode" "$round" "$delay" "$status" +[[ $status == 129 && -f $ready.handled ]] +CONTROL +# 3 modes x 6 cases, at most 5 seconds per case (including forced teardown). +# Do not retry failures. Delay varies delivery, not a claim of exact parser timing. +# Legacy process mode already reproduced EOF in run 34226553908. +# Keep it available explicitly; the default gate validates the replacement. +modes="plain command snapshot" +if [[ ${HUP_PARSER_INCLUDE_LEGACY:-false} == true ]]; then modes="$modes process"; fi +for mode in $modes; do + round=0 + for delay in 0 0.001 0.005 0.01 0.02 0.05; do + round=$((round + 1)) + timeout --signal=TERM --kill-after=1s 4s bash "$root/controller.sh" "$root" "$mode" "$round" "$delay" + done +done +printf "PASS: all selected bounded HUP parser controls\n" diff --git a/tests/test-push-worker-registration.sh b/tests/test-push-worker-registration.sh index db007b5..ee5bb3b 100755 --- a/tests/test-push-worker-registration.sh +++ b/tests/test-push-worker-registration.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash set -euo pipefail +# Safe runtime identity only: no environment dump, credentials, or xtrace. +printf "DIAG: bash=%s kernel=%s\n" "$BASH_VERSION" "$(uname -r)" +dpkg-query -W bash libc6 2>/dev/null || true ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) readonly ROOT_DIR @@ -82,6 +85,19 @@ printf source-safe assert_eq source-safe "$source_result" "source keeps zero runtime side effects" # shellcheck source=../tools/push.sh source "$SCRIPT" +( + # Inert direct-child job table: no SSH or production services. + sleep 10 & snapshot_child=$! + cleanup_snapshot_child() { kill -TERM "$snapshot_child" 2>/dev/null || true; wait "$snapshot_child" 2>/dev/null || true; } + trap cleanup_snapshot_child EXIT + job_is_active "$snapshot_child" || fail "snapshot misses active direct child" + if job_is_active 0; then fail "snapshot accepts absent PID"; fi + kill -TERM "$snapshot_child" + wait "$snapshot_child" 2>/dev/null || true + if job_is_active "$snapshot_child"; then fail "snapshot retains reaped child"; fi + trap - EXIT + pass "job snapshot preserves direct-child matching and reap semantics" +) trap 'rm -rf "$TEST_DIR"' EXIT setup_fixture() { @@ -1435,7 +1451,7 @@ run_state_publication_signal_case() ( } trap cleanup_publication_fixture EXIT mode=active; [[ "$state" == cleanup_failed ]] && mode=cleanup - root="$TEST_DIR/state-publish-$state-$phase-$signal_name"; CURRENT_FIXTURE_ROOT=$root + root="$TEST_DIR/state-publish-$state-$phase-$signal_name${5:-}"; CURRENT_FIXTURE_ROOT=$root write_active_grace_fixture "$root" mkdir -m 0700 "$root/capture" marker="$root/capture/publish-marker" @@ -1453,6 +1469,8 @@ run_state_publication_signal_case() ( [[ "$worker_pid" =~ ^[1-9][0-9]*$ && "$worker_start" =~ ^[1-9][0-9]*$ && "$managed_sid" =~ ^[1-9][0-9]*$ ]] || fail "$state/$phase/$signal_name marker identity malformed" wait_test_process_identity_present "$worker_pid" "$worker_start" || { cat "$marker" >&2; fail "$state/$phase/$signal_name worker identity missing at hook"; } test_watchdog_process 30 "$root/watchdog-timeout" "$main_pid" & watchdog=$!; watchdog_start=$(wait_test_process_start "$watchdog") + printf "DIAG: publication state=%s phase=%s signal=%s main=%s/%s worker=%s/%s sid=%s\n" \ + "$state" "$phase" "$signal_name" "$main_pid" "$main_start" "$worker_pid" "$worker_start" "$managed_sid" kill "-$signal_name" "$main_pid"; wait "$main_pid" || rc=$? kill -TERM "$watchdog" 2>/dev/null || true; wait "$watchdog" 2>/dev/null || true [[ ! -e "$root/watchdog-timeout" ]] || fail "$state/$phase/$signal_name watchdog fired" @@ -1490,6 +1508,11 @@ for publication_state in $publication_states; do done ( + # Three bounded extra probes; preserve original matrix and stop at first failure. + for diagnostic_round in 1 2 3; do + printf "DIAG: extra HUP probe=%s/3\n" "$diagnostic_round" + run_state_publication_signal_case cleanup_failed after-rename HUP 129 "-probe-$diagnostic_round" + done root="$TEST_DIR/normal-parallel"; setup_fixture "$root" : > "$root/capture/current"; : > "$root/capture/max" push_to_server() { diff --git a/tools/push.sh b/tools/push.sh index 3a4ac17..b578441 100755 --- a/tools/push.sh +++ b/tools/push.sh @@ -941,10 +941,13 @@ process_identity_matches() { } job_is_active() { - local expected="$1" pid + local expected="$1" pid active_jobs + # Bash 5.2 can report a trap parser EOF when HUP interrupts process substitution. + # Snapshot the inherited job table without process substitution or word splitting. + active_jobs=$(jobs -pr) || return 1 while IFS= read -r pid; do [[ "$pid" == "$expected" ]] && return 0 - done < <(jobs -pr) + done <<< "$active_jobs" return 1 }