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 <noreply@anthropic.com>
This commit is contained in:
parent
38a32a0c60
commit
dd42bcbe75
1 changed files with 35 additions and 2 deletions
|
|
@ -1,21 +1,54 @@
|
||||||
//! Test-only fixture helpers shared across modules' `#[cfg(test)]` code
|
//! 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
|
//! `cargo test`. See docs/ARCHITECTURE.md > "organize by pipeline stage, not
|
||||||
//! by layer": this exists to remove one specific piece of duplication
|
//! by layer": this exists to remove one specific piece of duplication
|
||||||
//! (two near-identical copies of "write an executable shell script"), not
|
//! (two near-identical copies of "write an executable shell script"), not
|
||||||
//! as a general test-utils dump.
|
//! as a general test-utils dump.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
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`,
|
/// Writes an executable `#!/bin/sh` script named `name` into `dir`,
|
||||||
/// running `body` as its contents. Used to stand in for a real binary
|
/// running `body` as its contents. Used to stand in for a real binary
|
||||||
/// (`repo-add`, a package's own `--version` command) in tests, without
|
/// (`repo-add`, a package's own `--version` command) in tests, without
|
||||||
/// needing the real tool installed or a mutated global `PATH`.
|
/// 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 {
|
pub(crate) fn write_executable_script(dir: &Path, name: &str, body: &str) -> PathBuf {
|
||||||
let path = dir.join(name);
|
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();
|
let mut perms = std::fs::metadata(&path).unwrap().permissions();
|
||||||
std::os::unix::fs::PermissionsExt::set_mode(&mut perms, 0o755);
|
std::os::unix::fs::PermissionsExt::set_mode(&mut perms, 0o755);
|
||||||
std::fs::set_permissions(&path, perms).unwrap();
|
std::fs::set_permissions(&path, perms).unwrap();
|
||||||
|
wait_until_executable(&path);
|
||||||
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue