diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75a72f2..1696af1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,3 +79,17 @@ jobs: run: | cargo build cargo test + + netbsd: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Test on NetBSD + uses: vmactions/netbsd-vm@v1 + with: + usesh: true + prepare: | + pkg_add rust + run: | + cargo build + cargo test diff --git a/src/tests/job.rs b/src/tests/job.rs index c796cd1..b07fa36 100644 --- a/src/tests/job.rs +++ b/src/tests/job.rs @@ -4,6 +4,7 @@ use std::time::{Duration, Instant}; use tempfile::TempDir; +use super::exec_signal_delay; use crate::{Exec, Redirection}; // --- Single-command Job tests --- @@ -237,6 +238,7 @@ fn broken_pipe_on_stdin() { fn terminate() { let start = Instant::now(); let handle = Exec::cmd("sleep").arg("10").start().unwrap(); + exec_signal_delay(); handle.terminate().unwrap(); handle.wait().unwrap(); assert!( @@ -251,6 +253,7 @@ fn terminate_twice() { let start = Instant::now(); let handle = Exec::cmd("sleep").arg("10").start().unwrap(); + exec_signal_delay(); handle.terminate().unwrap(); thread::sleep(Duration::from_millis(100)); handle.terminate().unwrap(); @@ -279,6 +282,7 @@ fn pid_while_running() { job.processes[0].exit_status().is_none(), "exit_status() should be None while running" ); + exec_signal_delay(); job.terminate().unwrap(); job.wait().unwrap(); // pid is still available after exit @@ -292,6 +296,7 @@ fn pid_while_running() { #[test] fn poll_running_process() { let job = Exec::cmd("sleep").arg("10").start().unwrap(); + exec_signal_delay(); assert!( job.poll().is_none(), "poll() should return None for running process" @@ -346,6 +351,7 @@ fn wait_timeout_zero() { "zero timeout took too long" ); assert!(result.is_none()); + exec_signal_delay(); job.terminate().unwrap(); job.wait().unwrap(); } @@ -434,6 +440,7 @@ fn exec_wait_timeout_terminate() { fn started_pid() { let start = Instant::now(); let job = Exec::cmd("sleep").arg("10").start().unwrap(); + exec_signal_delay(); assert!(job.pid() > 0, "pid() should be nonzero"); job.terminate().unwrap(); job.wait().unwrap(); @@ -446,6 +453,7 @@ fn started_pid() { #[test] fn started_kill() { let handle = Exec::cmd("sleep").arg("10").start().unwrap(); + exec_signal_delay(); handle.kill().unwrap(); let status = handle.wait().unwrap(); assert!(!status.success()); @@ -455,6 +463,7 @@ fn started_kill() { fn started_poll() { let start = Instant::now(); let job = Exec::cmd("sleep").arg("10").start().unwrap(); + exec_signal_delay(); assert!(job.poll().is_none(), "poll() should be None while running"); job.terminate().unwrap(); job.wait().unwrap(); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 1ccab83..bda2dbb 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -9,6 +9,15 @@ mod win32; use crate::{Capture, Communicator, Exec, ExitStatus, Job, Pipeline, Process, Redirection}; +/// Work around a NetBSD kernel race where signals sent immediately after +/// exec can be silently dropped. A brief sleep gives the child time to +/// become fully ready for signal delivery. +fn exec_signal_delay() { + if cfg!(target_os = "netbsd") { + std::thread::sleep(std::time::Duration::from_millis(1)); + } +} + fn assert_send_sync() {} #[test] diff --git a/src/tests/posix.rs b/src/tests/posix.rs index f10bebe..646e672 100644 --- a/src/tests/posix.rs +++ b/src/tests/posix.rs @@ -1,11 +1,13 @@ use std::time::{Duration, Instant}; +use super::exec_signal_delay; use crate::unix::{JobExt, PipelineExt}; use crate::{Exec, ExecExt, ExitStatus, Redirection}; #[test] fn err_terminate() { let job = Exec::cmd("sleep").arg("5").start().unwrap(); + exec_signal_delay(); assert!(job.poll().is_none()); job.terminate().unwrap(); assert!(job.wait().unwrap().is_killed_by(libc::SIGTERM)); @@ -29,6 +31,7 @@ fn waitpid_echild() { #[test] fn send_signal() { let job = Exec::cmd("sleep").arg("5").start().unwrap(); + exec_signal_delay(); job.send_signal(libc::SIGUSR1).unwrap(); assert_eq!(job.wait().unwrap().signal(), Some(libc::SIGUSR1)); } @@ -69,6 +72,7 @@ fn exec_setpgid() { .setpgid() .start() .unwrap(); + exec_signal_delay(); job.send_signal_group(libc::SIGTERM).unwrap(); assert!(job.wait().unwrap().is_killed_by(libc::SIGTERM)); } @@ -83,6 +87,7 @@ fn send_signal_group() { .setpgid() .start() .unwrap(); + exec_signal_delay(); job.send_signal_group(libc::SIGTERM).unwrap(); assert!(job.wait().unwrap().is_killed_by(libc::SIGTERM)); } @@ -99,6 +104,7 @@ fn send_signal_group_after_finish() { fn kill_process() { // kill() sends SIGKILL which cannot be caught. let job = Exec::cmd("sleep").arg("10").start().unwrap(); + exec_signal_delay(); job.kill().unwrap(); assert!(job.wait().unwrap().is_killed_by(libc::SIGKILL)); } @@ -108,10 +114,12 @@ fn kill_vs_terminate() { // Demonstrate that terminate (SIGTERM) and kill (SIGKILL) produce // different exit statuses. let j1 = Exec::cmd("sleep").arg("10").start().unwrap(); + exec_signal_delay(); j1.terminate().unwrap(); let status1 = j1.wait().unwrap(); let j2 = Exec::cmd("sleep").arg("10").start().unwrap(); + exec_signal_delay(); j2.kill().unwrap(); let status2 = j2.wait().unwrap(); @@ -155,6 +163,7 @@ fn exit_status_display() { #[test] fn started_send_signal() { let job = Exec::cmd("sleep").arg("5").start().unwrap(); + exec_signal_delay(); job.send_signal(libc::SIGTERM).unwrap(); let status = job.wait().unwrap(); assert!(status.is_killed_by(libc::SIGTERM)); @@ -167,6 +176,7 @@ fn started_send_signal_group() { .setpgid() .start() .unwrap(); + exec_signal_delay(); job.send_signal_group(libc::SIGKILL).unwrap(); let status = job.wait().unwrap(); assert!(status.is_killed_by(libc::SIGKILL) || status.is_killed_by(libc::SIGTERM)); @@ -183,6 +193,7 @@ fn pipeline_setpgid() { .start() .unwrap(); assert_eq!(handle.processes.len(), 2); + exec_signal_delay(); handle.send_signal_group(libc::SIGTERM).unwrap(); for p in &handle.processes { let status = p.wait().unwrap();