ic-host-process 0.8.4

Admitted host executable resolution, bounded execution and Git observations
Documentation
use super::*;

#[test]
fn output_is_observed_before_child_exit_and_descendant_pipe_cleanup() {
    let fixture = Fixture::new();
    let go = fixture.root.join("go");
    let mut command =
        command("printf '\\377'; while ! test -f \"$GO\"; do sleep 0.01; done; sleep 30 & exit 0");
    command.env("GO", &go);
    let mut child = OwnedChild::spawn(&mut command).unwrap();
    let mut observed = Vec::new();
    let evidence = communicate_child_with_observer(
        &mut child,
        None,
        LIMITS,
        SuccessfulExit::Cleanup,
        || false,
        |stream, bytes| {
            assert_eq!(stream, OutputStream::Stdout);
            observed.extend_from_slice(bytes);
            fs::write(&go, b"go").unwrap();
        },
    )
    .unwrap();
    assert_eq!(observed, [255]);
    assert_eq!(evidence.stdout, observed);
    assert!(evidence.status.unwrap().success());
    assert_reaped(&child);
}

#[test]
fn observer_receives_only_retained_prefix_on_each_stream_overflow() {
    let _fixture = Fixture::new();
    for (script, expected) in [
        ("printf 12345; sleep 30", OutputStream::Stdout),
        ("printf 12345 >&2; sleep 30", OutputStream::Stderr),
    ] {
        for maximum in [0, 4] {
            let mut child = OwnedChild::spawn(&mut command(script)).unwrap();
            let mut observed = Vec::new();
            let error = communicate_child_with_observer(
                &mut child,
                None,
                OutputLimits {
                    stdout_bytes: maximum,
                    stderr_bytes: maximum,
                    ..LIMITS
                },
                SuccessfulExit::Cleanup,
                || false,
                |stream, bytes| {
                    assert_eq!(stream, expected);
                    assert_ne!(bytes, []);
                    observed.extend_from_slice(bytes);
                },
            )
            .unwrap_err();
            let execution = error.execution_error().unwrap();
            assert!(
                matches!(execution.failure, ExecutionFailure::OutputLimit { stream } if stream == expected)
            );
            assert_eq!(observed, &b"12345"[..maximum]);
            let retained = match expected {
                OutputStream::Stdout => &execution.evidence.stdout,
                OutputStream::Stderr => &execution.evidence.stderr,
            };
            assert_eq!(&observed, retained);
            assert!(execution.group_error.is_none() && execution.wait_error.is_none());
            assert_reaped(&child);
        }
    }
}

#[test]
fn silent_child_allows_caller_progress_and_cancellation_without_output() {
    let _fixture = Fixture::new();
    let mut child = OwnedChild::spawn(&mut command("sleep 30")).unwrap();
    let mut polls = 0;
    let error = communicate_child_with_observer(
        &mut child,
        None,
        LIMITS,
        SuccessfulExit::Cleanup,
        || {
            polls += 1;
            polls == 3
        },
        |_, _| panic!("silent child emitted output"),
    )
    .unwrap_err();
    assert!(matches!(
        error.execution_error().unwrap().failure,
        ExecutionFailure::Cancelled
    ));
    assert_eq!(polls, 3);
    assert_reaped(&child);
}

#[test]
fn callback_unwind_cleans_group_even_when_borrowed_owner_survives_catch() {
    use std::{
        io::Read,
        panic::{AssertUnwindSafe, catch_unwind},
    };
    let _fixture = Fixture::new();
    for panic_in_output in [false, true] {
        let mut child = OwnedChild::spawn(&mut command("sleep 30 & printf ready; wait")).unwrap();
        // Retain a separate pipe as evidence that descendants release writers.
        let mut stderr = child.take_stderr().unwrap();
        let flags = rustix::fs::fcntl_getfl(&stderr).unwrap();
        rustix::fs::fcntl_setfl(&stderr, flags | rustix::fs::OFlags::NONBLOCK).unwrap();
        let panic = catch_unwind(AssertUnwindSafe(|| {
            let _ = communicate_child_with_observer(
                &mut child,
                None,
                LIMITS,
                SuccessfulExit::Cleanup,
                || {
                    assert!(panic_in_output, "cancel callback panic");
                    false
                },
                |_, _| panic!("output callback panic"),
            );
        }))
        .unwrap_err();
        assert_eq!(
            panic.downcast_ref::<&str>(),
            Some(&if panic_in_output {
                "output callback panic"
            } else {
                "cancel callback panic"
            })
        );
        assert_reaped(&child);
        let deadline = Instant::now() + Duration::from_secs(5);
        loop {
            match stderr.read(&mut [0]) {
                Ok(0) => break,
                Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
                    assert!(Instant::now() < deadline);
                    std::thread::sleep(Duration::from_millis(2));
                }
                other => panic!("unexpected pipe result: {other:?}"),
            }
        }
    }
}