asimov-runner 25.1.0

ASIMOV Software Development Kit (SDK) for Rust
Documentation
// This is free and unencumbered software released into the public domain.

use crate::{Command, ExecutorError, ExecutorResult, Input};
use std::{
    ffi::OsStr,
    io::{Cursor, ErrorKind},
    process::Stdio,
};
use tokio::{io::AsyncReadExt, process::Child};

#[derive(Debug)]
pub struct Executor(Command);

impl Executor {
    pub fn new(program: impl AsRef<OsStr>) -> Self {
        let libexec_path = asimov_env::paths::asimov_root()
            .join("libexec")
            .join(program.as_ref());

        let mut command = if libexec_path.exists() {
            Command::new(libexec_path)
        } else {
            Command::new(program)
        };

        command.env("NO_COLOR", "1"); // See: https://no-color.org
        command.stdin(Stdio::null());
        command.stdout(Stdio::null());
        command.stderr(Stdio::null());
        command.kill_on_drop(true);
        Self(command)
    }

    pub fn command(&mut self) -> &mut Command {
        &mut self.0
    }

    pub fn ignore_stdin(&mut self) {
        self.0.stdin(Stdio::null());
    }

    pub fn ignore_stdout(&mut self) {
        self.0.stdout(Stdio::null());
    }

    pub fn ignore_stderr(&mut self) {
        self.0.stderr(Stdio::null());
    }

    pub fn capture_stdout(&mut self) {
        self.0.stdout(Stdio::piped());
    }

    pub fn capture_stderr(&mut self) {
        self.0.stderr(Stdio::piped());
    }

    pub async fn execute(&mut self) -> ExecutorResult {
        let process = self.spawn().await?;
        self.wait(process).await
    }

    pub async fn execute_with_input(&mut self, input: &mut Input) -> ExecutorResult {
        let mut process = self.spawn().await?;
        match input {
            Input::Ignored => {},
            Input::AsyncRead(reader) => {
                let mut stdin = process.stdin.take().expect("should capture stdin");
                tokio::io::copy(&mut *reader, &mut stdin).await?;
            },
        }
        self.wait(process).await
    }

    pub async fn spawn(&mut self) -> Result<Child, ExecutorError> {
        match self.0.spawn() {
            Ok(process) => Ok(process),
            Err(err) if err.kind() == ErrorKind::NotFound => {
                let program = self.0.as_std().get_program().to_owned();
                return Err(ExecutorError::MissingProgram(program));
            },
            Err(err) => return Err(ExecutorError::SpawnFailure(err)),
        }
    }

    pub async fn wait(&mut self, process: Child) -> ExecutorResult {
        let output = process.wait_with_output().await?;

        #[cfg(feature = "tracing")]
        tracing::trace!("The command exited with: {}", output.status);

        if !output.status.success() {
            return Err(output.into());
        }

        Ok(Cursor::new(output.stdout))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_success() {
        let mut runner = Executor::new("curl");
        runner.command().arg("https://www.google.com");
        let result = runner.execute().await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_missing_program() {
        let mut runner = Executor::new("this-command-does-not-exist");
        let result = runner.execute().await;
        assert!(matches!(result, Err(ExecutorError::MissingProgram(_))));
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn test_spawn_failure() {
        let mut runner = Executor::new("/dev/null");
        let result = runner.execute().await;
        assert!(matches!(result, Err(ExecutorError::SpawnFailure(_))));
    }

    #[tokio::test]
    async fn test_unexpected_failure() {
        let mut runner = Executor::new("curl");
        let result = runner.execute().await;
        assert!(matches!(
            result,
            Err(ExecutorError::UnexpectedFailure(_, _))
        ));
    }
}