diff --git a/hive-runtime/src/stop.rs b/hive-runtime/src/stop.rs index 640c7090..896c30b7 100644 --- a/hive-runtime/src/stop.rs +++ b/hive-runtime/src/stop.rs @@ -64,6 +64,24 @@ mod tests { sleep 30 >/dev/null 2>&1 &\n\ wait\n"; + /// Write the script `body` to `path` as an executable, from a child process + /// rather than from this one: a write fd on it held here could be inherited + /// by a child another test thread forks, and exec'ing a file some process + /// still holds open for writing fails with `ETXTBSY`. + fn write_executable(path: &Path, body: &str) { + let status = std::process::Command::new("/bin/sh") + .args([ + "-c", + r#"printf '%s' "$1" > "$2" && chmod 755 "$2""#, + "sh", + body, + ]) + .arg(path) + .status() + .expect("run /bin/sh"); + assert!(status.success(), "writing {} failed", path.display()); + } + async fn exists(path: &Path) { for _ in 0..250 { if path.exists() { @@ -76,12 +94,9 @@ mod tests { #[tokio::test] async fn a_claude_stop_is_a_sigint_to_the_process() { - use std::os::unix::fs::PermissionsExt as _; - let dir = tempfile::tempdir().unwrap(); let program = dir.path().join("fake-claude"); - std::fs::write(&program, FAKE_CLAUDE).unwrap(); - std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap(); + write_executable(&program, FAKE_CLAUDE); let (ready, mark) = (dir.path().join("ready"), dir.path().join("int")); let config = Config { program: Some(program), diff --git a/swarm-authelia-bridge/src/main.rs b/swarm-authelia-bridge/src/main.rs index 1ea6d6a3..a68e7742 100644 --- a/swarm-authelia-bridge/src/main.rs +++ b/swarm-authelia-bridge/src/main.rs @@ -626,23 +626,38 @@ mod tests { let _ = std::fs::remove_dir_all(config.users_file.parent().unwrap()); } + /// Write the script `body` to `path` as an executable, from a child process + /// rather than from this one: a write fd on it held here could be inherited + /// by a child another test thread forks, and exec'ing a file some process + /// still holds open for writing fails with `ETXTBSY`. + fn write_executable(path: &std::path::Path, body: &str) { + let status = std::process::Command::new("/bin/sh") + .args([ + "-c", + r#"printf '%s' "$1" > "$2" && chmod 755 "$2""#, + "sh", + body, + ]) + .arg(path) + .status() + .expect("run /bin/sh"); + assert!(status.success(), "writing {} failed", path.display()); + } + /// `handle` waits for the `flock` `swarmctl` takes, not only for the /// in-process lock: it is held here while `handle` runs with a lock of /// its own that nothing contends. #[tokio::test] async fn an_identity_request_waits_for_the_users_file_lock() { - use std::os::unix::fs::PermissionsExt as _; use std::time::Duration; let mut config = config_with_one_operator(); let dir = config.users_file.parent().unwrap().to_owned(); let authelia = dir.join("authelia"); - std::fs::write( + write_executable( &authelia, "#!/bin/sh\nprintf 'Random Password: pw\\nDigest: $argon2id$x\\n'\n", - ) - .unwrap(); - std::fs::set_permissions(&authelia, std::fs::Permissions::from_mode(0o755)).unwrap(); + ); config.authelia_bin = authelia; let held = crate::store::lock_users_file(&config.users_file, Duration::from_secs(1)) diff --git a/swarm-claude-login/src/login.rs b/swarm-claude-login/src/login.rs index 1b504623..545071ac 100644 --- a/swarm-claude-login/src/login.rs +++ b/swarm-claude-login/src/login.rs @@ -355,8 +355,6 @@ fn failed(why: &str, session: Option<&Session>) -> Reply { #[cfg(test)] pub(crate) mod tests { - use std::os::unix::fs::PermissionsExt as _; - use super::*; /// A stand-in for `claude auth login`: prints a URL, reads a code, and @@ -389,9 +387,25 @@ echo "Login successful." } } - /// Every stub, written once before any test spawns one: a file still - /// open for writing in a child another test forked meanwhile cannot be - /// executed (`ETXTBSY`). + /// Write the script `body` to `path` as an executable, from a child process + /// rather than from this one: a write fd on it held here could be inherited + /// by a child another test thread forks, and exec'ing a file some process + /// still holds open for writing fails with `ETXTBSY`. + fn write_executable(path: &Path, body: &str) { + let status = std::process::Command::new("/bin/sh") + .args([ + "-c", + r#"printf '%s' "$1" > "$2" && chmod 755 "$2""#, + "sh", + body, + ]) + .arg(path) + .status() + .expect("run /bin/sh"); + assert!(status.success(), "writing {} failed", path.display()); + } + + /// Every stub, written once before any test spawns one. pub(crate) fn stubs() -> &'static Path { static DIR: std::sync::OnceLock = std::sync::OnceLock::new(); DIR.get_or_init(|| { @@ -406,10 +420,7 @@ echo "Login successful." ("silent", "#!/bin/sh\nexec sleep 600\n"), ("exits", "#!/bin/sh\necho \"not logged in\" >&2\nexit 1\n"), ] { - let stub = dir.join(name); - std::fs::write(&stub, script).expect("stub"); - std::fs::set_permissions(&stub, std::fs::Permissions::from_mode(0o755)) - .expect("chmod"); + write_executable(&dir.join(name), script); } dir }) diff --git a/swarmctl/src/main.rs b/swarmctl/src/main.rs index 1561c1a4..bfedbf5e 100644 --- a/swarmctl/src/main.rs +++ b/swarmctl/src/main.rs @@ -1573,6 +1573,24 @@ mod tests { fs::remove_dir_all(&dir).ok(); } + /// Write the script `body` to `path` as an executable, from a child process + /// rather than from this one: a write fd on it held here could be inherited + /// by a child another test thread forks, and exec'ing a file some process + /// still holds open for writing fails with `ETXTBSY`. + fn write_executable(path: &std::path::Path, body: &str) { + let status = std::process::Command::new("/bin/sh") + .args([ + "-c", + r#"printf '%s' "$1" > "$2" && chmod 755 "$2""#, + "sh", + body, + ]) + .arg(path) + .status() + .expect("run /bin/sh"); + assert!(status.success(), "writing {} failed", path.display()); + } + /// A scratch directory holding a `users.yml` with one user, `carol`, and /// a stand-in `authelia` that takes `delay` seconds to mint a password. fn locked_users_fixture(tag: &str, delay: &str) -> Paths { @@ -1580,14 +1598,12 @@ mod tests { let _ = fs::remove_dir_all(&dir); fs::create_dir_all(&dir).expect("temp dir"); let authelia_bin = dir.join("authelia"); - fs::write( + write_executable( &authelia_bin, - format!( + &format!( "#!/bin/sh\nsleep {delay}\nprintf 'Random Password: pw\\nDigest: $argon2id$x\\n'\n" ), - ) - .expect("fake authelia"); - fs::set_permissions(&authelia_bin, Permissions::from_mode(0o755)).expect("chmod"); + ); let users_file = dir.join("users.yml"); fs::write( &users_file,