test(compile): readiness-gate the survivor fixtures — macOS CI race
macOS/luajit failed shutdown_force_kills_outstanding_ledger_groups with "survivor alive pre-shutdown": on a slow scheduler the leader (`( trap '' TERM; ... ) & echo $! > pidfile`) can exit before the backgrounded subshell installs its trap, so the leader-exit group-TERM kills the "survivor". Linux wins that race consistently; macOS runners don't. The same race made three sibling tests vacuously green when it fired (a dead survivor trivially satisfies "survivor dies" and trivially bounds the drain). Fix: a shared fixture (survivor_script / survivor_cmdline) writes a readiness file immediately after `trap` and the leader busy-waits on it before exiting — the trap is provably installed before any group-TERM can be sent. Applied to the three process.rs unit fixtures and the acc08/acc09 acceptance twins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
This commit is contained in:
parent
50abf91c15
commit
6902959b09
|
|
@ -2749,6 +2749,33 @@ mod tests {
|
|||
.is_ok_and(|o| o.status.success())
|
||||
}
|
||||
|
||||
/// Fixture: background a TERM-ignoring survivor and let the
|
||||
/// leader exit only after the survivor's trap is INSTALLED
|
||||
/// (readiness file). Without the gate, a slow scheduler (macOS
|
||||
/// CI, observed) can deliver the leader-exit group-TERM before
|
||||
/// the subshell's `trap` runs — killing the "survivor": flaky
|
||||
/// red for tests that need it alive, vacuous green for tests
|
||||
/// that assert its death. `redirect` sheds the survivor's
|
||||
/// stdout/stderr (the acceptance-8 shape); without it the
|
||||
/// survivor keeps fd1 (the acceptance-9 shape). Returns
|
||||
/// (script, pidfile).
|
||||
fn survivor_script(dir: &std::path::Path, redirect: bool) -> (String, std::path::PathBuf) {
|
||||
let pidfile = dir.join("pid");
|
||||
let ready = dir.join("ready");
|
||||
let redirect_part = if redirect {
|
||||
"exec >/dev/null 2>&1; "
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let script = format!(
|
||||
"( trap '' TERM; : > {ready}; {redirect_part}sleep 30 ) & echo $! > {pid}; \
|
||||
while [ ! -e {ready} ]; do sleep 0.01; done",
|
||||
ready = ready.display(),
|
||||
pid = pidfile.display(),
|
||||
);
|
||||
(script, pidfile)
|
||||
}
|
||||
|
||||
/// Poll `path` until it holds a parseable pid. Fixture scripts
|
||||
/// write descendant pids there.
|
||||
fn wait_pidfile(path: &std::path::Path) -> i32 {
|
||||
|
|
@ -2859,13 +2886,9 @@ mod tests {
|
|||
// terminal event arrives and the readers finish — only the
|
||||
// ledger's kill(-pgid, 0) probe can catch it.
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let pidfile = dir.path().join("pid");
|
||||
let mut sup = ProcessSupervisor::new();
|
||||
sup.set_group_term_grace(Duration::from_millis(200));
|
||||
let script = format!(
|
||||
"( trap '' TERM; exec >/dev/null 2>&1; sleep 30 ) & echo $! > {}",
|
||||
pidfile.display()
|
||||
);
|
||||
let (script, pidfile) = survivor_script(dir.path(), true);
|
||||
let id = sup
|
||||
.spawn(sh_group_spec("survivor", &script))
|
||||
.expect("spawn");
|
||||
|
|
@ -2938,13 +2961,9 @@ mod tests {
|
|||
// grace is long enough that the ledger cannot fire on its
|
||||
// own — only shutdown's force-kill can reap the survivor.
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let pidfile = dir.path().join("pid");
|
||||
let mut sup = ProcessSupervisor::new();
|
||||
sup.set_group_term_grace(Duration::from_secs(30));
|
||||
let script = format!(
|
||||
"( trap '' TERM; exec >/dev/null 2>&1; sleep 30 ) & echo $! > {}",
|
||||
pidfile.display()
|
||||
);
|
||||
let (script, pidfile) = survivor_script(dir.path(), true);
|
||||
let id = sup
|
||||
.spawn(sh_group_spec("survivor", &script))
|
||||
.expect("spawn");
|
||||
|
|
@ -3007,15 +3026,14 @@ mod tests {
|
|||
// TERM and KEEPS fd1, so the readers stay alive and the old
|
||||
// drain would block ~2s per EXIT_OUTPUT_DRAIN_TIMEOUT (and
|
||||
// then the join would hang). In-drain ledger enforcement
|
||||
// SIGKILLs at the grace bound instead.
|
||||
// SIGKILLs at the grace bound instead. Readiness-gated so an
|
||||
// early leader-exit TERM can't reap the holder and let the
|
||||
// bound hold vacuously.
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let mut sup = ProcessSupervisor::new();
|
||||
sup.set_group_term_grace(Duration::from_millis(300));
|
||||
let id = sup
|
||||
.spawn(sh_group_spec(
|
||||
"holder",
|
||||
"( trap '' TERM; sleep 30 ) & echo started",
|
||||
))
|
||||
.expect("spawn");
|
||||
let (script, _pidfile) = survivor_script(dir.path(), false);
|
||||
let id = sup.spawn(sh_group_spec("holder", &script)).expect("spawn");
|
||||
let stop = Instant::now() + Duration::from_secs(5);
|
||||
let mut max_tick = Duration::ZERO;
|
||||
let mut events = Vec::new();
|
||||
|
|
|
|||
|
|
@ -453,22 +453,38 @@ fn acc07_term_trapping_child_falls_to_sigkill() {
|
|||
);
|
||||
}
|
||||
|
||||
/// Fixture: background a TERM-ignoring survivor; the leader exits
|
||||
/// only after the survivor's trap is INSTALLED (readiness file).
|
||||
/// Without the gate, a slow scheduler (macOS CI, observed) delivers
|
||||
/// the leader-exit group-TERM before the subshell's `trap` runs and
|
||||
/// kills the "survivor" — making these assertions vacuous. Mirrors
|
||||
/// the process.rs unit fixture.
|
||||
fn survivor_cmdline(dir: &Path, redirect: bool) -> (String, std::path::PathBuf) {
|
||||
let pidfile = dir.join("pid");
|
||||
let ready = dir.join("ready");
|
||||
let redirect_part = if redirect {
|
||||
"exec >/dev/null 2>&1; "
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let cmdline = format!(
|
||||
"( trap '' TERM; : > {ready}; {redirect_part}sleep 30 ) & echo $! > {pid}; \
|
||||
while [ ! -e {ready} ]; do sleep 0.01; done",
|
||||
ready = ready.display(),
|
||||
pid = pidfile.display(),
|
||||
);
|
||||
(cmdline, pidfile)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acc08_ledger_reaps_term_ignoring_redirected_survivor() {
|
||||
// The bite: the survivor ignores TERM and sheds its output, so
|
||||
// the terminal event arrives AND the readers finish — only the
|
||||
// liveness probe can catch it.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let pidfile = dir.path().join("pid");
|
||||
let mut s = editor();
|
||||
compile_run(
|
||||
&s,
|
||||
&format!(
|
||||
"( trap '' TERM; exec >/dev/null 2>&1; sleep 30 ) & echo $! > {}",
|
||||
pidfile.display()
|
||||
),
|
||||
dir.path(),
|
||||
);
|
||||
let (cmdline, pidfile) = survivor_cmdline(dir.path(), true);
|
||||
compile_run(&s, &cmdline, dir.path());
|
||||
assert!(
|
||||
pump_until(&mut s, 5_000, |s| compilation_text(s)
|
||||
.contains("exited with code 0")),
|
||||
|
|
@ -489,7 +505,8 @@ fn acc09_pipe_holding_survivor_bounded_tick_latency() {
|
|||
// at the grace bound instead of blocking ~2s.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut s = editor();
|
||||
compile_run(&s, "( trap '' TERM; sleep 30 ) & echo started", dir.path());
|
||||
let (cmdline, _pidfile) = survivor_cmdline(dir.path(), false);
|
||||
compile_run(&s, &cmdline, dir.path());
|
||||
let stop = Instant::now() + Duration::from_secs(6);
|
||||
let mut max_tick = Duration::ZERO;
|
||||
let mut done = false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue