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 send_signal_returns_typed_error_when_child_is_still_running_after_signal_failure() {
    let mut process = spawn_long_running_process();
    let mut signal_attempts = 0;
    let mut reap_attempts = 0;

    let error = process
        .send_signal_with_reaper(
            GracefulTerminationPhase::Interrupt,
            signal::INTERRUPT_SIGNAL_NAME,
            |_| {
                signal_attempts += 1;
                Err(io::Error::new(
                    io::ErrorKind::PermissionDenied,
                    "injected signal failure",
                ))
            },
            |_| {
                reap_attempts += 1;
                Ok(None)
            },
        )
        .unwrap_err();

    assert_that!(error.process_name()).is_equal_to("long-running");
    assert_that!(matches!(&error, TerminationError::SignalFailed { .. })).is_true();
    assert_that!(error.attempt_errors().len()).is_equal_to(1);
    assert_attempt_error(
        &error.attempt_errors()[0],
        TerminationAttemptPhase::Interrupt,
        TerminationAttemptOperation::SendSignal,
        Some(signal::INTERRUPT_SIGNAL_NAME),
        io::ErrorKind::PermissionDenied,
        "injected signal failure",
    );
    assert_that!(signal_attempts).is_equal_to(1);
    assert_that!(reap_attempts).is_equal_to(2);
    assert_that!(process.is_drop_armed()).is_true();

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

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

    let error = process
        .send_signal_with_reaper(
            GracefulTerminationPhase::Terminate,
            signal::TERMINATE_SIGNAL_NAME,
            |_| {
                Err(io::Error::new(
                    io::ErrorKind::PermissionDenied,
                    "injected signal failure",
                ))
            },
            |_| {
                reap_attempts += 1;
                match reap_attempts {
                    1 => Ok(None),
                    2 => Err(io::Error::other("injected status failure")),
                    _ => panic!("unexpected reap attempt"),
                }
            },
        )
        .unwrap_err();

    assert_that!(error.process_name()).is_equal_to("long-running");
    assert_that!(matches!(&error, TerminationError::SignalFailed { .. })).is_true();
    assert_that!(error.attempt_errors().len()).is_equal_to(2);
    assert_attempt_error(
        &error.attempt_errors()[0],
        TerminationAttemptPhase::Terminate,
        TerminationAttemptOperation::SendSignal,
        Some(signal::TERMINATE_SIGNAL_NAME),
        io::ErrorKind::PermissionDenied,
        "injected signal failure",
    );
    assert_attempt_error(
        &error.attempt_errors()[1],
        TerminationAttemptPhase::Terminate,
        TerminationAttemptOperation::CheckStatus,
        Some(signal::TERMINATE_SIGNAL_NAME),
        io::ErrorKind::Other,
        "injected status failure",
    );

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