Skip to content

Commit f295110

Browse files
committed
BROKEN TEST: assert that dropped Handles eventually reap the zombie child
1 parent 04c2c8c commit f295110

File tree

1 file changed

+84
-0
lines changed

1 file changed

+84
-0
lines changed

src/test.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,3 +656,87 @@ fn test_pids() -> io::Result<()> {
656656

657657
Ok(())
658658
}
659+
660+
#[cfg(not(windows))]
661+
fn ps_observes_pid(pid: u32) -> io::Result<bool> {
662+
let pid_str = &pid.to_string()[..];
663+
let ps_output = cmd!("ps", "-p", pid_str)
664+
.unchecked()
665+
.stdout_capture()
666+
.run()?;
667+
let ps_str = String::from_utf8_lossy(&ps_output.stdout);
668+
let ps_lines: Vec<&str> = ps_str.lines().collect();
669+
// `ps` prints headers on the first line by default.
670+
assert!(ps_lines.len() == 1 || ps_lines.len() == 2);
671+
if ps_lines.len() == 2 {
672+
assert!(ps_lines[1].contains(pid_str));
673+
// The exit code should agree with the output.
674+
assert!(ps_output.status.success());
675+
Ok(true)
676+
} else {
677+
assert!(!ps_output.status.success());
678+
Ok(false)
679+
}
680+
}
681+
682+
// We don't spawn reaper threads on Windows, and `ps` doesn't exist on Windows either.
683+
#[cfg(not(windows))]
684+
#[test]
685+
fn test_zombies_reaped() -> io::Result<()> {
686+
let mut child_handles = Vec::new();
687+
let mut child_pids = Vec::new();
688+
689+
// Spawn 10 children that will exit immediately.
690+
let (mut stdout_reader, stdout_writer) = os_pipe::pipe()?;
691+
for _ in 0..10 {
692+
let handle = cmd!(path_to_exe("status"), "0")
693+
.stdout_file(stdout_writer.try_clone()?)
694+
.start()?;
695+
child_pids.push(handle.pids()[0]);
696+
child_handles.push(handle);
697+
}
698+
699+
// Spawn 10 children that will wait on stdin to exit. The previous 10 children will probably
700+
// exit while we're doing this.
701+
let (stdin_reader, stdin_writer) = os_pipe::pipe()?;
702+
for _ in 0..10 {
703+
let handle = cmd!(path_to_exe("cat"))
704+
.stdin_file(stdin_reader.try_clone()?)
705+
.stdout_file(stdout_writer.try_clone()?)
706+
.start()?;
707+
child_pids.push(handle.pids()[0]);
708+
child_handles.push(handle);
709+
}
710+
drop(stdin_reader);
711+
drop(stdout_writer);
712+
713+
// Drop all the handles. The first 10 children will probably get reaped at this point without
714+
// spawning a thread. The last 10 children definitely have not exited, and each of them will
715+
// get a waiter thread.
716+
drop(child_handles);
717+
718+
// Drop the stdin writer. Now the last 10 children will begin exiting.
719+
drop(stdin_writer);
720+
721+
// Read the stdout pipe to EOF. This means all the children have exited. It's not a *guarantee*
722+
// that `ps` won't still observe them, but this plus a few `ps` retries should be good enough.
723+
// If this test starts spuriously failing, I'll need to double check this assumption.
724+
let mut stdout_bytes = Vec::new();
725+
stdout_reader.read_to_end(&mut stdout_bytes)?;
726+
assert_eq!(stdout_bytes.len(), 0, "no output expected");
727+
728+
// Assert that all the children get cleaned up. This is a Unix-only test, so we can just shell
729+
// out to `ps`.
730+
for (i, pid) in child_pids.into_iter().enumerate() {
731+
eprintln!("checking child #{i} (PID {pid})");
732+
// Retry `ps` 100 times for each child, to be as confident as possible that the child has
733+
// time to get reaped.
734+
let mut tries = 0;
735+
while ps_observes_pid(pid)? {
736+
tries += 1;
737+
assert!(tries < 100, "PID never went away?");
738+
}
739+
}
740+
741+
Ok(())
742+
}

0 commit comments

Comments
 (0)