Skip to content

Instantly share code, notes, and snippets.

@volks73
Last active June 18, 2018 15:22
Show Gist options
  • Save volks73/2e15476d952b6d03e8ec6c6a64884866 to your computer and use it in GitHub Desktop.
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
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))
}
@volks73
Copy link
Author

volks73 commented Jun 17, 2018

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

@epage
Copy link

epage commented Jun 18, 2018

/// 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();
/// ```

@epage
Copy link

epage commented Jun 18, 2018

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