tokio-process-tools 0.11.2

Correctness-focused async subprocess orchestration for Tokio: bounded output, multi-consumer streams, output detection, guaranteed cleanup and graceful termination.
Documentation
#![allow(missing_docs)]

mod common;

use assertr::prelude::*;
use common::*;

#[tokio::test]
async fn must_be_terminated_is_idempotent_when_already_armed() {
    let mut process = spawn_long_running_process();

    process.must_be_terminated();
    assert_that!(process.is_drop_armed()).is_true();

    process.kill().await.unwrap();
}

#[tokio::test]
async fn must_be_terminated_re_arms_safeguards_after_opt_out() {
    let mut process = spawn_long_running_process();

    process.must_not_be_terminated();
    assert_that!(process.is_drop_disarmed()).is_true();

    process.must_be_terminated();
    assert_that!(process.is_drop_armed()).is_true();

    process.kill().await.unwrap();
}

#[cfg(unix)]
#[tokio::test]
async fn must_not_be_terminated_lets_child_outlive_dropped_handle() {
    use nix::errno::Errno;
    use nix::sys::signal::{self, Signal};
    use nix::sys::wait::waitpid;
    use nix::unistd::Pid;

    let mut process = spawn_long_running_process();
    let pid = process.id().unwrap();

    process.must_not_be_terminated();
    assert_that!(process.is_drop_disarmed()).is_true();
    drop(process);

    let pid = Pid::from_raw(pid.cast_signed());
    assert_that!(signal::kill(pid, None).is_ok()).is_true();

    signal::kill(pid, Signal::SIGKILL).unwrap();
    match waitpid(pid, None) {
        Ok(_) | Err(Errno::ECHILD) => {}
        Err(err) => {
            assert_that!(err).fail(format_args!("waitpid failed: {err}"));
        }
    }
}

#[cfg(unix)]
#[tokio::test]
async fn must_not_be_terminated_still_closes_stdin_on_drop() {
    use nix::errno::Errno;
    use nix::sys::wait::waitpid;
    use nix::unistd::Pid;
    use std::fs;
    use std::time::Duration;
    use tempfile::tempdir;
    use tokio_process_tools::{DEFAULT_MAX_BUFFERED_CHUNKS, DEFAULT_READ_CHUNK_SIZE, Process};

    let temp_dir = tempdir().unwrap();
    let output_file = temp_dir.path().join("stdin-result.txt");
    let output_file = output_file.to_str().unwrap().replace('\'', "'\"'\"'");

    let mut cmd = tokio::process::Command::new("sh");
    cmd.arg("-c")
        .arg(format!("cat >/dev/null; printf eof > '{output_file}'"));

    let mut process = Process::new(cmd)
        .name("sh")
        .stdout_and_stderr(|stream| {
            stream
                .broadcast()
                .lossy_without_backpressure()
                .no_replay()
                .read_chunk_size(DEFAULT_READ_CHUNK_SIZE)
                .max_buffered_chunks(DEFAULT_MAX_BUFFERED_CHUNKS)
        })
        .spawn()
        .unwrap();
    let pid = Pid::from_raw(process.id().unwrap().cast_signed());

    process.must_not_be_terminated();
    drop(process);

    match tokio::time::timeout(
        Duration::from_secs(2),
        tokio::task::spawn_blocking(move || waitpid(pid, None)),
    )
    .await
    .unwrap()
    .unwrap()
    {
        Ok(_) | Err(Errno::ECHILD) => {}
        Err(err) => {
            assert_that!(err).fail(format_args!("waitpid failed: {err}"));
        }
    }

    assert_that!(fs::read_to_string(temp_dir.path().join("stdin-result.txt")).unwrap())
        .is_equal_to("eof");
}

#[cfg(unix)]
#[tokio::test]
async fn must_not_be_terminated_still_closes_stdout_pipe_on_drop() {
    use nix::errno::Errno;
    use nix::sys::wait::waitpid;
    use nix::unistd::Pid;
    use std::time::Duration;
    use tokio_process_tools::{DEFAULT_MAX_BUFFERED_CHUNKS, DEFAULT_READ_CHUNK_SIZE, Process};

    let mut cmd = tokio::process::Command::new("yes");
    cmd.arg("tick");

    let mut process = Process::new(cmd)
        .name("yes")
        .stdout_and_stderr(|stream| {
            stream
                .broadcast()
                .lossy_without_backpressure()
                .no_replay()
                .read_chunk_size(DEFAULT_READ_CHUNK_SIZE)
                .max_buffered_chunks(DEFAULT_MAX_BUFFERED_CHUNKS)
        })
        .spawn()
        .unwrap();
    let pid = Pid::from_raw(process.id().unwrap().cast_signed());

    process.must_not_be_terminated();
    drop(process);

    match tokio::time::timeout(
        Duration::from_secs(2),
        tokio::task::spawn_blocking(move || waitpid(pid, None)),
    )
    .await
    .unwrap()
    .unwrap()
    {
        Ok(_) | Err(Errno::ECHILD) => {}
        Err(err) => {
            assert_that!(err).fail(format_args!("waitpid failed: {err}"));
        }
    }
}