test(compile): portable process probes — macOS CI

Two supervisor unit tests failed on the macOS CI matrix (both Lua
flavors; Linux green):

- pgid_of read /proc/<pid>/stat, which has no macOS equivalent — now
  probes via `ps -o pgid=` (portable, still avoids widening the nix
  feature set with `process` for getpgid).
- the setsid escape-hatch test requires util-linux's setsid(1),
  absent on macOS — now skips per-test when setsid isn't on PATH
  (the m6_5 selective-skip precedent); the escape hatch is a
  Linux-production behavior and the other group-lifecycle tests
  still run everywhere.

Also fixed while here: the acceptance suite's pid_alive was a /proc
existence check, which on macOS made every "descendant is dead"
assertion vacuously TRUE (passing, but toothless) — now a portable
`kill -0` probe, so the group-kill assertions bite on both OSes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
This commit is contained in:
Levi Neuwirth 2026-07-13 16:35:32 +01:00
parent 37fac4324a
commit 50abf91c15
2 changed files with 38 additions and 11 deletions

View File

@ -2723,18 +2723,30 @@ mod tests {
nix::sys::signal::kill(Pid::from_raw(pid), None).is_ok()
}
/// Process group of `pid` per /proc/<pid>/stat field 5 (Linux;
/// avoids widening the nix feature set with `process`). The comm
/// field may contain spaces, so parse after the closing paren.
/// Process group of `pid` via `ps` (portable across Linux and
/// macOS CI — the previous /proc/<pid>/stat read has no macOS
/// equivalent; `ps -o pgid=` avoids widening the nix feature set
/// with `process` for `getpgid`).
fn pgid_of(pid: u32) -> i32 {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).expect("read stat");
let after_comm = &stat[stat.rfind(')').expect("comm paren") + 2..];
after_comm
.split_whitespace()
.nth(2) // state ppid pgrp → index 2
.expect("pgrp field")
let out = std::process::Command::new("ps")
.args(["-o", "pgid=", "-p", &pid.to_string()])
.output()
.expect("run ps");
String::from_utf8_lossy(&out.stdout)
.trim()
.parse()
.expect("pgrp parses")
.expect("pgid parses")
}
/// True when `name` resolves on PATH. Fixture-dependency gate:
/// the setsid escape-hatch test needs util-linux's setsid(1),
/// absent on macOS — skip per-test rather than fail (the
/// `m6_5_repl_acceptance` selective-skip precedent).
fn binary_available(name: &str) -> bool {
std::process::Command::new("which")
.arg(name)
.output()
.is_ok_and(|o| o.status.success())
}
/// Poll `path` until it holds a parseable pid. Fixture scripts
@ -3034,6 +3046,14 @@ mod tests {
// joins must complete, and the per-runtime active-reader
// count must return to zero — across repeated cycles, so
// nothing accumulates.
if !binary_available("setsid") {
// util-linux's setsid(1) is absent on macOS CI; the
// escape hatch is a Linux-production behavior. Skip
// rather than fail — the other group-lifecycle tests
// still run everywhere.
eprintln!("skipping: setsid(1) not on PATH");
return;
}
let dir = tempfile::tempdir().expect("tempdir");
let mut escapees = Vec::new();
for round in 0..3 {

View File

@ -184,7 +184,14 @@ fn wait_pidfile(s: &mut EditorState, path: &Path) -> i32 {
}
fn pid_alive(pid: i32) -> bool {
Path::new(&format!("/proc/{pid}")).exists()
// `kill -0` probe via /bin/kill: portable across Linux and macOS
// (a /proc existence check has no macOS equivalent and would
// make every "descendant is dead" assertion vacuously true
// there).
std::process::Command::new("kill")
.args(["-0", &pid.to_string()])
.output()
.is_ok_and(|o| o.status.success())
}
/// Errors getter marshalled to a comparable Rust shape (encoded as