1use 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}