From d159e875aee71841198c67cd1a4e848b8bb9e465 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 5 Dec 2019 11:58:05 -0700 Subject: [PATCH] fix(stdin): Provide a Command wrapper Many of our problems with `stdin` are derived from using extension traits. By moving away from them, we fix the API problems and make it easier to add other features like timeout (#10) or signalling (#84). So the new philosphy if: - Core functionality is provided by extension traits - Provide a convinience API that makes `Command` friendlier. These do not need to be generalized. Other abstractions can provide their own (like `duct`). Fixes #73 --- README.md | 11 +- src/cargo.rs | 22 +- src/cmd.rs | 496 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 79 ++++---- src/stdin.rs | 224 --------------------- tests/examples.rs | 12 +- 6 files changed, 557 insertions(+), 287 deletions(-) create mode 100644 src/cmd.rs delete mode 100644 src/stdin.rs diff --git a/README.md b/README.md index 3e71767..6ddf3cc 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,10 @@ assert_cmd = "0.11" Here's a trivial example: ```rust,no_run -extern crate assert_cmd; +use assert_cmd::Command; -use std::process::Command; -use assert_cmd::prelude::*; - -Command::cargo_bin("bin_fixture") - .unwrap() - .assert() - .success(); +let mut cmd = Command::cargo_bin("bin_fixture").unwrap(); +cmd.assert().success(); ``` ## Relevant crates diff --git a/src/cargo.rs b/src/cargo.rs index cb52c4e..345071f 100644 --- a/src/cargo.rs +++ b/src/cargo.rs @@ -124,14 +124,24 @@ where fn cargo_bin>(name: S) -> Result; } +impl CommandCargoExt for crate::cmd::Command { + fn cargo_bin>(name: S) -> Result { + crate::cmd::Command::cargo_bin(name) + } +} + impl CommandCargoExt for process::Command { fn cargo_bin>(name: S) -> Result { - let path = cargo_bin(name); - if path.is_file() { - Ok(process::Command::new(path)) - } else { - Err(CargoError::with_cause(NotFoundError { path })) - } + cargo_bin_cmd(name) + } +} + +pub(crate) fn cargo_bin_cmd>(name: S) -> Result { + let path = cargo_bin(name); + if path.is_file() { + Ok(process::Command::new(path)) + } else { + Err(CargoError::with_cause(NotFoundError { path })) } } diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 0000000..e1f2bf3 --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,496 @@ +//! [`std::process::Command`][Command] customized for testing. +//! +//! [Command]: https://doc.rust-lang.org/std/process/struct.Command.html + +use std::ffi; +use std::io; +use std::io::Write; +use std::path; +use std::process; + +use crate::assert::Assert; +use crate::assert::OutputAssertExt; +use crate::output::dump_buffer; +use crate::output::DebugBuffer; +use crate::output::OutputError; +use crate::output::OutputOkExt; +use crate::output::OutputResult; + +/// [`std::process::Command`][Command] customized for testing. +/// +/// [Command]: https://doc.rust-lang.org/std/process/struct.Command.html +#[derive(Debug)] +pub struct Command { + cmd: process::Command, + stdin: Option>, +} + +impl Command { + /// Constructs a new `Command` from a `std` `Command`. + pub fn from_std(cmd: process::Command) -> Self { + Self { cmd, stdin: None } + } + + /// Create a `Command` to run a specific binary of the current crate. + /// + /// See the [`cargo` module documentation][`cargo`] for caveats and workarounds. + /// + /// # Examples + /// + /// ```rust,no_run + /// use assert_cmd::Command; + /// + /// let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")) + /// .unwrap(); + /// let output = cmd.unwrap(); + /// println!("{:?}", output); + /// ``` + /// + /// ```rust,no_run + /// use assert_cmd::Command; + /// + /// let mut cmd = Command::cargo_bin("bin_fixture") + /// .unwrap(); + /// let output = cmd.unwrap(); + /// println!("{:?}", output); + /// ``` + /// + /// [`cargo`]: index.html + pub fn cargo_bin>(name: S) -> Result { + let cmd = crate::cargo::cargo_bin_cmd(name)?; + Ok(Self::from_std(cmd)) + } + + /// Write `buffer` to `stdin` when the `Command` is run. + /// + /// # Examples + /// + /// ```rust + /// use assert_cmd::Command; + /// + /// let mut cmd = Command::new("cat") + /// .arg("-et") + /// .write_stdin("42") + /// .assert() + /// .stdout("42"); + /// ``` + pub fn write_stdin(&mut self, buffer: S) -> &mut Self + where + S: Into>, + { + self.stdin = Some(buffer.into()); + self + } + + /// Write `path`s content to `stdin` when the `Command` is run. + /// + /// Paths are relative to the [`env::current_dir`][env_current_dir] and not + /// [`Command::current_dir`][Command_current_dir]. + /// + /// [env_current_dir]: https://doc.rust-lang.org/std/env/fn.current_dir.html + /// [Command_current_dir]: https://doc.rust-lang.org/std/process/struct.Command.html#method.current_dir + pub fn pipe_stdin

(&mut self, file: P) -> io::Result<&mut Self> + where + P: AsRef, + { + let buffer = std::fs::read(file)?; + Ok(self.write_stdin(buffer)) + } + + /// Run a `Command`, returning an [`OutputResult`][OutputResult]. + /// + /// # Examples + /// + /// ```rust + /// use assert_cmd::Command; + /// + /// let result = Command::new("echo") + /// .args(&["42"]) + /// .ok(); + /// assert!(result.is_ok()); + /// ``` + /// + /// [OutputResult]: type.OutputResult.html + pub fn ok(&mut self) -> OutputResult { + OutputOkExt::ok(self) + } + + /// Run a `Command`, unwrapping the [`OutputResult`][OutputResult]. + /// + /// # Examples + /// + /// ```rust + /// use assert_cmd::Command; + /// + /// let output = Command::new("echo") + /// .args(&["42"]) + /// .unwrap(); + /// ``` + /// + /// [OutputResult]: type.OutputResult.html + pub fn unwrap(&mut self) -> process::Output { + OutputOkExt::unwrap(self) + } + + /// Run a `Command`, unwrapping the error in the [`OutputResult`][OutputResult]. + /// + /// # Examples + /// + /// ```rust,no_run + /// use assert_cmd::Command; + /// + /// let err = Command::new("a-command") + /// .args(&["--will-fail"]) + /// .unwrap_err(); + /// ``` + /// + /// [Output]: https://doc.rust-lang.org/std/process/struct.Output.html + pub fn unwrap_err(&mut self) -> OutputError { + OutputOkExt::unwrap_err(self) + } + + /// Run a `Command` and make assertions on the [`Output`]. + /// + /// # Examples + /// + /// ```rust,no_run + /// use assert_cmd::Command; + /// + /// let mut cmd = Command::cargo_bin("bin_fixture") + /// .unwrap() + /// .assert() + /// .success(); + /// ``` + /// + /// [`Output`]: https://doc.rust-lang.org/std/process/struct.Output.html + pub fn assert(&mut self) -> Assert { + OutputAssertExt::assert(self) + } +} + +/// Mirror [`std::process::Command`][Command]'s API +/// +/// [Command]: https://doc.rust-lang.org/std/process/struct.Command.html +impl Command { + /// Constructs a new `Command` for launching the program at + /// path `program`, with the following default configuration: + /// + /// * No arguments to the program + /// * Inherit the current process's environment + /// * Inherit the current process's working directory + /// * Inherit stdin/stdout/stderr for `spawn` or `status`, but create pipes for `output` + /// + /// Builder methods are provided to change these defaults and + /// otherwise configure the process. + /// + /// If `program` is not an absolute path, the `PATH` will be searched in + /// an OS-defined way. + /// + /// The search path to be used may be controlled by setting the + /// `PATH` environment variable on the Command, + /// but this has some implementation limitations on Windows + /// (see issue #37519). + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use assert_cmd::Command; + /// + /// Command::new("sh").unwrap(); + /// ``` + pub fn new>(program: S) -> Self { + let cmd = process::Command::new(program); + Self::from_std(cmd) + } + + /// Adds an argument to pass to the program. + /// + /// Only one argument can be passed per use. So instead of: + /// + /// ```no_run + /// # assert_cmd::Command::new("sh") + /// .arg("-C /path/to/repo") + /// # ; + /// ``` + /// + /// usage would be: + /// + /// ```no_run + /// # assert_cmd::Command::new("sh") + /// .arg("-C") + /// .arg("/path/to/repo") + /// # ; + /// ``` + /// + /// To pass multiple arguments see [`args`]. + /// + /// [`args`]: #method.args + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use assert_cmd::Command; + /// + /// Command::new("ls") + /// .arg("-l") + /// .arg("-a") + /// .unwrap(); + /// ``` + pub fn arg>(&mut self, arg: S) -> &mut Self { + self.cmd.arg(arg); + self + } + + /// Adds multiple arguments to pass to the program. + /// + /// To pass a single argument see [`arg`]. + /// + /// [`arg`]: #method.arg + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use assert_cmd::Command; + /// + /// Command::new("ls") + /// .args(&["-l", "-a"]) + /// .unwrap(); + /// ``` + pub fn args(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + self.cmd.args(args); + self + } + + /// Inserts or updates an environment variable mapping. + /// + /// Note that environment variable names are case-insensitive (but case-preserving) on Windows, + /// and case-sensitive on all other platforms. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use assert_cmd::Command; + /// + /// Command::new("ls") + /// .env("PATH", "/bin") + /// .unwrap_err(); + /// ``` + pub fn env(&mut self, key: K, val: V) -> &mut Self + where + K: AsRef, + V: AsRef, + { + self.cmd.env(key, val); + self + } + + /// Adds or updates multiple environment variable mappings. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use assert_cmd::Command; + /// use std::process::Stdio; + /// use std::env; + /// use std::collections::HashMap; + /// + /// let filtered_env : HashMap = + /// env::vars().filter(|&(ref k, _)| + /// k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH" + /// ).collect(); + /// + /// Command::new("printenv") + /// .env_clear() + /// .envs(&filtered_env) + /// .unwrap(); + /// ``` + pub fn envs(&mut self, vars: I) -> &mut Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + self.cmd.envs(vars); + self + } + + /// Removes an environment variable mapping. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use assert_cmd::Command; + /// + /// Command::new("ls") + /// .env_remove("PATH") + /// .unwrap_err(); + /// ``` + pub fn env_remove>(&mut self, key: K) -> &mut Self { + self.cmd.env_remove(key); + self + } + + /// Clears the entire environment map for the child process. + /// + /// # Examples + /// + /// Basic usage: + /// + /// use std::process::Command;; + + /// + /// Command::new("ls") + /// .env_clear() + /// .unwrap_err(); + /// ``` + pub fn env_clear(&mut self) -> &mut Self { + self.cmd.env_clear(); + self + } + + /// Sets the working directory for the child process. + /// + /// # Platform-specific behavior + /// + /// If the program path is relative (e.g., `"./script.sh"`), it's ambiguous + /// whether it should be interpreted relative to the parent's working + /// directory or relative to `current_dir`. The behavior in this case is + /// platform specific and unstable, and it's recommended to use + /// [`canonicalize`] to get an absolute program path instead. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use assert_cmd::Command; + /// + /// Command::new("ls") + /// .current_dir("/bin") + /// .unwrap(); + /// ``` + /// + /// [`canonicalize`]: ../fs/fn.canonicalize.html + pub fn current_dir>(&mut self, dir: P) -> &mut Self { + self.cmd.current_dir(dir); + self + } + + /// 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. + /// + /// # Examples + /// + /// ```should_panic + /// use assert_cmd::Command; + /// use std::io::{self, Write}; + /// let output = Command::new("/bin/cat") + /// .arg("file.txt") + /// .output() + /// .expect("failed to execute process"); + /// + /// println!("status: {}", output.status); + /// io::stdout().write_all(&output.stdout).unwrap(); + /// io::stderr().write_all(&output.stderr).unwrap(); + /// + /// assert!(output.status.success()); + /// ``` + pub fn output(&mut self) -> io::Result { + self.spawn()?.wait_with_output() + } + + fn spawn(&mut self) -> io::Result { + // stdout/stderr should only be piped for `output` according to `process::Command::new`. + self.cmd.stdin(process::Stdio::piped()); + self.cmd.stdout(process::Stdio::piped()); + self.cmd.stderr(process::Stdio::piped()); + + let mut spawned = self.cmd.spawn()?; + + if let Some(buffer) = self.stdin.as_ref() { + spawned + .stdin + .as_mut() + .expect("Couldn't get mut ref to command stdin") + .write_all(&buffer)?; + } + Ok(spawned) + } +} + +impl From for Command { + fn from(cmd: process::Command) -> Self { + Command::from_std(cmd) + } +} + +impl<'c> OutputOkExt for &'c mut Command { + fn ok(self) -> OutputResult { + let output = self.output().map_err(OutputError::with_cause)?; + if output.status.success() { + Ok(output) + } else { + let error = OutputError::new(output).set_cmd(format!("{:?}", self.cmd)); + let error = if let Some(stdin) = self.stdin.as_ref() { + error.set_stdin(stdin.clone()) + } else { + error + }; + Err(error) + } + } + + fn unwrap_err(self) -> OutputError { + match self.ok() { + Ok(output) => { + if let Some(stdin) = self.stdin.as_ref() { + panic!( + "Completed successfully:\ncommand=`{:?}`\nstdin=```{}```\nstdout=```{}```", + self.cmd, + dump_buffer(&stdin), + dump_buffer(&output.stdout) + ) + } else { + panic!( + "Completed successfully:\ncommand=`{:?}`\nstdout=```{}```", + self.cmd, + dump_buffer(&output.stdout) + ) + } + } + Err(err) => err, + } + } +} + +impl<'c> OutputAssertExt for &'c mut Command { + fn assert(self) -> Assert { + let output = self.output().unwrap(); + let assert = Assert::new(output).append_context("command", format!("{:?}", self.cmd)); + if let Some(stdin) = self.stdin.as_ref() { + assert.append_context("stdin", DebugBuffer::new(stdin.clone())) + } else { + assert + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 1756830..99b1b5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ //! **Assert [`Command`]** - Easy command initialization and assertions. //! //! `assert_cmd` includes support for: -//! - Setting up your program-under-test (see [`CommandCargoExt`], [`CommandStdInExt`]). -//! - Verifying your program-under-test (see [`OutputOkExt`], [`OutputAssertExt`]). +//! - Setting up your program-under-test. +//! - Verifying your program-under-test. //! //! ```toml //! [dependencies] @@ -12,18 +12,19 @@ //! ## Overview //! //! Create a [`Command`]: -//! - `Command::new(path)`, see [`Command`] -//! - `Command::cargo_bin(name)`, see [`CommandCargoExt`] +//! - `Command::new(path)` +//! - `Command::from_std(...)` +//! - `Command::cargo_bin(name)` //! //! Configure a [`Command`]: -//! - `arg` / `args`, see [`Command`] -//! - `current_dir`, see [`Command`] -//! - `env` / `envs` / `env_remove` / `env_clear`, see [`Command`] -//! - `with_stdin`, see [`CommandStdInExt`] -//! -//! Validate either a [`Command`] or `Output`: -//! - `ok` / `unwrap` / `unwrap_err`, see [`OutputOkExt`] -//! - `assert` ([`OutputAssertExt`]) +//! - `arg` / `args` +//! - `current_dir` +//! - `env` / `envs` / `env_remove` / `env_clear` +//! - `write_stdin` / `pipe_stdin` +//! +//! Validate a [`Command`]: +//! - `ok` / `unwrap` / `unwrap_err` +//! - `assert` //! - `success`, see [`Assert`] //! - `failure`, see [`Assert`] //! - `interrupted`, see [`Assert`] @@ -31,14 +32,17 @@ //! - `stdout`, see [`Assert`] //! - `stderr`, see [`Assert`] //! +//! Note: [`Command`] is provided as a convenience. Extension traits for [`std::process::Command`] +//! and `Output` are provided for interoperability: +//! - [`CommandCargoExt`] +//! - [`OutputOkExt`] +//! - [`OutputAssertExt`] +//! //! ## Examples //! //! Here's a trivial example: //! ```rust,no_run -//! extern crate assert_cmd; -//! -//! use std::process::Command; -//! use assert_cmd::prelude::*; +//! use assert_cmd::Command; //! //! fn main() { //! let mut cmd = Command::cargo_bin("bin_fixture").unwrap(); @@ -48,20 +52,15 @@ //! //! And a little of everything: //! ```rust,no_run -//! extern crate assert_cmd; -//! -//! use std::process::Command; -//! use assert_cmd::prelude::*; +//! use assert_cmd::Command; //! //! fn main() { //! let mut cmd = Command::cargo_bin("bin_fixture").unwrap(); -//! cmd +//! let assert = cmd //! .arg("-A") //! .env("stdout", "hello") -//! .env("exit", "42"); -//! let assert = cmd -//! .with_stdin() -//! .buffer("42") +//! .env("exit", "42") +//! .write_stdin("42") //! .assert(); //! assert //! .failure() @@ -87,7 +86,6 @@ //! - Addresses several architectural problems. //! //! Key points in migrating from `assert_cli`: -//! - [`Command`] is extended with traits rather than being wrapping in custom logic. //! - The command-under-test is run eagerly, with assertions happening immediately. //! - [`success()`] is not implicit and requires being explicitly called. //! - `stdout`/`stderr` aren't automatically trimmed before being passed to the `Predicate`. @@ -98,11 +96,11 @@ //! [tempfile]: https://crates.io/crates/tempfile //! [duct]: https://crates.io/crates/duct //! [assert_fs]: https://crates.io/crates/assert_fs -//! [`Command`]: https://doc.rust-lang.org/std/process/struct.Command.html +//! [`Command`]: cmd/struct.Command.html +//! [`std::process::Command`]: https://doc.rust-lang.org/std/process/struct.Command.html //! [`Assert`]: assert/struct.Assert.html //! [`success()`]: assert/struct.Assert.html#method.success //! [`CommandCargoExt`]: cargo/trait.CommandCargoExt.html -//! [`CommandStdInExt`]: stdin/trait.CommandStdInExt.html //! [`OutputOkExt`]: cmd/trait.OutputOkExt.html //! [`OutputAssertExt`]: assert/trait.OutputAssertExt.html @@ -113,28 +111,22 @@ /// # Examples /// /// ```should_panic -/// #[macro_use] -/// extern crate assert_cmd; -/// -/// use std::process::Command; -/// use assert_cmd::prelude::*; +/// use assert_cmd::Command; /// /// fn main() { -/// let mut cmd = Command::cargo_bin(crate_name!()).unwrap(); -/// cmd +/// let mut cmd = Command::cargo_bin(assert_cmd::crate_name!()).unwrap(); +/// let assert = cmd /// .arg("-A") /// .env("stdout", "hello") /// .env("exit", "42") -/// .with_stdin() -/// .buffer("42"); -/// let assert = cmd.assert(); +/// .write_stdin("42") +/// .assert(); /// assert /// .failure() /// .code(42) /// .stdout("hello\n"); /// } /// ``` -#[cfg(not(feature = "no_cargo"))] #[macro_export] macro_rules! crate_name { () => { @@ -144,17 +136,16 @@ macro_rules! crate_name { pub mod assert; pub mod cargo; +pub mod cmd; pub mod output; -pub mod stdin; /// Extension traits that are useful to have available. pub mod prelude { pub use crate::assert::OutputAssertExt; pub use crate::cargo::CommandCargoExt; pub use crate::output::OutputOkExt; - pub use crate::stdin::CommandStdInExt; } -#[macro_use] -extern crate doc_comment; -doctest!("../README.md"); +pub use crate::cmd::Command; + +doc_comment::doctest!("../README.md"); diff --git a/src/stdin.rs b/src/stdin.rs deleted file mode 100644 index c2147d4..0000000 --- a/src/stdin.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! Write to `stdin` of a [`Command`][Command]. -//! -//! [Command]: https://doc.rust-lang.org/std/process/struct.Command.html - -use std::fs; -use std::io; -use std::io::Read; -use std::io::Write; -use std::path; -use std::process; - -use crate::assert::Assert; -use crate::assert::OutputAssertExt; -use crate::output::dump_buffer; -use crate::output::DebugBuffer; -use crate::output::OutputError; -use crate::output::OutputOkExt; -use crate::output::OutputResult; - -/// Write to `stdin` of a [`Command`][Command]. -/// -/// [Command]: https://doc.rust-lang.org/std/process/struct.Command.html -pub trait CommandStdInExt { - /// Write `buffer` to `stdin` when the command is run. - /// - /// # Examples - /// - /// ```rust - /// use assert_cmd::prelude::*; - /// - /// use std::process::Command; - /// - /// let mut cmd = Command::new("cat"); - /// cmd - /// .arg("-et"); - /// cmd - /// .with_stdin() - /// .buffer("42") - /// .assert() - /// .stdout("42"); - /// ``` - fn with_stdin(&mut self) -> StdInCommandBuilder<'_>; -} - -impl CommandStdInExt for process::Command { - fn with_stdin(&mut self) -> StdInCommandBuilder<'_> { - StdInCommandBuilder { cmd: self } - } -} - -/// For adding a stdin to a [`Command`][Command]. -/// -/// [Command]: https://doc.rust-lang.org/std/process/struct.Command.html -#[derive(Debug)] -pub struct StdInCommandBuilder<'a> { - cmd: &'a mut process::Command, -} - -impl<'a> StdInCommandBuilder<'a> { - /// Write `buffer` to `stdin` when the [`Command`][Command] is run. - /// - /// # Examples - /// - /// ```rust - /// use assert_cmd::prelude::*; - /// - /// use std::process::Command; - /// - /// let mut cmd = Command::new("cat"); - /// cmd - /// .arg("-et"); - /// cmd - /// .with_stdin() - /// .buffer("42") - /// .assert() - /// .stdout("42"); - /// ``` - /// - /// [Command]: https://doc.rust-lang.org/std/process/struct.Command.html - pub fn buffer(&mut self, buffer: S) -> StdInCommand<'_> - where - S: Into>, - { - StdInCommand { - cmd: self.cmd, - stdin: buffer.into(), - } - } - - /// Write `path`s content to `stdin` when the [`Command`][Command] is run. - /// - /// Paths are relative to the [`env::current_dir`][env_current_dir] and not - /// [`Command::current_dir`][Command_current_dir]. - /// - /// # Examples - /// - /// ```rust - /// use assert_cmd::prelude::*; - /// - /// use std::process::Command; - /// - /// let mut cmd = Command::new("cat"); - /// cmd - /// .arg("-et"); - /// cmd - /// .with_stdin() - /// .buffer("42") - /// .assert() - /// .stdout("42"); - /// ``` - /// - /// [Command]: https://doc.rust-lang.org/std/process/struct.Command.html - /// [env_current_dir]: https://doc.rust-lang.org/std/env/fn.current_dir.html - /// [Command_current_dir]: https://doc.rust-lang.org/std/process/struct.Command.html#method.current_dir - pub fn path

(&mut self, file: P) -> io::Result> - where - P: AsRef, - { - let file = file.as_ref(); - let mut buffer = Vec::new(); - fs::File::open(file)?.read_to_end(&mut buffer)?; - Ok(StdInCommand { - cmd: self.cmd, - stdin: buffer, - }) - } -} - -/// [`Command`][Command] that carries the `stdin` buffer. -/// -/// Create a `StdInCommand` through the [`CommandStdInExt`][CommandStdInExt] trait. -/// -/// # Examples -/// -/// ```rust -/// use assert_cmd::prelude::*; -/// -/// use std::process::Command; -/// -/// let mut cmd = Command::new("cat"); -/// cmd -/// .arg("-et"); -/// cmd -/// .with_stdin() -/// .buffer("42") -/// .assert() -/// .stdout("42"); -/// ``` -/// -/// [Command]: https://doc.rust-lang.org/std/process/struct.Command.html -/// [CommandStdInExt]: trait.CommandStdInExt.html -#[derive(Debug)] -pub struct StdInCommand<'a> { - cmd: &'a mut process::Command, - stdin: Vec, -} - -impl<'a> StdInCommand<'a> { - /// Executes the [`Command`][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. - /// - /// *(mirrors [`Command::output`][Command_output])* - /// - /// [Command]: https://doc.rust-lang.org/std/process/struct.Command.html - /// [Command_output]: https://doc.rust-lang.org/std/process/struct.Command.html#method.output - pub fn output(&mut self) -> io::Result { - self.spawn()?.wait_with_output() - } - - fn spawn(&mut self) -> io::Result { - // stdout/stderr should only be piped for `output` according to `process::Command::new`. - self.cmd.stdin(process::Stdio::piped()); - self.cmd.stdout(process::Stdio::piped()); - self.cmd.stderr(process::Stdio::piped()); - - let mut spawned = self.cmd.spawn()?; - - spawned - .stdin - .as_mut() - .expect("Couldn't get mut ref to command stdin") - .write_all(&self.stdin)?; - Ok(spawned) - } -} - -impl<'c, 'a> OutputOkExt for &'c mut StdInCommand<'a> { - fn ok(self) -> OutputResult { - let output = self.output().map_err(OutputError::with_cause)?; - if output.status.success() { - Ok(output) - } else { - let error = OutputError::new(output) - .set_cmd(format!("{:?}", self.cmd)) - .set_stdin(self.stdin.clone()); - Err(error) - } - } - - fn unwrap_err(self) -> OutputError { - match self.ok() { - Ok(output) => panic!( - "Completed successfully:\ncommand=`{:?}`\nstdin=```{}```\nstdout=```{}```", - self.cmd, - dump_buffer(&self.stdin), - dump_buffer(&output.stdout) - ), - Err(err) => err, - } - } -} - -impl<'c> OutputAssertExt for &'c mut StdInCommand<'c> { - fn assert(self) -> Assert { - let output = self.output().unwrap(); - Assert::new(output) - .append_context("command", format!("{:?}", self.cmd)) - .append_context("stdin", DebugBuffer::new(self.stdin.clone())) - } -} diff --git a/tests/examples.rs b/tests/examples.rs index 17cf5eb..b2c9151 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -1,6 +1,4 @@ -use std::process::Command; - -use assert_cmd::prelude::*; +use assert_cmd::Command; #[test] fn lib_example() { @@ -8,7 +6,11 @@ fn lib_example() { cmd.assert().success(); let mut cmd = Command::cargo_bin("bin_fixture").unwrap(); - cmd.arg("-A").env("stdout", "hello").env("exit", "42"); - let assert = cmd.with_stdin().buffer("42").assert(); + let assert = cmd + .arg("-A") + .env("stdout", "hello") + .env("exit", "42") + .write_stdin("42") + .assert(); assert.failure().code(42).stdout("hello\n"); }