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

Add optional access to Command's stdin handle #107

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 176 additions & 12 deletions src/assert.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use std::default;
use std::ffi::{OsStr, OsString};
use std::io::Write;
use std::fmt;
use std::io::{Error, Write};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::process::{ChildStdin, Command, Stdio};
use std::vec::Vec;

use environment::Environment;
Expand All @@ -13,7 +14,6 @@ use errors::*;
use output::{Content, Output, OutputKind, OutputPredicate};

/// Assertions for a specific command.
#[derive(Debug)]
#[must_use]
pub struct Assert {
cmd: Vec<OsString>,
Expand All @@ -22,7 +22,7 @@ pub struct Assert {
expect_success: Option<bool>,
expect_exit_code: Option<i32>,
expect_output: Vec<OutputPredicate>,
stdin_contents: Option<Vec<u8>>,
stdin_contents: Vec<Box<InputPredicate>>,
}

impl default::Default for Assert {
Expand All @@ -46,11 +46,25 @@ impl default::Default for Assert {
expect_success: Some(true),
expect_exit_code: None,
expect_output: vec![],
stdin_contents: None,
stdin_contents: vec![],
}
}
}

impl fmt::Debug for Assert {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Assert")
.field("cmd", &self.cmd)
.field("env", &self.env)
.field("current_dir", &self.current_dir)
.field("expect_success", &self.expect_success)
.field("expect_exit_code", &self.expect_exit_code)
.field("expect_output", &self.expect_output)
.field("stdin_contents", &self.stdin_contents.len())
.finish()
}
}

impl Assert {
/// Run the crate's main binary.
///
Expand Down Expand Up @@ -80,6 +94,27 @@ impl Assert {
}
}

/// Run a specific example of the current crate.
///
/// Defaults to asserting _successful_ execution.
pub fn example<S: AsRef<OsStr>>(name: S) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From commit message

The example constructor is added to the Assert struct as a helper
method to run an example from the crate instead of having to use the
command constructor. This is similar to the cargo_binary but uses
the --example option instead of the --bin option.

Could you split this out into a dedicated PR so we can address these topics separately?

Assert {
cmd: vec![
OsStr::new("cargo"),
OsStr::new("run"),
#[cfg(not(debug_assertions))]
OsStr::new("--release"),
OsStr::new("--quiet"),
OsStr::new("--example"),
name.as_ref(),
OsStr::new("--"),
].into_iter()
.map(OsString::from)
.collect(),
..Self::default()
}
}

/// Run a custom command.
///
/// Defaults to asserting _successful_ execution.
Expand Down Expand Up @@ -121,6 +156,8 @@ impl Assert {
///
/// # Examples
///
/// Basic usage.
///
/// ```rust
/// extern crate assert_cli;
///
Expand All @@ -129,8 +166,86 @@ impl Assert {
/// .stdout().contains("42")
/// .unwrap();
/// ```
pub fn stdin<S: Into<Vec<u8>>>(mut self, contents: S) -> Self {
self.stdin_contents = Some(contents.into());
///
/// A closure can also be used to compute the contents to write to stdin.
///
/// ```rust
/// extern crate assert_cli;
///
/// use std::io::Write;
/// use std::process::ChildStdin;
///
/// assert_cli::Assert::command(&["cat"])
/// .stdin(|s: &mut ChildStdin| {
/// s.write_all("42".as_bytes())
/// })
/// .stdout().contains("42")
/// .unwrap();
/// ```
///
/// Content can be composed over time with a chain. This allows for mimicking the streaming
/// nature of stdio when the CLI application is used with pipes.
///
/// ```rust
/// extern crate assert_cli;
///
/// assert_cli::Assert::command(&["cat"])
/// .stdin("4")
/// .stdin("2")
/// .stdout().contains("42")
/// .unwrap();
/// ```
///
/// or to mimick streaming of discontinuous data from a pipe.
///
/// ```rust
/// extern crate assert_cli;
///
/// use std::thread;
/// use std::time::Duration;
///
/// assert_cli::Assert::command(&["cat"])
/// .stdin("4")
/// .stdin(|_: &mut _| {
/// thread::sleep(Duration::from_secs(1));
/// Ok(())
/// })
/// .stdin("2")
/// .stdout().contains("42")
/// .unwrap();
/// ```
///
/// The previous example can also be implemented with a custom struct type for better code
/// reuse in multiple tests and arguably improved readability.
///
/// ```rust
/// extern crate assert_cli;
///
/// use assert_cli::InputPredicate;
/// use std::io::Error;
/// use std::process::ChildStdin;
/// use std::thread;
/// use std::time::Duration;
///
/// struct Wait(u64);
///
/// impl InputPredicate for Wait {
/// fn write(&self, _stdin: &mut ChildStdin) -> Result<(), Error> {
/// thread::sleep(Duration::from_secs(self.0));
/// Ok(())
/// }
/// }
///
/// fn main() {
/// assert_cli::Assert::command(&["cat"])
/// .stdin("4")
/// .stdin(Wait(1))
/// .stdin("2")
/// .stdout().contains("42")
/// .unwrap();
/// }
pub fn stdin<P: Into<Box<InputPredicate>>>(mut self, pred: P) -> Self {
self.stdin_contents.push(pred.into());
self
}

Expand Down Expand Up @@ -357,14 +472,17 @@ impl Assert {
.spawn()
.chain_with(|| AssertionError::new(self.cmd.clone()))?;

if let Some(ref contents) = self.stdin_contents {
spawned
if !self.stdin_contents.is_empty() {
let mut stdin = spawned
.stdin
.as_mut()
.expect("Couldn't get mut ref to command stdin")
.write_all(contents)
.chain_with(|| AssertionError::new(self.cmd.clone()))?;
.expect("Couldn't get mut ref to command stdin");
for p in &self.stdin_contents {
p.write(&mut stdin)
.chain_with(|| AssertionError::new(self.cmd.clone()))?;
}
}

let output = spawned
.wait_with_output()
.chain_with(|| AssertionError::new(self.cmd.clone()))?;
Expand Down Expand Up @@ -529,6 +647,52 @@ impl OutputAssertionBuilder {
}
}

/// A type for writing to stdin during a test.
pub trait InputPredicate {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we redo the API to use the predicates crate, this name will become confusing. Maybe StdinWriter?

I'd say we could reuse the Write trait but that'd be annoying with providing trait implementations. Maybe we don't make it so automatic but provide a helper content factory or something?

/// Write to stdin.
///
/// This provides a "handle" or "hook" to directly access the stdin pipe for lower-level
/// control and usage.
fn write(&self, stdin: &mut ChildStdin) -> Result<(), Error>;
}

impl<F> InputPredicate for F
where
F: Fn(&mut ChildStdin) -> Result<(), Error>,
{
fn write(&self, stdin: &mut ChildStdin) -> Result<(), Error> {
self(stdin)
}
}

impl<P> From<P> for Box<InputPredicate>
where
P: InputPredicate + 'static,
{
fn from(p: P) -> Self {
Box::new(p)
}
}

impl From<Vec<u8>> for Box<InputPredicate> {
fn from(contents: Vec<u8>) -> Self {
Box::new(move |s: &mut ChildStdin| s.write_all(&contents))
}
}

impl<'a> From<&'a [u8]> for Box<InputPredicate> {
fn from(contents: &[u8]) -> Self {
Self::from(contents.to_owned())
}
}

impl<'a> From<&'a str> for Box<InputPredicate> {
fn from(contents: &str) -> Self {
let c = contents.to_owned();
Box::new(move |s: &mut ChildStdin| s.write_all(c.as_bytes()))
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
3 changes: 2 additions & 1 deletion src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use std::io;
use failure;

fn format_cmd(cmd: &[ffi::OsString]) -> String {
let result: Vec<String> = cmd.iter()
let result: Vec<String> = cmd
.iter()
.map(|s| s.to_string_lossy().into_owned())
.collect();
result.join(" ")
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ mod diff;
mod output;

pub use assert::Assert;
pub use assert::InputPredicate;
pub use assert::OutputAssertionBuilder;
/// Environment is a re-export of the Environment crate
///
Expand Down