Skip to main content

cli_assert/
command.rs

1use crate::utils::{bytes_to_str, PathExt};
2use std::borrow::Cow;
3use std::ffi::{OsStr, OsString};
4use std::io::Write;
5use std::path::{Path, PathBuf};
6use std::process::{Child, ExitStatus};
7
8pub struct Command {
9  /// Path to the program to be executed.
10  program: OsString,
11  /// Arguments passed to spawned program.
12  program_args: Vec<OsString>,
13  /// Path to the directory this command is executed in.
14  current_dir: PathBuf,
15  /// Child process.
16  child: Option<Child>,
17  /// The raw content to be written to stdin of the child process.
18  stdin: Option<Vec<u8>>,
19  /// The raw content of the stdout of the child process.
20  stdout: Vec<u8>,
21  /// The raw content of the stderr of the child process.
22  stderr: Vec<u8>,
23  /// Execution status of the child process.
24  status: ExitStatus,
25  /// Flag indicating if the command is expected to complete successfully.
26  expected_success: Option<bool>,
27  /// Flag indicating if the command is expected to fail.
28  expected_failure: Option<bool>,
29  /// Expected status code.
30  expected_status: Option<i32>,
31  /// Expected stdout.
32  expected_stdout: Option<Vec<u8>>,
33  /// Expected stderr.
34  expected_stderr: Option<Vec<u8>>,
35}
36
37impl Command {
38  pub fn new(program: impl AsRef<OsStr>, caller_file: impl AsRef<str>, manifest_dir: impl AsRef<str>) -> Self {
39    let manifest_path = Path::new(manifest_dir.as_ref());
40    let caller_path = Path::new(caller_file.as_ref())
41      .parent()
42      .expect("failed to retrieve parent directory for caller file");
43    let current_dir = match manifest_path.rem(caller_path) {
44      None => caller_path.into(),
45      Some(path_buf) => {
46        if path_buf.components().count() == 0 {
47          PathBuf::from(".")
48        } else {
49          path_buf
50        }
51      }
52    };
53    Self {
54      program: program.as_ref().into(),
55      program_args: vec![],
56      current_dir,
57      child: None,
58      stdin: None,
59      stdout: vec![],
60      stderr: vec![],
61      status: ExitStatus::default(),
62      expected_success: None,
63      expected_failure: None,
64      expected_status: None,
65      expected_stdout: None,
66      expected_stderr: None,
67    }
68  }
69
70  pub fn current_dir(mut self, dir: impl AsRef<Path>) -> Self {
71    self.current_dir = dir.as_ref().into();
72    self
73  }
74
75  pub fn arg(mut self, arg: impl AsRef<OsStr>) -> Self {
76    self.program_args.push(arg.as_ref().into());
77    self
78  }
79
80  pub fn success(mut self) -> Self {
81    self.expected_success = Some(true);
82    self.expected_failure = None;
83    self
84  }
85
86  pub fn failure(mut self) -> Self {
87    self.expected_failure = Some(true);
88    self.expected_success = None;
89    self
90  }
91
92  pub fn code(mut self, code: i32) -> Self {
93    self.expected_status = Some(code);
94    self
95  }
96
97  pub fn stdin(mut self, bytes: impl AsRef<[u8]>) -> Self {
98    self.stdin = Some(bytes.as_ref().to_vec());
99    self
100  }
101
102  pub fn stdout(mut self, bytes: impl AsRef<[u8]>) -> Self {
103    self.expected_stdout = Some(bytes.as_ref().to_vec());
104    self
105  }
106
107  pub fn stderr(mut self, bytes: impl AsRef<[u8]>) -> Self {
108    self.expected_stderr = Some(bytes.as_ref().to_vec());
109    self
110  }
111
112  pub fn spawn(&mut self) {
113    if self.child.is_some() {
114      panic!("command is already spawned");
115    }
116    let mut command = std::process::Command::new(self.program.clone());
117    let mut child = command
118      .args(self.program_args.clone())
119      .current_dir(self.current_dir.clone())
120      .stdin(std::process::Stdio::piped())
121      .stdout(std::process::Stdio::piped())
122      .stderr(std::process::Stdio::piped())
123      .spawn()
124      .expect("failed to spawn requested command");
125    if let Some(bytes) = &self.stdin {
126      let mut stdin = child.stdin.take().expect("failed to obtain child process stdin");
127      stdin.write_all(bytes).expect("failed to write child process stdin");
128    }
129    self.child = Some(child);
130  }
131
132  pub fn wait(&mut self) {
133    let child = self.child.take().expect("command is not spawned");
134    let output = child.wait_with_output().expect("failed to obtain child process output");
135    self.stdout = output.stdout;
136    self.stderr = output.stderr;
137    self.status = output.status;
138    self.assert();
139  }
140
141  pub fn execute(&mut self) {
142    self.spawn();
143    self.wait();
144  }
145
146  pub fn stop(&mut self) {
147    if let Some(child) = &mut self.child {
148      child.kill().expect("failed to force a child process to stop");
149    } else {
150      panic!("command is not spawned");
151    }
152  }
153
154  pub fn get_program(&self) -> &OsStr {
155    &self.program
156  }
157
158  pub fn get_current_dir(&self) -> &Path {
159    &self.current_dir
160  }
161
162  pub fn get_stdout(&'_ self) -> Cow<'_, str> {
163    String::from_utf8_lossy(&self.stdout)
164  }
165
166  pub fn get_stderr(&'_ self) -> Cow<'_, str> {
167    String::from_utf8_lossy(&self.stderr)
168  }
169
170  pub fn get_stdout_raw(&self) -> &[u8] {
171    &self.stdout
172  }
173
174  pub fn get_stderr_raw(&self) -> &[u8] {
175    &self.stderr
176  }
177
178  pub fn get_status(&self) -> ExitStatus {
179    self.status
180  }
181
182  /// Checks all assertions.
183  fn assert(&self) {
184    if let Some(true) = self.expected_success {
185      if !self.status.success() {
186        panic!("expected success");
187      }
188    }
189    if let Some(true) = self.expected_failure {
190      if self.status.success() {
191        panic!("expected failure");
192      }
193    }
194    if let Some(expected) = self.expected_status {
195      let actual = self.status.code().expect("failed to retrieve status code");
196      if actual != expected {
197        println!("\nexpected status code: {}\n  actual status code: {}", expected, actual);
198        panic!("unexpected status");
199      }
200    }
201    if let Some(expected) = &self.expected_stdout {
202      let actual = self.get_stdout_raw();
203      if actual != expected {
204        println!("\nexpected stdout: {:?}\n  actual stdout: {:?}", expected, actual);
205        println!("\n\nexpected stdout: {}\n  actual stdout: {}", bytes_to_str(expected), bytes_to_str(expected));
206        panic!("unexpected stdout");
207      }
208    }
209    if let Some(expected) = &self.expected_stderr {
210      let actual = self.get_stderr_raw();
211      if actual != expected {
212        println!("\nexpected stderr: {:?}\n  actual stderr: {:?}", expected, actual);
213        println!("\n\nexpected stderr: {}\n  actual stderr: {}", bytes_to_str(expected), bytes_to_str(actual));
214        panic!("unexpected stderr");
215      }
216    }
217  }
218}