tokio-process-tools 0.9.0

Correctness-focused async subprocess orchestration for Tokio: bounded output, multi-consumer streams, output detection, guaranteed cleanup and graceful termination.
Documentation
use super::*;

#[tokio::test]
async fn preflight_status_error_does_not_stop_termination_escalation() {
    use std::sync::Arc;
    use std::sync::atomic::{AtomicBool, Ordering};

    let mut process = spawn_long_running_process();
    let interrupt_attempted = Arc::new(AtomicBool::new(false));
    let interrupt_attempted_in_sender = Arc::clone(&interrupt_attempted);
    let terminate_attempted = Arc::new(AtomicBool::new(false));
    let terminate_attempted_in_sender = Arc::clone(&terminate_attempted);

    let outcome = process
        .terminate_inner_with_preflight_reaper(
            Duration::from_millis(10),
            Duration::from_millis(10),
            |_| Err(io::Error::other("injected preflight status failure")),
            move |_| {
                interrupt_attempted_in_sender.store(true, Ordering::SeqCst);
                Err(io::Error::other("injected interrupt signal failure"))
            },
            move |_| {
                terminate_attempted_in_sender.store(true, Ordering::SeqCst);
                Err(io::Error::other("injected terminate signal failure"))
            },
        )
        .await
        .unwrap();

    assert_that!(interrupt_attempted.load(Ordering::SeqCst)).is_true();
    assert_that!(terminate_attempted.load(Ordering::SeqCst)).is_true();
    assert_that!(outcome.exit_status.success()).is_false();
    assert_that!(process.is_drop_disarmed()).is_true();
}

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

    tokio::time::sleep(Duration::from_millis(50)).await;

    assert_that!(process.id()).is_some();

    process.send_interrupt_signal().unwrap();

    assert_that!(process.id()).is_none();
    assert_that!(process.is_drop_disarmed()).is_true();
}

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

    tokio::time::sleep(Duration::from_millis(50)).await;

    assert_that!(process.id()).is_some();

    process.send_terminate_signal().unwrap();

    assert_that!(process.id()).is_none();
    assert_that!(process.is_drop_disarmed()).is_true();
}

#[tokio::test]
async fn send_signal_reaps_exit_observed_after_signal_failure() {
    let mut process = spawn_long_running_process();
    let mut signal_attempts = 0;
    let mut reap_attempts = 0;

    let result = process.send_signal_with_reaper(
        GracefulTerminationPhase::Interrupt,
        signal::INTERRUPT_SIGNAL_NAME,
        |_| {
            signal_attempts += 1;
            Err(io::Error::new(
                io::ErrorKind::NotFound,
                "injected signal failure",
            ))
        },
        |process| {
            reap_attempts += 1;
            match reap_attempts {
                1 => Ok(None),
                2 => {
                    process.must_not_be_terminated();
                    Ok(Some(successful_exit_status()))
                }
                _ => panic!("unexpected reap attempt"),
            }
        },
    );

    assert_that!(result).is_ok();
    assert_that!(signal_attempts).is_equal_to(1);
    assert_that!(reap_attempts).is_equal_to(2);
    assert_that!(process.is_drop_disarmed()).is_true();

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