Skip to content
This repository was archived by the owner on Dec 29, 2021. It is now read-only.

Commit 58f445a

Browse files
committed
feat(cmd): Augment process::Command
This is an experiment in trying to use extension traits rather than wrapping `process::Command`. This both makes it more extensible (can interop with other crates) and able to be adapted to other "Command" crates like `duct`. `cli_test_dir` has something like `CommandStdInExt` called `CommandExt`. Differences include: - Scoped name since traits generally are pulled out of any namespace they are in. - Preserves the command and `stdin` to for richer error reporting.
1 parent 712c738 commit 58f445a

File tree

2 files changed

+370
-0
lines changed

2 files changed

+370
-0
lines changed

src/cmd.rs

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
use std::ffi;
2+
use std::fmt;
3+
use std::io::Write;
4+
use std::io;
5+
use std::process;
6+
use std::str;
7+
8+
use failure;
9+
10+
/// Extend `Command` with helpers for running the current crate's binaries.
11+
pub trait CommandCargoExt {
12+
/// Create a `Command` to run the crate's main binary.
13+
///
14+
/// # Examples
15+
///
16+
/// ```rust
17+
/// extern crate assert_cli;
18+
/// use std::process::Command;
19+
/// use assert_cli::cmd::*;
20+
///
21+
/// Command::main_binary()
22+
/// .output()
23+
/// .unwrap();
24+
/// ```
25+
fn main_binary() -> Self;
26+
27+
/// Create a `Command` Run a specific binary of the current crate.
28+
///
29+
/// # Examples
30+
///
31+
/// ```rust
32+
/// extern crate assert_cli;
33+
/// use std::process::Command;
34+
/// use assert_cli::cmd::*;
35+
///
36+
/// Command::cargo_binary("assert_fixture")
37+
/// .output()
38+
/// .unwrap();
39+
/// ```
40+
fn cargo_binary<S: AsRef<ffi::OsStr>>(name: S) -> Self;
41+
}
42+
43+
impl CommandCargoExt for process::Command {
44+
fn main_binary() -> Self {
45+
let mut cmd = process::Command::new("carg");
46+
cmd.arg("run").arg("--quit").arg("--");
47+
cmd
48+
}
49+
50+
fn cargo_binary<S: AsRef<ffi::OsStr>>(name: S) -> Self {
51+
let mut cmd = process::Command::new("carg");
52+
cmd.arg("run")
53+
.arg("--quit")
54+
.arg("--bin")
55+
.arg(name.as_ref())
56+
.arg("--");
57+
cmd
58+
}
59+
}
60+
61+
/// Extend `Command` with a helper to pass a buffer to `stdin`
62+
pub trait CommandStdInExt {
63+
/// Write `buffer` to `stdin` when the command is run.
64+
///
65+
/// # Examples
66+
///
67+
/// ```rust
68+
/// extern crate assert_cli;
69+
/// use std::process::Command;
70+
/// use assert_cli::cmd::*;
71+
///
72+
/// Command::new("cat")
73+
/// .with_stdin("42")
74+
/// .unwrap();
75+
/// ```
76+
fn with_stdin<S>(self, buffer: S) -> StdInCommand
77+
where
78+
S: Into<Vec<u8>>;
79+
}
80+
81+
impl CommandStdInExt for process::Command {
82+
fn with_stdin<S>(self, buffer: S) -> StdInCommand
83+
where
84+
S: Into<Vec<u8>>,
85+
{
86+
StdInCommand {
87+
cmd: self,
88+
stdin: buffer.into(),
89+
}
90+
}
91+
}
92+
93+
/// `std::process::Command` with a `stdin` buffer.
94+
pub struct StdInCommand {
95+
cmd: process::Command,
96+
stdin: Vec<u8>,
97+
}
98+
99+
impl StdInCommand {
100+
/// Executes the command as a child process, waiting for it to finish and collecting all of its
101+
/// output.
102+
///
103+
/// By default, stdout and stderr are captured (and used to provide the resulting output).
104+
/// Stdin is not inherited from the parent and any attempt by the child process to read from
105+
/// the stdin stream will result in the stream immediately closing.
106+
///
107+
/// *(mirrors `std::process::Command::output`**
108+
pub fn output(&mut self) -> io::Result<process::Output> {
109+
self.spawn()?.wait_with_output()
110+
}
111+
112+
/// Executes the command as a child process, returning a handle to it.
113+
///
114+
/// By default, stdin, stdout and stderr are inherited from the parent.
115+
///
116+
/// *(mirrors `std::process::Command::spawn`**
117+
fn spawn(&mut self) -> io::Result<process::Child> {
118+
// stdout/stderr should only be piped for `output` according to `process::Command::new`.
119+
self.cmd.stdin(process::Stdio::piped());
120+
self.cmd.stdout(process::Stdio::piped());
121+
self.cmd.stderr(process::Stdio::piped());
122+
123+
let mut spawned = self.cmd.spawn()?;
124+
125+
spawned
126+
.stdin
127+
.as_mut()
128+
.expect("Couldn't get mut ref to command stdin")
129+
.write_all(&self.stdin)?;
130+
Ok(spawned)
131+
}
132+
}
133+
134+
/// `std::process::Output` represented as a `Result`.
135+
pub type OutputResult = Result<process::Output, OutputError>;
136+
137+
/// Extends `std::process::Output` with methods to to convert it to an `OutputResult`.
138+
pub trait OutputOkExt
139+
where
140+
Self: ::std::marker::Sized,
141+
{
142+
/// Convert an `std::process::Output` into an `OutputResult`.
143+
///
144+
/// # Examples
145+
///
146+
/// ```rust,ignore
147+
/// extern crate assert_cli;
148+
/// use std::process::Command;
149+
/// use assert_cli::cmd::*;
150+
///
151+
/// Command::new("echo")
152+
/// .args(&["42"])
153+
/// .output()
154+
/// .ok()
155+
/// .unwrap();
156+
/// ```
157+
fn ok(self) -> OutputResult;
158+
159+
/// Unwrap a `std::process::Output` but with a prettier message than `.ok().unwrap()`.
160+
///
161+
/// # Examples
162+
///
163+
/// ```rust,ignore
164+
/// extern crate assert_cli;
165+
/// use std::process::Command;
166+
/// use assert_cli::cmd::*;
167+
///
168+
/// Command::new("echo")
169+
/// .args(&["42"])
170+
/// .output()
171+
/// .unwrap();
172+
/// ```
173+
fn unwrap(self) {
174+
if let Err(err) = self.ok() {
175+
panic!("{}", err);
176+
}
177+
}
178+
}
179+
180+
impl OutputOkExt for process::Output {
181+
/// Convert an `std::process::Output` into an `OutputResult`.
182+
///
183+
/// # Examples
184+
///
185+
/// ```rust,ignore
186+
/// extern crate assert_cli;
187+
/// use std::process::Command;
188+
/// use assert_cli::cmd::*;
189+
///
190+
/// Command::new("echo")
191+
/// .args(&["42"])
192+
/// .output()
193+
/// .ok()
194+
/// .unwrap();
195+
/// ```
196+
fn ok(self) -> OutputResult {
197+
if self.status.success() {
198+
Ok(self)
199+
} else {
200+
let error = OutputError::new(self);
201+
Err(error)
202+
}
203+
}
204+
}
205+
206+
impl<'c> OutputOkExt for &'c mut process::Command {
207+
/// Convert an `std::process::Command` into an `OutputResult`.
208+
///
209+
/// # Examples
210+
///
211+
/// ```rust,ignore
212+
/// extern crate assert_cli;
213+
/// use std::process::Command;
214+
/// use assert_cli::cmd::*;
215+
///
216+
/// Command::new("echo")
217+
/// .args(&["42"])
218+
/// .ok()
219+
/// .unwrap();
220+
/// ```
221+
fn ok(self) -> OutputResult {
222+
let output = self.output().map_err(|e| OutputError::with_cause(e))?;
223+
if output.status.success() {
224+
Ok(output)
225+
} else {
226+
let error = OutputError::new(output).set_cmd(format!("{:?}", self));
227+
Err(error)
228+
}
229+
}
230+
}
231+
232+
impl<'c> OutputOkExt for &'c mut StdInCommand {
233+
/// Convert an `std::process::Command` into an `OutputResult`.
234+
///
235+
/// # Examples
236+
///
237+
/// ```rust,ignore
238+
/// extern crate assert_cli;
239+
/// use std::process::Command;
240+
/// use assert_cli::cmd::*;
241+
///
242+
/// Command::new("cat")
243+
/// .with_stdin("42")
244+
/// .ok()
245+
/// .unwrap();
246+
/// ```
247+
fn ok(self) -> OutputResult {
248+
let output = self.output().map_err(|e| OutputError::with_cause(e))?;
249+
if output.status.success() {
250+
Ok(output)
251+
} else {
252+
let error = OutputError::new(output)
253+
.set_cmd(format!("{:?}", self.cmd))
254+
.set_stdin(self.stdin.clone());
255+
Err(error)
256+
}
257+
}
258+
}
259+
260+
#[derive(Fail, Debug)]
261+
struct Output {
262+
output: process::Output,
263+
}
264+
265+
impl fmt::Display for Output {
266+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
267+
if let Some(code) = self.output.status.code() {
268+
writeln!(f, "code={}", code)?;
269+
} else {
270+
writeln!(f, "code=<interrupted>")?;
271+
}
272+
if let Ok(stdout) = str::from_utf8(&self.output.stdout) {
273+
writeln!(f, "stdout=```{}```", stdout)?;
274+
} else {
275+
writeln!(f, "stdout=```{:?}```", self.output.stdout)?;
276+
}
277+
if let Ok(stderr) = str::from_utf8(&self.output.stderr) {
278+
writeln!(f, "stderr=```{}```", stderr)?;
279+
} else {
280+
writeln!(f, "stderr=```{:?}```", self.output.stderr)?;
281+
}
282+
283+
Ok(())
284+
}
285+
}
286+
287+
#[derive(Debug)]
288+
enum OutputCause {
289+
Expected(Output),
290+
Unexpected(failure::Error),
291+
}
292+
293+
impl fmt::Display for OutputCause {
294+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
295+
match *self {
296+
OutputCause::Expected(ref e) => write!(f, "{}", e),
297+
OutputCause::Unexpected(ref e) => write!(f, "{}", e),
298+
}
299+
}
300+
}
301+
302+
/// `std::process::Output` as a `Fail`.
303+
#[derive(Fail, Debug)]
304+
pub struct OutputError {
305+
cmd: Option<String>,
306+
stdin: Option<Vec<u8>>,
307+
cause: OutputCause,
308+
}
309+
310+
impl OutputError {
311+
/// Convert `std::process::Output` into a `Fail`.
312+
pub fn new(output: process::Output) -> Self {
313+
Self {
314+
cmd: None,
315+
stdin: None,
316+
cause: OutputCause::Expected(Output { output }),
317+
}
318+
}
319+
320+
/// For errors that happen in creating a `std::process::Output`.
321+
pub fn with_cause<E>(cause: E) -> Self
322+
where
323+
E: Into<failure::Error>,
324+
{
325+
Self {
326+
cmd: None,
327+
stdin: None,
328+
cause: OutputCause::Unexpected(cause.into()),
329+
}
330+
}
331+
332+
/// Add the command line for additional context.
333+
pub fn set_cmd(mut self, cmd: String) -> Self {
334+
self.cmd = Some(cmd);
335+
self
336+
}
337+
338+
/// Add the `stdn` for additional context.
339+
pub fn set_stdin(mut self, stdin: Vec<u8>) -> Self {
340+
self.stdin = Some(stdin);
341+
self
342+
}
343+
344+
/// Access the contained `std::process::Output`.
345+
pub fn as_output(&self) -> Option<&process::Output> {
346+
match self.cause {
347+
OutputCause::Expected(ref e) => Some(&e.output),
348+
OutputCause::Unexpected(_) => None,
349+
}
350+
}
351+
}
352+
353+
impl fmt::Display for OutputError {
354+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
355+
if let Some(ref cmd) = self.cmd {
356+
writeln!(f, "command=`{}`", cmd)?;
357+
}
358+
if let Some(ref stdin) = self.stdin {
359+
if let Ok(stdin) = str::from_utf8(&stdin) {
360+
writeln!(f, "stdin=```{}```", stdin)?;
361+
} else {
362+
writeln!(f, "stdin=```{:?}```", stdin)?;
363+
}
364+
}
365+
write!(f, "{}", self.cause)
366+
}
367+
}

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ mod assert;
137137
mod diff;
138138
mod output;
139139

140+
/// `std::process::Command` extensions.
141+
pub mod cmd;
142+
140143
pub use assert::Assert;
141144
pub use assert::OutputAssertionBuilder;
142145
/// Environment is a re-export of the Environment crate

0 commit comments

Comments
 (0)