Last active
June 18, 2018 15:22
-
-
Save volks73/2e15476d952b6d03e8ec6c6a64884866 to your computer and use it in GitHub Desktop.
Extending process::Command using the assert_cmd crate to write a sequence of input to stdin over time
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extern crate assert_cmd; | |
extern crate failure; | |
extern crate predicates; | |
use assert_cmd::{Assert, OutputAssertExt}; | |
use predicates::Predicate; | |
use std::ffi::OsStr; | |
use std::fmt; | |
use std::io; | |
use std::process::{self, ChildStdin}; | |
use std::thread; | |
use std::time::Duration; | |
/// Write to `stdin` of a `Command`. | |
/// | |
/// This is different from the `CommandStdInExt` trait in that it allows writing a sequence of | |
/// values to `stdin` over time. It also exposes the low-level "handle" to the stdin pipe via the | |
/// `StdinWriter` trait. for greater flexibility if needed. | |
pub trait CommandInputExt { | |
fn input<W>(self, writer: W) -> InputCommand | |
where W: Into<Box<StdinWriter>>; | |
} | |
/// Enables a slightly cleaner usage of the `main_binary`, `cargo_binary`, and `cargo_example` | |
/// constructors of the `Command` extension traits when different arguments are needed. | |
/// | |
/// For example, without this trait: | |
/// | |
/// ```rust | |
/// let mut c = Command::main_binary().unwrap(); | |
/// c.args(&["-v"]); | |
/// c.assert().success(); | |
/// ``` | |
/// | |
/// but with this trait, the above becomes: | |
/// | |
/// ```rust | |
/// Command::main_binary() | |
/// .unwrap() | |
/// .with_args(&["-v"]) | |
/// .assert() | |
/// .success(); | |
/// ``` | |
/// | |
/// which can be simplified even further with this trait implemented for `Result` and unwrapping | |
/// within the trait implementation. Thus, | |
/// | |
/// ```rust | |
/// Command::main_binary() | |
/// .with_args(&["-v"]) | |
/// .assert() | |
/// .success() | |
/// ``` | |
/// | |
/// which is arguably more readable and less messy/noisy. | |
pub trait CommandWithArgs { | |
fn with_args<I, S>(self, args: I) -> process::Command | |
where I: IntoIterator<Item = S>, | |
S: AsRef<OsStr>; | |
} | |
/// Writes to `stdin`. | |
/// | |
/// Exposes the low-level `stdin` pipe for greater flexibility and customization if needed. | |
pub trait StdinWriter { | |
fn write(&self, stdin: &mut ChildStdin) -> Result<(), io::Error>; | |
} | |
// Adds the `input` method to `process::Command`. | |
impl CommandInputExt for process::Command { | |
fn input<W>(self, writer: W) -> InputCommand | |
where W: Into<Box<StdinWriter>> | |
{ | |
InputCommand { | |
cmd: self, | |
writers: vec![writer.into()], | |
} | |
} | |
} | |
// This implementation enables the chaining of `input` and `then` methods. Without this | |
// implementation, the `InputCommand` functions identically to `StdInCommand`, where only a single | |
// value can be written. | |
impl CommandInputExt for InputCommand { | |
fn input<W>(mut self, writer: W) -> InputCommand | |
where W: Into<Box<StdinWriter>> | |
{ | |
self.writers.push(writer.into()); | |
self | |
} | |
} | |
// The `args` method of `process::Command` uses a mutable reference, but the `assert`, `input`, | |
// `then`, and `with_stdin` methods that are added to the `process::Command` from the assert_cmd | |
// crate consume the `process::Command`. So, when a arguments need to be added this implementation | |
// cleans up the usage. | |
impl CommandWithArgs for process::Command { | |
fn with_args<I, S>(mut self, args: I) -> process::Command | |
where I: IntoIterator<Item = S>, | |
S: AsRef<OsStr> | |
{ | |
self.args(args); | |
self | |
} | |
} | |
// Most of the time, including example usage in the [assert_cmd]() crate, an `unwrap` is used | |
// immediately after the `main_binary`, `cargo_binary`, and `example_binary` constructors. This | |
// cleans up using arguments with the command and "hides" the unwrap. | |
impl CommandWithArgs for Result<process::Command, failure::Error> { | |
fn with_args<I, S>(self, args: I) -> process::Command | |
where I: IntoIterator<Item = S>, | |
S: AsRef<OsStr> | |
{ | |
self.unwrap().with_args(args) | |
} | |
} | |
impl<F> StdinWriter for F | |
where F: Fn(&mut ChildStdin) -> Result<(), io::Error>, | |
{ | |
fn write(&self, stdin: &mut ChildStdin) -> Result<(), io::Error> { | |
self(stdin) | |
} | |
} | |
impl<P> From<P> for Box<StdinWriter> | |
where P: StdinWriter + 'static, | |
{ | |
fn from(p: P) -> Self { | |
Box::new(p) | |
} | |
} | |
impl From<Vec<u8>> for Box<StdinWriter> { | |
fn from(contents: Vec<u8>) -> Self { | |
Box::new(move |s: &mut ChildStdin| s.write_all(&contents)) | |
} | |
} | |
impl<'a> From<&'a [u8]> for Box<StdinWriter> { | |
fn from(contents: &[u8]) -> Self { | |
Self::from(contents.to_owned()) | |
} | |
} | |
impl<'a> From<&'a str> for Box<StdinWriter> { | |
fn from(contents: &str) -> Self { | |
let c = contents.to_owned(); | |
Box::new(move |s: &mut ChildStdin| s.write_all(c.as_bytes())) | |
} | |
} | |
/// Command that carries a sequence of writes to `stdin`. | |
/// | |
/// Create an `InputCommand` through the `InputCommandExt` trait. The implementation and API | |
/// mirrors the `StdInCommand` and `StdInCommandExt` implementation and API with minor differences | |
/// to use the `StdinWriter` trait and a sequence of inputs. | |
/// | |
/// # Example | |
/// | |
/// This is a trival example because this can also be accomplished using the `StdInCommand` type and | |
/// `StdInCommandExt` trait with the `with_stdin`. If just needing to send a single value of bytes | |
/// to a command, then use the `with_stdin` method. However, this demonstrates writing to `stdin` | |
/// multiple times. | |
/// | |
/// ```rust | |
/// Command::new("cat") | |
/// .input("Hello") | |
/// .input(" World") | |
/// .assert() | |
/// .success() | |
/// .stdout(&predicates::ord::eq("Hello World")); | |
/// ``` | |
pub struct InputCommand { | |
cmd: process::Command, | |
writers: Vec<Box<StdinWriter>>, | |
} | |
impl InputCommand { | |
/// Executes the command as a child process, waiting for it to finish and collecting all of its | |
/// output. | |
/// | |
/// By default, stdout and stderr are captured (and used to provide the resulting output). | |
/// Stdin is not inherited from the parent and any attempt by the child process to read from | |
/// the stdin stream will result in the stream immediately closing. | |
/// | |
/// Note, this mirrors the InputCommand::output and std::process::Command::output methods. | |
pub fn output(&mut self) -> io::Result<process::Output> { | |
self.spawn()?.wait_with_output() | |
} | |
/// A wrapper around the `input` method. | |
/// | |
/// This is useful for creating a more fluent and readable or command, where something needs to | |
/// happen with `stdin` but it is not actually input. | |
/// | |
/// # Example | |
/// | |
/// ```rust | |
/// Command::new("cat") | |
/// .input("Hello") | |
/// // Wait for a second, which is not actually writing any "input" to stdin, so using | |
/// // the `input` method would be slightly confusing. | |
/// .then(|_| { | |
/// thread::sleep(Duration::from_secs(1)); | |
/// }) | |
/// .input( "World") | |
/// .assert() | |
/// .success() | |
/// .stdout(&predicates::ord::eq("Hello World")); | |
/// ``` | |
pub fn then<W>(self, writer: W) -> Self | |
where W: Into<Box<StdinWriter>> | |
{ | |
self.input(writer) | |
} | |
/// Executes the command as a child process, returning a handle to it. | |
/// | |
/// By default, stdin, stdout, and stderr are inherited from the parent. This will iterate | |
/// through each "input" and execute the `write` method of the `StdinWriter` trait _before_ | |
/// returning the handle. | |
/// | |
/// Note, this mirrors the `InputCommand::spawn` and `std::process::Command::spawn` methods. | |
fn spawn(&mut self) -> io::Result<process::Child> { | |
self.cmd.stdin(process::Stdio::piped()); | |
self.cmd.stdout(process::Stdio::piped()); | |
self.cmd.stderr(process::Stdio::piped()); | |
let mut spawned = self.cmd.spawn()?; | |
{ | |
let mut stdin = spawned | |
.stdin | |
.as_mut() | |
.expect("Couldn't get mut ref to command stdin"); | |
for w in &self.writers { | |
w.write(&mut stdin)?; | |
} | |
} | |
Ok(spawned) | |
} | |
} | |
impl<'c> OutputAssertExt for &'c mut InputCommand { | |
fn assert(self) -> Assert { | |
let output = self.output().unwrap(); | |
Assert::new(output).set_cmd(format!("{:?}", self.cmd)) | |
} | |
} | |
/// An implementation of `StdinWriter` that waits for N seconds but does not actually write | |
/// anything to `stdin`. | |
/// | |
/// This is useful in combination with a chain of `input` methods from the `InputCommandExt` trait | |
/// if a delay is needed between writes to `stdin` or before asserting the correctness of the | |
/// output from the command. | |
/// | |
/// # Example | |
/// | |
/// ```rust | |
/// Command::new("cat") | |
/// .input("Hello") | |
/// .then(Wait(5)) // Wait five seconds | |
/// .input(" World") | |
/// .assert() | |
/// .success() | |
/// .stdout(&predicates::ord::eq("Hello World")); | |
/// ``` | |
#[allow(dead_code)] | |
#[derive(Debug)] | |
pub struct Wait(pub u64); | |
impl StdinWriter for Wait { | |
fn write(&self, _stdin: &mut ChildStdin) -> Result<(), io::Error> { | |
thread::sleep(Duration::from_secs(self.0)); | |
Ok(()) | |
} | |
} | |
/// A helper to hopefully improve readability of tests. | |
/// | |
/// It might be better to use the `satisfies` function instead of constructing this directly. | |
/// | |
/// # Example | |
/// | |
/// ```rust | |
/// Command::new("cat") | |
/// .input("Hello") | |
/// .input(" World") | |
/// .assert() | |
/// .success() | |
/// .stdout(&Satisfies(Box::new(|content| { | |
/// content == "Hello World".as_bytes() | |
/// }))); | |
/// ``` | |
#[allow(dead_code)] | |
pub struct Satisfies(Box<Fn(&[u8]) -> bool>); | |
impl Predicate<[u8]> for Satisfies { | |
fn eval(&self, variable: &[u8]) -> bool { | |
(self.0)(variable) | |
} | |
} | |
impl fmt::Display for Satisfies { | |
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
write!(f, "satisfies") | |
} | |
} | |
/// A wrapper around constructing a `Satisfies`. | |
/// | |
/// # Example | |
/// | |
/// This is a trival example because this is essentially what the `is`, `predicates::ord::eq`, and | |
/// `predicate::eq` functions do, but this can be used when more complex post-processing is needed | |
/// on the output from `stdout` in order to determine success. There is also the | |
/// `predicate::function` function that does the exact same thing, but this is arguably easier to | |
/// read in a test. | |
/// | |
/// ```rust | |
/// Command::new("cat") | |
/// .input("Hello") | |
/// .input(" World") | |
/// .assert() | |
/// .success() | |
/// .stdout(&satisfies(|content| { | |
/// content == "Hello World".as_bytes() | |
/// })); | |
/// ``` | |
#[allow(dead_code)] | |
pub fn satisfies<F>(f: F) -> Satisfies | |
where F: Fn(&[u8]) -> bool + 'static | |
{ | |
Satisfies(Box::new(f)) | |
} |
/// but with this trait, the above becomes:
///
/// ```rust
/// Command::main_binary()
/// .unwrap()
/// .with_args(&["-v"])
/// .assert()
/// .success();
/// ```
I can see how the trait helps with handling unwrap
, but how does it help here? main_binary().unwrap()
returns a Command
which you can call args
on, right? So with_args
is redundant with args
?
So couldn't this just as easily be
/// ```rust
/// Command::main_binary()
/// .unwrap()
/// .args(&["-v"])
/// .assert()
/// .success();
/// ```
RE is
and satisfies
Why not just do
pub use predicates::ord::eq as is;
pub use predicates::function::function as satisfies;`
Alternatively, I've added an `IntoOutputPredicate` trait to `assert_cmd`, allowing
```rust
Command::main_binary()
.unwrap()
.env("stdout", "hello")
.env("stderr", "world")
.assert()
.stderr("world\n");
See https://github.com/assert-rs/assert_cmd/blob/master/src/assert.rs#L199
The downside is I've not found good ways to attach this trait to b"binary"
(treats it as a [u8; size]
rather than [u8]
slice). Need to try Fn
but I suspect it will have similar problems.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See the assert_cmd crate and its documentation for more information. Special thanks to the authors of the assert_cli and assert_cmd crates for their work, information, and patience.
License: Apache-2 or MIT