From fd62dff3fa32c3a56ec16bce48013ab200312361 Mon Sep 17 00:00:00 2001 From: broken-circle <252359939+broken-circle@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:55:40 -0700 Subject: [PATCH] Make `testTeardownSequence()` resilient to scheduling latency The test drove a `teardown(using:)` sequence (`SIGQUIT`, `SIGTERM`, then `SIGINT`, each with a grace period, ending in an implicit `SIGKILL`) against a bash child whose `INT` trap echoes and `exit 42`s, and which otherwise looped in a foreground `sleep 0.1`. On a loaded Linux runner it failed with `signaled(9)` and a transcript missing the final `saw SIGINT`. Bash defers a trapped signal until the running foreground command completes. When scheduling pressure delayed the deferred `INT` trap past the grace period, the implicit `SIGKILL` reached the child before its deferred `INT` trap ran, so it died on the signal instead of exiting `42`. Only the `SIGINT` step gates anything; the `QUIT` and `TERM` traps just echo, so those steps always run their full duration regardless of timing, which is why the sequence got that far and no further. Idle in `wait` on a backgrounded `sleep` instead. A trapped signal interrupts `wait` immediately and runs the handler at once, with no dependence on a sleep interval elapsing, so the `INT` trap fires well inside the grace period even under load. The backgrounded `sleep` is short so a signal that lands as bash enters the `wait` is serviced at the next safe point within one interval, rather than blocking on a long-lived child. --- Tests/SubprocessTests/UnixTests.swift | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Tests/SubprocessTests/UnixTests.swift b/Tests/SubprocessTests/UnixTests.swift index 184ec5b1..7579e046 100644 --- a/Tests/SubprocessTests/UnixTests.swift +++ b/Tests/SubprocessTests/UnixTests.swift @@ -189,13 +189,20 @@ extension SubprocessUnixTests { arguments: [ "-c", """ - set -e - trap 'echo saw SIGQUIT;' QUIT - trap 'echo saw SIGTERM;' TERM - trap 'echo saw SIGINT; exit 42;' INT + trap 'echo saw SIGQUIT' QUIT + trap 'echo saw SIGTERM' TERM + trap 'echo saw SIGINT; exit 42' INT echo ready - while true; do sleep 0.1; done - exit 2 + # A trapped signal interrupts `wait` immediately, so the handler runs + # without waiting for a sleep interval to elapse, unlike a foreground + # `sleep`, whose completion (and the trap deferred behind it) can slip + # past the teardown window under load. The backgrounded sleep is short + # so a signal landing as bash enters the wait is still serviced within + # one interval rather than stranding on a long-lived child. + while true; do + sleep 0.2 & + wait $! + done """, ], input: .none, @@ -205,8 +212,8 @@ extension SubprocessUnixTests { return try await withThrowingTaskGroup(of: Void.self) { group in // Gate the teardown task on bash having actually installed // its signal traps. The reader signals readiness when it - // sees the `ready` marker the script prints after the - // `trap` lines. + // sees the `ready` marker the script prints once its traps are + // installed, just before it begins waiting. let (readyStream, readyContinuation) = AsyncStream.makeStream(of: Void.self) group.addTask {