From c8c652ebc0b54a6e27f899d2100467de9f78b8aa Mon Sep 17 00:00:00 2001 From: Christopher Field Date: Fri, 18 May 2018 17:57:22 -0400 Subject: [PATCH 1/2] Add optional access to Command's stdin handle Add access to Command's stdin handle Add InputPredicate type for stdin This trait allows for the computation of the contents to be written to stdin using the `stdin` method of the `Assert` type. A series of `From` implementations are added for the trait so that there is no functionality change to the `stdin` method. By changing to the trait, a "handle" is provided for a user to access stdin during a test. There is no functionality change to the public API, but the `stdin` method could be used in a chain to write content to stdin over time. This can be useful when testing the functionality of a CLI application with streaming input. Add `stdin_contents` field to debug implementation Change lifetimes for trait implementation The 'static lifetime requirement is changed to 'a for the From<&str> implementation of the InputPredicate type. This makes its usage a little more flexible. Add example with a delay An example of composing stdin content over time as a stream to the CLI application under test is added to the `stdin` method. Fix tests Add documentation to `InputPredicate` trait Remove excessive whitespace Add some documentation to explain examples Add `From` implementation A `From<&[u8]>` implementation was missing for the `InputPredicate` trait to avoid a regression. Add export of `InputPredicate` to public API This is needed so users can define types that implement the trait in their tests. Change `From` trait bounds to be generic After some usage with creating another test, types that implemented the `InputPredicate` trait also needed to implement the `From` trait for the `Box` to work. So, the built-in `From` trait to create a boxed `InputPredicate` is changed to be more generic, so (maybe?) the conversion trait does not need to be implement for all types. Refactor formatting for consistency Add example of using type An example is added for the `stdin` method that demonstrates how to define an `InputPredicate` implementation using a unit struct instead of a closure. This improves code re-use and arguably readability/fluency. Add `example` constructor 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. Refactor formatting with rustfmt Fix comment grammer Refactor formatting Add `stdin_contents` field to debug implementation Fix tests Add documentation to `InputPredicate` trait Remove excessive whitespace Add export of `InputPredicate` to public API This is needed so users can define types that implement the trait in their tests. Refactor formatting for consistency Add `example` constructor 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. Refactor formatting with rustfmt Fix comment grammer Refactor formatting --- src/assert.rs | 188 ++++++++++++++++++++++++++++++++++++++++++++++---- src/errors.rs | 3 +- src/lib.rs | 1 + 3 files changed, 179 insertions(+), 13 deletions(-) diff --git a/src/assert.rs b/src/assert.rs index aa11a77..94ac4a2 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -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; @@ -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, @@ -22,7 +22,7 @@ pub struct Assert { expect_success: Option, expect_exit_code: Option, expect_output: Vec, - stdin_contents: Option>, + stdin_contents: Vec>, } impl default::Default for Assert { @@ -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. /// @@ -80,6 +94,27 @@ impl Assert { } } + /// Run a specific example of the current crate. + /// + /// Defaults to asserting _successful_ execution. + pub fn example>(name: S) -> Self { + 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. @@ -121,6 +156,8 @@ impl Assert { /// /// # Examples /// + /// Basic usage. + /// /// ```rust /// extern crate assert_cli; /// @@ -129,8 +166,86 @@ impl Assert { /// .stdout().contains("42") /// .unwrap(); /// ``` - pub fn stdin>>(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>>(mut self, pred: P) -> Self { + self.stdin_contents.push(pred.into()); self } @@ -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()))?; @@ -529,6 +647,52 @@ impl OutputAssertionBuilder { } } +/// A type for writing to stdin during a test. +pub trait InputPredicate { + /// 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 InputPredicate for F +where + F: Fn(&mut ChildStdin) -> Result<(), Error>, +{ + fn write(&self, stdin: &mut ChildStdin) -> Result<(), Error> { + self(stdin) + } +} + +impl

From

for Box +where + P: InputPredicate + 'static, +{ + fn from(p: P) -> Self { + Box::new(p) + } +} + +impl From> for Box { + fn from(contents: Vec) -> Self { + Box::new(move |s: &mut ChildStdin| s.write_all(&contents)) + } +} + +impl<'a> From<&'a [u8]> for Box { + fn from(contents: &[u8]) -> Self { + Self::from(contents.to_owned()) + } +} + +impl<'a> From<&'a str> for Box { + 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::*; diff --git a/src/errors.rs b/src/errors.rs index 0658b52..22abaeb 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,7 +5,8 @@ use std::io; use failure; fn format_cmd(cmd: &[ffi::OsString]) -> String { - let result: Vec = cmd.iter() + let result: Vec = cmd + .iter() .map(|s| s.to_string_lossy().into_owned()) .collect(); result.join(" ") diff --git a/src/lib.rs b/src/lib.rs index 9b055b4..6e8f159 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 /// From c3a13f5225ec3c199c80b97f14d095bf043e09db Mon Sep 17 00:00:00 2001 From: Christopher Field Date: Sun, 20 May 2018 22:04:15 -0500 Subject: [PATCH 2/2] Rename `InputPredicate to `StdinWriter` The `InputPredicate` trait is renamed to `StdinWriter` to avoid confusion and naming conflicts with upcoming API changes and coming usage refactoring to use the `predicate` crate upstream. --- src/assert.rs | 22 +++++++++++----------- src/lib.rs | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/assert.rs b/src/assert.rs index 94ac4a2..540ab08 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -22,7 +22,7 @@ pub struct Assert { expect_success: Option, expect_exit_code: Option, expect_output: Vec, - stdin_contents: Vec>, + stdin_contents: Vec>, } impl default::Default for Assert { @@ -221,7 +221,7 @@ impl Assert { /// ```rust /// extern crate assert_cli; /// - /// use assert_cli::InputPredicate; + /// use assert_cli::StdinWriter; /// use std::io::Error; /// use std::process::ChildStdin; /// use std::thread; @@ -229,7 +229,7 @@ impl Assert { /// /// struct Wait(u64); /// - /// impl InputPredicate for Wait { + /// impl StdinWriter for Wait { /// fn write(&self, _stdin: &mut ChildStdin) -> Result<(), Error> { /// thread::sleep(Duration::from_secs(self.0)); /// Ok(()) @@ -244,7 +244,7 @@ impl Assert { /// .stdout().contains("42") /// .unwrap(); /// } - pub fn stdin>>(mut self, pred: P) -> Self { + pub fn stdin>>(mut self, pred: P) -> Self { self.stdin_contents.push(pred.into()); self } @@ -648,7 +648,7 @@ impl OutputAssertionBuilder { } /// A type for writing to stdin during a test. -pub trait InputPredicate { +pub trait StdinWriter { /// Write to stdin. /// /// This provides a "handle" or "hook" to directly access the stdin pipe for lower-level @@ -656,7 +656,7 @@ pub trait InputPredicate { fn write(&self, stdin: &mut ChildStdin) -> Result<(), Error>; } -impl InputPredicate for F +impl StdinWriter for F where F: Fn(&mut ChildStdin) -> Result<(), Error>, { @@ -665,28 +665,28 @@ where } } -impl

From

for Box +impl

From

for Box where - P: InputPredicate + 'static, + P: StdinWriter + 'static, { fn from(p: P) -> Self { Box::new(p) } } -impl From> for Box { +impl From> for Box { fn from(contents: Vec) -> Self { Box::new(move |s: &mut ChildStdin| s.write_all(&contents)) } } -impl<'a> From<&'a [u8]> for Box { +impl<'a> From<&'a [u8]> for Box { fn from(contents: &[u8]) -> Self { Self::from(contents.to_owned()) } } -impl<'a> From<&'a str> for Box { +impl<'a> From<&'a str> for Box { fn from(contents: &str) -> Self { let c = contents.to_owned(); Box::new(move |s: &mut ChildStdin| s.write_all(c.as_bytes())) diff --git a/src/lib.rs b/src/lib.rs index 6e8f159..42c2f40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -138,7 +138,7 @@ mod diff; mod output; pub use assert::Assert; -pub use assert::InputPredicate; +pub use assert::StdinWriter; pub use assert::OutputAssertionBuilder; /// Environment is a re-export of the Environment crate ///