From dd42bcbe75554fd45988b4d8d2ff3a071886157e Mon Sep 17 00:00:00 2001 From: Austin Schaefer Date: Sun, 20 Sep 2026 09:54:14 +0200 Subject: [PATCH] Fix ETXTBSY flake in stub-script tests write_executable_script now probe-execs the script and retries on Text file busy, so it returns only once the script is actually runnable. Co-Authored-By: Claude Sonnet 5 --- src/test_support.rs | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/test_support.rs b/src/test_support.rs index 49b701a..d53c80b 100644 --- a/src/test_support.rs +++ b/src/test_support.rs @@ -1,21 +1,54 @@ //! Test-only fixture helpers shared across modules' `#[cfg(test)]` code -//! (`publisher`, `sanity`) — not production code, and not built outside +//! (`publisher`, `sanity`, `notifier`) — not production code, and not built outside //! `cargo test`. See docs/ARCHITECTURE.md > "organize by pipeline stage, not //! by layer": this exists to remove one specific piece of duplication //! (two near-identical copies of "write an executable shell script"), not //! as a general test-utils dump. use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, Instant}; + +/// Set by `wait_until_executable`'s probe so the script exits before +/// running its real body. +const PROBE_ENV: &str = "PKGWATCH_TEST_SCRIPT_PROBE"; +/// `ETXTBSY`: exec of a file some process still has open for writing. +const TEXT_FILE_BUSY: i32 = 26; /// Writes an executable `#!/bin/sh` script named `name` into `dir`, /// running `body` as its contents. Used to stand in for a real binary /// (`repo-add`, a package's own `--version` command) in tests, without /// needing the real tool installed or a mutated global `PATH`. +/// +/// Doesn't return until the script can actually be exec'd. `cargo test` +/// runs tests on parallel threads, and a `fork` on another thread between +/// this function's write-open and close leaves the child holding a copy of +/// the write fd until it execs, so an immediate exec of the new script can +/// fail with `Text file busy`. Once no holder is left none can appear (our +/// fd is closed), so a probe exec that succeeds proves later ones will. pub(crate) fn write_executable_script(dir: &Path, name: &str, body: &str) -> PathBuf { let path = dir.join(name); - std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap(); + std::fs::write( + &path, + format!("#!/bin/sh\n[ -z \"${PROBE_ENV}\" ] || exit 0\n{body}\n"), + ) + .unwrap(); let mut perms = std::fs::metadata(&path).unwrap().permissions(); std::os::unix::fs::PermissionsExt::set_mode(&mut perms, 0o755); std::fs::set_permissions(&path, perms).unwrap(); + wait_until_executable(&path); path } + +fn wait_until_executable(path: &Path) { + let deadline = Instant::now() + Duration::from_secs(5); + loop { + match Command::new(path).env(PROBE_ENV, "1").status() { + Ok(_) => return, + Err(err) if err.raw_os_error() == Some(TEXT_FILE_BUSY) && Instant::now() < deadline => { + std::thread::sleep(Duration::from_millis(5)); + } + Err(err) => panic!("probe-exec of {} failed: {err}", path.display()), + } + } +}