asimov_runner/
runner.rs

1// This is free and unencumbered software released into the public domain.
2
3use crate::{Command, SysexitsError};
4use core::fmt;
5use std::{
6    ffi::{OsStr, OsString},
7    io::{Cursor, ErrorKind},
8    process::{ExitStatus, Output, Stdio},
9};
10use tokio::process::Child;
11
12#[derive(Debug)]
13pub struct Runner(Command);
14
15impl Runner {
16    pub fn new(program: impl AsRef<OsStr>) -> Self {
17        let mut command = Command::new(program);
18        command.env("NO_COLOR", "1");
19        command.stdin(Stdio::null());
20        command.stdout(Stdio::null());
21        command.stderr(Stdio::null());
22        command.kill_on_drop(true);
23        Self(command)
24    }
25
26    pub fn command(&mut self) -> &mut Command {
27        &mut self.0
28    }
29
30    pub fn ignore_stdin(&mut self) {
31        self.0.stdin(Stdio::null());
32    }
33
34    pub fn ignore_stdout(&mut self) {
35        self.0.stdout(Stdio::null());
36    }
37
38    pub fn ignore_stderr(&mut self) {
39        self.0.stderr(Stdio::null());
40    }
41
42    pub fn capture_stdout(&mut self) {
43        self.0.stdout(Stdio::piped());
44    }
45
46    pub fn capture_stderr(&mut self) {
47        self.0.stderr(Stdio::piped());
48    }
49
50    pub async fn spawn(&mut self) -> Result<Child, RunnerError> {
51        match self.0.spawn() {
52            Ok(process) => Ok(process),
53            Err(err) if err.kind() == ErrorKind::NotFound => {
54                let program = self.0.as_std().get_program().to_owned();
55                return Err(RunnerError::MissingProgram(program));
56            }
57            Err(err) => return Err(RunnerError::SpawnFailure(err)),
58        }
59    }
60
61    pub async fn wait(&mut self, process: Child) -> RunnerResult {
62        let output = process.wait_with_output().await?;
63
64        #[cfg(feature = "tracing")]
65        tracing::trace!("The command exited with: {}", output.status);
66
67        if !output.status.success() {
68            return Err(output.into());
69        }
70
71        Ok(Cursor::new(output.stdout))
72    }
73
74    pub async fn execute(&mut self) -> RunnerResult {
75        let process = self.spawn().await?;
76        self.wait(process).await
77    }
78}
79
80pub type RunnerResult = std::result::Result<Cursor<Vec<u8>>, RunnerError>;
81
82#[derive(Debug)]
83pub enum RunnerError {
84    MissingProgram(OsString),
85    SpawnFailure(std::io::Error),
86    Failure(SysexitsError, Option<String>),
87    UnexpectedFailure(Option<i32>, Option<String>),
88    UnexpectedOther(std::io::Error),
89}
90
91impl core::error::Error for RunnerError {}
92
93impl fmt::Display for RunnerError {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self {
96            Self::MissingProgram(program) => {
97                write!(f, "Missing program: {}", program.to_string_lossy())
98            }
99            Self::SpawnFailure(err) => write!(f, "Failed to spawn process: {}", err),
100            Self::Failure(error, stderr) => {
101                write!(
102                    f,
103                    "Command failed with exit code {}",
104                    error.code().unwrap_or(-1),
105                )?;
106                if let Some(stderr) = stderr {
107                    write!(f, "\n{}", stderr)?;
108                }
109                Ok(())
110            }
111            Self::UnexpectedFailure(code, stderr) => {
112                write!(
113                    f,
114                    "Command failed with unexpected exit code: {}",
115                    code.unwrap_or(-1)
116                )?;
117                if let Some(stderr) = stderr {
118                    write!(f, "\n{}", stderr)?;
119                }
120                Ok(())
121            }
122            Self::UnexpectedOther(err) => write!(f, "Unexpected error: {}", err),
123        }
124    }
125}
126
127impl From<Output> for RunnerError {
128    fn from(output: Output) -> Self {
129        let stderr = String::from_utf8(output.stderr).ok();
130        match SysexitsError::try_from(output.status) {
131            Ok(error) => Self::Failure(error, stderr),
132            Err(code) => Self::UnexpectedFailure(code, stderr),
133        }
134    }
135}
136
137impl From<ExitStatus> for RunnerError {
138    fn from(status: ExitStatus) -> Self {
139        match SysexitsError::try_from(status) {
140            Ok(error) => Self::Failure(error, None),
141            Err(code) => Self::UnexpectedFailure(code, None),
142        }
143    }
144}
145
146impl From<std::io::Error> for RunnerError {
147    fn from(error: std::io::Error) -> Self {
148        Self::UnexpectedOther(error)
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[tokio::test]
157    async fn test_success() {
158        let mut runner = Runner::new("curl");
159        runner.command().arg("http://neverssl.com");
160        let result = runner.execute().await;
161        assert!(result.is_ok());
162    }
163
164    #[tokio::test]
165    async fn test_missing_program() {
166        let mut runner = Runner::new("this-command-does-not-exist");
167        let result = runner.execute().await;
168        assert!(matches!(result, Err(RunnerError::MissingProgram(_))));
169    }
170
171    #[tokio::test]
172    async fn test_spawn_failure() {
173        let mut runner = Runner::new("/dev/null");
174        let result = runner.execute().await;
175        assert!(matches!(result, Err(RunnerError::SpawnFailure(_))));
176    }
177
178    #[tokio::test]
179    async fn test_unexpected_failure() {
180        let mut runner = Runner::new("curl");
181        let result = runner.execute().await;
182        assert!(matches!(result, Err(RunnerError::UnexpectedFailure(_, _))));
183    }
184}