aion-cli 0.31.0

The `aion` command line: operate Aion durable workflows over gRPC and run the Aion server.
//! The shared teardown guard for every test in this crate that spawns a child
//! process.
//!
//! # Why this exists
//!
//! An e2e test that spawns `aion server` and `aion dev`, works with `?` on a
//! dozen fallible calls, and kills its children only on the last line has a
//! teardown that runs on exactly ONE path. Every other path — a missed
//! deadline, a failed assertion, an `io::Error` out of `try_wait` — returns
//! early, the `Child` values drop without ever being killed (`std`'s `Child`
//! explicitly does NOT kill on drop), and the processes are reparented to
//! init. That is not a theoretical leak: an `aion dev --endpoint
//! 127.0.0.1:53390` from a failed run was found 23 minutes later still running
//! out of a battery workspace that had already been deleted, holding a port
//! and a compiler's worth of CPU against the next measurement taken on that
//! machine.
//!
//! [`Reaped`] moves teardown from the happy path to `Drop`, which runs on
//! every path there is.
//!
//! # Why the module lives here rather than in `common/`
//!
//! `#[path = "test_support/child.rs"] mod ...` is the idiom this crate already
//! uses for `test_support/gleam.rs`: one file, included by each test binary
//! that needs it, resolved relative to the including file so nothing reaches
//! across a crate boundary and `cargo package` keeps working.

use std::io::{self, Read as _};
use std::ops::{Deref, DerefMut};
use std::process::{Child, Command, Output};

/// A spawned [`Child`] that is killed and reaped when it goes out of scope.
///
/// **The invariant: a spawned child never outlives the test that spawned it,
/// on any path.** Not on the happy path, not on an early `?`, not on a failed
/// assertion, not on a panic that unwinds the test thread. `Drop` sends a kill
/// and then waits for the corpse, so nothing is left running and nothing is
/// left as a zombie.
///
/// # Using it
///
/// Spawn through [`Reaped::spawn`] rather than [`Command::spawn`]: the child
/// is under the guard from the instant it exists, with no window in which an
/// early return could strand it.
///
/// The guard [`Deref`]s to the [`Child`] it owns, so every call a test already
/// makes — `try_wait`, `kill`, `wait`, `id`, `stdout.take()` — keeps working
/// unchanged, and a helper taking `&mut Child` still accepts `&mut guard`.
///
/// # Deliberate mid-test kills
///
/// A test that kills its child ON PURPOSE (a `kill -9` failover proof, a
/// SIGTERM drain proof) keeps doing exactly that through the deref. The guard
/// does not fight it: `Drop` finds the process already gone and does nothing.
///
/// # Errors it cannot return
///
/// `Drop` cannot return a `Result`, and a test must not be failed by its own
/// teardown, so a kill that fails for a reason OTHER than "the process has
/// already exited" is reported on stderr, naming the pid and the error. It is
/// never swallowed silently: a teardown that cannot reap is exactly the
/// condition this type exists to make visible.
pub(crate) struct Reaped {
    /// The supervised process. Owned outright — nothing hands it out by value.
    child: Child,
}

impl Reaped {
    /// Spawn `command` and take ownership of the resulting child.
    ///
    /// # Errors
    ///
    /// Propagates the [`Command::spawn`] failure unchanged. No child exists on
    /// that path, so there is nothing to reap.
    pub(crate) fn spawn(command: &mut Command) -> io::Result<Self> {
        Ok(Self {
            child: command.spawn()?,
        })
    }

    /// Kill the child and wait for it, for the teardown a test wants to
    /// assert on.
    ///
    /// A child that has already exited is not an error: the kill is skipped
    /// and the corpse is still reaped, so this is safe to call after the
    /// process ended on its own, and safe to call twice.
    ///
    /// # Errors
    ///
    /// Fails when the kill fails for any reason other than the process having
    /// already exited, or when waiting for it fails.
    pub(crate) fn kill_and_wait(&mut self) -> io::Result<()> {
        // `std` reports a kill against an already-reaped child as
        // `InvalidInput` — the pid may have been recycled, so it refuses to
        // signal it. That is the "already exited" case, not a failure. Every
        // other error is returned.
        if let Err(error) = self.child.kill()
            && error.kind() != io::ErrorKind::InvalidInput
        {
            return Err(error);
        }
        self.child.wait()?;
        Ok(())
    }

    /// Wait for the child to exit and collect everything it wrote, the way
    /// [`Child::wait_with_output`] does.
    ///
    /// [`Child::wait_with_output`] consumes the `Child` by value, which a
    /// guard cannot hand out without breaking its own invariant, so the drain
    /// is performed here instead: stdin is closed, stdout is drained on a
    /// separate thread while stderr is drained on this one (draining them in
    /// sequence would deadlock against a child that fills the other pipe), and
    /// the child is then reaped. `Drop` afterwards finds it already gone.
    ///
    /// # Errors
    ///
    /// Fails when either pipe cannot be read, when the thread draining stdout
    /// panics, or when waiting for the child fails.
    pub(crate) fn wait_with_output(&mut self) -> io::Result<Output> {
        drop(self.child.stdin.take());
        let stdout_pipe = self.child.stdout.take();
        let stderr_pipe = self.child.stderr.take();

        let drain = stdout_pipe.map(|mut pipe| {
            std::thread::spawn(move || {
                let mut buffer = Vec::new();
                pipe.read_to_end(&mut buffer).map(|_| buffer)
            })
        });

        let mut stderr = Vec::new();
        if let Some(mut pipe) = stderr_pipe {
            pipe.read_to_end(&mut stderr)?;
        }

        let stdout = match drain {
            Some(handle) => handle
                .join()
                .map_err(|_| io::Error::other("the stdout drain thread panicked"))??,
            None => Vec::new(),
        };

        let status = self.child.wait()?;
        Ok(Output {
            status,
            stdout,
            stderr,
        })
    }
}

impl Deref for Reaped {
    type Target = Child;

    fn deref(&self) -> &Self::Target {
        &self.child
    }
}

impl DerefMut for Reaped {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.child
    }
}

impl Drop for Reaped {
    fn drop(&mut self) {
        let pid = self.child.id();
        if let Err(error) = self.kill_and_wait() {
            // Tests cannot return errors from `Drop`; stderr is the honest
            // channel. Naming the pid makes the survivor findable by hand.
            eprintln!(
                "test teardown could not reap child process {pid}: {error} — it may still be \
                 running"
            );
        }
    }
}

/// Guards on the guard. These keep every path of [`Reaped`] exercised in every
/// test binary that includes this module, and they spawn `/bin/sh`, not a
/// server, so they cost milliseconds.
#[cfg(test)]
mod reaping {
    use super::Reaped;
    use std::io;
    use std::process::{Command, Stdio};

    /// A child that is still running when the guard is asked to tear it down
    /// is killed and reaped, and asking twice is not an error — the second
    /// call sees an already-exited process, which is the same condition
    /// `Drop` sees after an explicit teardown.
    #[test]
    fn a_running_child_is_killed_and_reaped_and_the_teardown_is_idempotent() -> io::Result<()> {
        let mut guard = Reaped::spawn(Command::new("/bin/sh").args(["-c", "sleep 300"]))?;
        // Through the deref, exactly as the tests use it.
        assert!(
            guard.try_wait()?.is_none(),
            "a freshly spawned `sleep 300` must still be running"
        );

        guard.kill_and_wait()?;

        let Some(status) = guard.try_wait()? else {
            return Err(io::Error::other(
                "a reaped child must report an exit status",
            ));
        };
        assert!(
            !status.success(),
            "a killed child must not report a successful exit: {status}"
        );

        // The second teardown is the one `Drop` performs after an explicit
        // `kill_and_wait`; it must not report a failure.
        guard.kill_and_wait()
    }

    /// A child that exited on its own is reaped without the teardown treating
    /// the absent process as a failure.
    #[test]
    fn a_child_that_already_exited_is_not_a_teardown_failure() -> io::Result<()> {
        let mut guard = Reaped::spawn(Command::new("/bin/sh").args(["-c", "exit 0"]))?;
        let status = guard.wait()?;
        assert!(status.success(), "`exit 0` must exit successfully");
        guard.kill_and_wait()
    }

    /// The guard's own drain collects both streams, so a test that needs the
    /// child's output never has to hand the `Child` out by value to get it.
    #[test]
    fn the_drain_collects_both_streams_and_the_status() -> io::Result<()> {
        let mut guard = Reaped::spawn(
            Command::new("/bin/sh")
                .args(["-c", "echo to-stdout; echo to-stderr >&2; exit 3"])
                .stdout(Stdio::piped())
                .stderr(Stdio::piped()),
        )?;
        let output = guard.wait_with_output()?;
        assert_eq!(
            output.status.code(),
            Some(3),
            "the drain must carry the child's own exit code"
        );
        assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "to-stdout");
        assert_eq!(String::from_utf8_lossy(&output.stderr).trim(), "to-stderr");
        Ok(())
    }
}