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/assert.rs b/src/assert.rs index ce98e04..bb65713 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -11,8 +11,8 @@ use predicates::str::PredicateStrExt; use predicates_core; use predicates_tree::CaseTreeExt; -use crate::cmd::dump_buffer; -use crate::cmd::output_fmt; +use crate::output::dump_buffer; +use crate::output::output_fmt; /// Assert the state of an [`Output`]. /// 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 index 8a72591..e1f2bf3 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1,41 +1,108 @@ -//! Simplify one-off runs of programs. +//! [`std::process::Command`][Command] customized for testing. +//! +//! [Command]: https://doc.rust-lang.org/std/process/struct.Command.html -use std::error::Error; -use std::fmt; +use std::ffi; +use std::io; +use std::io::Write; +use std::path; use std::process; -use std::str; -/// Converts a type to an [`OutputResult`]. -/// -/// This is for example implemented on [`std::process::Output`]. -/// -/// # Examples -/// -/// ```rust -/// use assert_cmd::prelude::*; -/// -/// use std::process::Command; -/// -/// let result = Command::new("echo") -/// .args(&["42"]) -/// .ok(); -/// assert!(result.is_ok()); -/// ``` +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. /// -/// [`std::process::Output`]: https://doc.rust-lang.org/std/process/struct.Output.html -/// [`OutputResult`]: type.OutputResult.html -pub trait OutputOkExt -where - Self: ::std::marker::Sized, -{ - /// Convert an [`Output`][Output] to an [`OutputResult`][OutputResult]. +/// [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::prelude::*; + /// 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. /// - /// use std::process::Command; + /// 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"]) @@ -43,40 +110,34 @@ where /// assert!(result.is_ok()); /// ``` /// - /// [Output]: https://doc.rust-lang.org/std/process/struct.Output.html /// [OutputResult]: type.OutputResult.html - fn ok(self) -> OutputResult; + pub fn ok(&mut self) -> OutputResult { + OutputOkExt::ok(self) + } - /// Unwrap a [`Output`][Output] but with a prettier message than `.ok().unwrap()`. + /// Run a `Command`, unwrapping the [`OutputResult`][OutputResult]. /// /// # Examples /// /// ```rust - /// use assert_cmd::prelude::*; - /// - /// use std::process::Command; + /// use assert_cmd::Command; /// /// let output = Command::new("echo") /// .args(&["42"]) /// .unwrap(); /// ``` /// - /// [Output]: https://doc.rust-lang.org/std/process/struct.Output.html - fn unwrap(self) -> process::Output { - match self.ok() { - Ok(output) => output, - Err(err) => panic!("{}", err), - } + /// [OutputResult]: type.OutputResult.html + pub fn unwrap(&mut self) -> process::Output { + OutputOkExt::unwrap(self) } - /// Unwrap a [`Output`][Output] but with a prettier message than `ok().err().unwrap()`. + /// Run a `Command`, unwrapping the error in the [`OutputResult`][OutputResult]. /// /// # Examples /// /// ```rust,no_run - /// use assert_cmd::prelude::*; - /// - /// use std::process::Command; + /// use assert_cmd::Command; /// /// let err = Command::new("a-command") /// .args(&["--will-fail"]) @@ -84,267 +145,352 @@ where /// ``` /// /// [Output]: https://doc.rust-lang.org/std/process/struct.Output.html - fn unwrap_err(self) -> OutputError { - match self.ok() { - Ok(output) => panic!( - "Command completed successfully\nstdout=```{}```", - dump_buffer(&output.stdout) - ), - Err(err) => err, - } + pub fn unwrap_err(&mut self) -> OutputError { + OutputOkExt::unwrap_err(self) } -} -impl OutputOkExt for process::Output { - fn ok(self) -> OutputResult { - if self.status.success() { - Ok(self) - } else { - let error = OutputError::new(self); - Err(error) - } - } -} - -impl<'c> OutputOkExt for &'c mut process::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)); - Err(error) - } - } - - fn unwrap_err(self) -> OutputError { - match self.ok() { - Ok(output) => panic!( - "Completed successfully:\ncommand=`{:?}`\nstdout=```{}```", - self, - dump_buffer(&output.stdout) - ), - Err(err) => err, - } + /// 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) } } -/// [`Output`] represented as a [`Result`]. -/// -/// Generally produced by [`OutputOkExt`]. -/// -/// # Examples +/// Mirror [`std::process::Command`][Command]'s API /// -/// ```rust -/// use assert_cmd::prelude::*; -/// -/// use std::process::Command; -/// -/// let result = Command::new("echo") -/// .args(&["42"]) -/// .ok(); -/// assert!(result.is_ok()); -/// ``` -/// -/// [`Output`]: https://doc.rust-lang.org/std/process/struct.Output.html -/// [`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html -/// [`OutputOkExt`]: trait.OutputOkExt.html -pub type OutputResult = Result; - -/// [`Command`] error. -/// -/// Generally produced by [`OutputOkExt`]. -/// -/// # Examples -/// -/// ```rust,no_run -/// use assert_cmd::prelude::*; -/// -/// use std::process::Command; -/// -/// let err = Command::new("a-command") -/// .args(&["--will-fail"]) -/// .unwrap_err(); -/// ``` -/// -/// [`Command`]: https://doc.rust-lang.org/std/process/struct.Command.html -/// [`OutputOkExt`]: trait.OutputOkExt.html -#[derive(Debug)] -pub struct OutputError { - cmd: Option, - stdin: Option>, - cause: OutputCause, -} +/// [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) + } -impl OutputError { - /// Convert [`Output`] into an [`Error`]. + /// Adds an argument to pass to the program. /// - /// [`Output`]: https://doc.rust-lang.org/std/process/struct.Output.html - /// [`Error`]: https://doc.rust-lang.org/std/error/trait.Error.html - pub fn new(output: process::Output) -> Self { - Self { - cmd: None, - stdin: None, - cause: OutputCause::Expected(Output { output }), - } + /// 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 } - /// For errors that happen in creating a [`Output`]. + /// Adds multiple arguments to pass to the program. /// - /// [`Output`]: https://doc.rust-lang.org/std/process/struct.Output.html - pub fn with_cause(cause: E) -> Self + /// 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 - E: Error + Send + Sync + 'static, + I: IntoIterator, + S: AsRef, { - Self { - cmd: None, - stdin: None, - cause: OutputCause::Unexpected(Box::new(cause)), - } + self.cmd.args(args); + self } - /// Add the command line for additional context. - pub fn set_cmd(mut self, cmd: String) -> Self { - self.cmd = Some(cmd); + /// 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 } - /// Add the `stdin` for additional context. - pub fn set_stdin(mut self, stdin: Vec) -> Self { - self.stdin = Some(stdin); + /// 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 } - /// Access the contained [`Output`]. + /// Removes an environment variable mapping. /// /// # Examples /// - /// ```rust,no_run - /// use assert_cmd::prelude::*; + /// Basic usage: /// - /// use std::process::Command; + /// ```no_run + /// use assert_cmd::Command; /// - /// let err = Command::new("a-command") - /// .args(&["--will-fail"]) - /// .unwrap_err(); - /// let output = err - /// .as_output() - /// .unwrap(); - /// assert_eq!(Some(42), output.status.code()); + /// Command::new("ls") + /// .env_remove("PATH") + /// .unwrap_err(); /// ``` - /// - /// [`Output`]: https://doc.rust-lang.org/std/process/struct.Output.html - pub fn as_output(&self) -> Option<&process::Output> { - match self.cause { - OutputCause::Expected(ref e) => Some(&e.output), - OutputCause::Unexpected(_) => None, - } + pub fn env_remove>(&mut self, key: K) -> &mut Self { + self.cmd.env_remove(key); + self } -} -impl Error for OutputError { - fn description(&self) -> &str { - "Command failed." - } + /// Clears the entire environment map for the child process. + /// + /// # Examples + /// + /// Basic usage: + /// + /// use std::process::Command;; - fn cause(&self) -> Option<&dyn Error> { - if let OutputCause::Unexpected(ref err) = self.cause { - Some(err.as_ref()) - } else { - None - } + /// + /// Command::new("ls") + /// .env_clear() + /// .unwrap_err(); + /// ``` + pub fn env_clear(&mut self) -> &mut Self { + self.cmd.env_clear(); + self } -} -impl fmt::Display for OutputError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(ref cmd) = self.cmd { - writeln!(f, "command=`{}`", cmd)?; - } - if let Some(ref stdin) = self.stdin { - if let Ok(stdin) = str::from_utf8(stdin) { - writeln!(f, "stdin=```{}```", stdin)?; - } else { - writeln!(f, "stdin=```{:?}```", stdin)?; - } - } - write!(f, "{}", self.cause) + /// 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 } -} -#[derive(Debug)] -enum OutputCause { - Expected(Output), - Unexpected(Box), -} - -impl fmt::Display for OutputCause { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - OutputCause::Expected(ref e) => write!(f, "{}", e), - OutputCause::Unexpected(ref e) => write!(f, "{}", e), - } + /// 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() } -} -#[derive(Debug)] -struct Output { - output: process::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()); -impl fmt::Display for Output { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - output_fmt(&self.output, f) - } -} + let mut spawned = self.cmd.spawn()?; -pub(crate) fn output_fmt(output: &process::Output, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(code) = output.status.code() { - writeln!(f, "code={}", code)?; - } else { - writeln!(f, "code=")?; + 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) } - - write!(f, "stdout=```")?; - write_buffer(&output.stdout, f)?; - writeln!(f, "```")?; - - write!(f, "stderr=```")?; - write_buffer(&output.stderr, f)?; - writeln!(f, "```")?; - - Ok(()) } -pub(crate) fn dump_buffer(buffer: &[u8]) -> String { - if let Ok(buffer) = str::from_utf8(buffer) { - buffer.to_string() - } else { - format!("{:?}", buffer) +impl From for Command { + fn from(cmd: process::Command) -> Self { + Command::from_std(cmd) } } -pub(crate) fn write_buffer(buffer: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Ok(buffer) = str::from_utf8(buffer) { - write!(f, "{}", buffer) - } else { - write!(f, "{:?}", buffer) +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) + } } -} - -#[derive(Debug)] -pub(crate) struct DebugBuffer { - buffer: Vec, -} -impl DebugBuffer { - pub(crate) fn new(buffer: Vec) -> Self { - DebugBuffer { buffer } + 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 fmt::Display for DebugBuffer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write_buffer(&self.buffer, f) +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 4dffbe3..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 { () => { @@ -145,16 +137,15 @@ macro_rules! crate_name { pub mod assert; pub mod cargo; pub mod cmd; -pub mod stdin; +pub mod output; /// Extension traits that are useful to have available. pub mod prelude { pub use crate::assert::OutputAssertExt; pub use crate::cargo::CommandCargoExt; - pub use crate::cmd::OutputOkExt; - pub use crate::stdin::CommandStdInExt; + pub use crate::output::OutputOkExt; } -#[macro_use] -extern crate doc_comment; -doctest!("../README.md"); +pub use crate::cmd::Command; + +doc_comment::doctest!("../README.md"); diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..8a72591 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,350 @@ +//! Simplify one-off runs of programs. + +use std::error::Error; +use std::fmt; +use std::process; +use std::str; + +/// Converts a type to an [`OutputResult`]. +/// +/// This is for example implemented on [`std::process::Output`]. +/// +/// # Examples +/// +/// ```rust +/// use assert_cmd::prelude::*; +/// +/// use std::process::Command; +/// +/// let result = Command::new("echo") +/// .args(&["42"]) +/// .ok(); +/// assert!(result.is_ok()); +/// ``` +/// +/// [`std::process::Output`]: https://doc.rust-lang.org/std/process/struct.Output.html +/// [`OutputResult`]: type.OutputResult.html +pub trait OutputOkExt +where + Self: ::std::marker::Sized, +{ + /// Convert an [`Output`][Output] to an [`OutputResult`][OutputResult]. + /// + /// # Examples + /// + /// ```rust + /// use assert_cmd::prelude::*; + /// + /// use std::process::Command; + /// + /// let result = Command::new("echo") + /// .args(&["42"]) + /// .ok(); + /// assert!(result.is_ok()); + /// ``` + /// + /// [Output]: https://doc.rust-lang.org/std/process/struct.Output.html + /// [OutputResult]: type.OutputResult.html + fn ok(self) -> OutputResult; + + /// Unwrap a [`Output`][Output] but with a prettier message than `.ok().unwrap()`. + /// + /// # Examples + /// + /// ```rust + /// use assert_cmd::prelude::*; + /// + /// use std::process::Command; + /// + /// let output = Command::new("echo") + /// .args(&["42"]) + /// .unwrap(); + /// ``` + /// + /// [Output]: https://doc.rust-lang.org/std/process/struct.Output.html + fn unwrap(self) -> process::Output { + match self.ok() { + Ok(output) => output, + Err(err) => panic!("{}", err), + } + } + + /// Unwrap a [`Output`][Output] but with a prettier message than `ok().err().unwrap()`. + /// + /// # Examples + /// + /// ```rust,no_run + /// use assert_cmd::prelude::*; + /// + /// use std::process::Command; + /// + /// let err = Command::new("a-command") + /// .args(&["--will-fail"]) + /// .unwrap_err(); + /// ``` + /// + /// [Output]: https://doc.rust-lang.org/std/process/struct.Output.html + fn unwrap_err(self) -> OutputError { + match self.ok() { + Ok(output) => panic!( + "Command completed successfully\nstdout=```{}```", + dump_buffer(&output.stdout) + ), + Err(err) => err, + } + } +} + +impl OutputOkExt for process::Output { + fn ok(self) -> OutputResult { + if self.status.success() { + Ok(self) + } else { + let error = OutputError::new(self); + Err(error) + } + } +} + +impl<'c> OutputOkExt for &'c mut process::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)); + Err(error) + } + } + + fn unwrap_err(self) -> OutputError { + match self.ok() { + Ok(output) => panic!( + "Completed successfully:\ncommand=`{:?}`\nstdout=```{}```", + self, + dump_buffer(&output.stdout) + ), + Err(err) => err, + } + } +} + +/// [`Output`] represented as a [`Result`]. +/// +/// Generally produced by [`OutputOkExt`]. +/// +/// # Examples +/// +/// ```rust +/// use assert_cmd::prelude::*; +/// +/// use std::process::Command; +/// +/// let result = Command::new("echo") +/// .args(&["42"]) +/// .ok(); +/// assert!(result.is_ok()); +/// ``` +/// +/// [`Output`]: https://doc.rust-lang.org/std/process/struct.Output.html +/// [`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html +/// [`OutputOkExt`]: trait.OutputOkExt.html +pub type OutputResult = Result; + +/// [`Command`] error. +/// +/// Generally produced by [`OutputOkExt`]. +/// +/// # Examples +/// +/// ```rust,no_run +/// use assert_cmd::prelude::*; +/// +/// use std::process::Command; +/// +/// let err = Command::new("a-command") +/// .args(&["--will-fail"]) +/// .unwrap_err(); +/// ``` +/// +/// [`Command`]: https://doc.rust-lang.org/std/process/struct.Command.html +/// [`OutputOkExt`]: trait.OutputOkExt.html +#[derive(Debug)] +pub struct OutputError { + cmd: Option, + stdin: Option>, + cause: OutputCause, +} + +impl OutputError { + /// Convert [`Output`] into an [`Error`]. + /// + /// [`Output`]: https://doc.rust-lang.org/std/process/struct.Output.html + /// [`Error`]: https://doc.rust-lang.org/std/error/trait.Error.html + pub fn new(output: process::Output) -> Self { + Self { + cmd: None, + stdin: None, + cause: OutputCause::Expected(Output { output }), + } + } + + /// For errors that happen in creating a [`Output`]. + /// + /// [`Output`]: https://doc.rust-lang.org/std/process/struct.Output.html + pub fn with_cause(cause: E) -> Self + where + E: Error + Send + Sync + 'static, + { + Self { + cmd: None, + stdin: None, + cause: OutputCause::Unexpected(Box::new(cause)), + } + } + + /// Add the command line for additional context. + pub fn set_cmd(mut self, cmd: String) -> Self { + self.cmd = Some(cmd); + self + } + + /// Add the `stdin` for additional context. + pub fn set_stdin(mut self, stdin: Vec) -> Self { + self.stdin = Some(stdin); + self + } + + /// Access the contained [`Output`]. + /// + /// # Examples + /// + /// ```rust,no_run + /// use assert_cmd::prelude::*; + /// + /// use std::process::Command; + /// + /// let err = Command::new("a-command") + /// .args(&["--will-fail"]) + /// .unwrap_err(); + /// let output = err + /// .as_output() + /// .unwrap(); + /// assert_eq!(Some(42), output.status.code()); + /// ``` + /// + /// [`Output`]: https://doc.rust-lang.org/std/process/struct.Output.html + pub fn as_output(&self) -> Option<&process::Output> { + match self.cause { + OutputCause::Expected(ref e) => Some(&e.output), + OutputCause::Unexpected(_) => None, + } + } +} + +impl Error for OutputError { + fn description(&self) -> &str { + "Command failed." + } + + fn cause(&self) -> Option<&dyn Error> { + if let OutputCause::Unexpected(ref err) = self.cause { + Some(err.as_ref()) + } else { + None + } + } +} + +impl fmt::Display for OutputError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(ref cmd) = self.cmd { + writeln!(f, "command=`{}`", cmd)?; + } + if let Some(ref stdin) = self.stdin { + if let Ok(stdin) = str::from_utf8(stdin) { + writeln!(f, "stdin=```{}```", stdin)?; + } else { + writeln!(f, "stdin=```{:?}```", stdin)?; + } + } + write!(f, "{}", self.cause) + } +} + +#[derive(Debug)] +enum OutputCause { + Expected(Output), + Unexpected(Box), +} + +impl fmt::Display for OutputCause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + OutputCause::Expected(ref e) => write!(f, "{}", e), + OutputCause::Unexpected(ref e) => write!(f, "{}", e), + } + } +} + +#[derive(Debug)] +struct Output { + output: process::Output, +} + +impl fmt::Display for Output { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + output_fmt(&self.output, f) + } +} + +pub(crate) fn output_fmt(output: &process::Output, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(code) = output.status.code() { + writeln!(f, "code={}", code)?; + } else { + writeln!(f, "code=")?; + } + + write!(f, "stdout=```")?; + write_buffer(&output.stdout, f)?; + writeln!(f, "```")?; + + write!(f, "stderr=```")?; + write_buffer(&output.stderr, f)?; + writeln!(f, "```")?; + + Ok(()) +} + +pub(crate) fn dump_buffer(buffer: &[u8]) -> String { + if let Ok(buffer) = str::from_utf8(buffer) { + buffer.to_string() + } else { + format!("{:?}", buffer) + } +} + +pub(crate) fn write_buffer(buffer: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Ok(buffer) = str::from_utf8(buffer) { + write!(f, "{}", buffer) + } else { + write!(f, "{:?}", buffer) + } +} + +#[derive(Debug)] +pub(crate) struct DebugBuffer { + buffer: Vec, +} + +impl DebugBuffer { + pub(crate) fn new(buffer: Vec) -> Self { + DebugBuffer { buffer } + } +} + +impl fmt::Display for DebugBuffer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write_buffer(&self.buffer, f) + } +} diff --git a/src/stdin.rs b/src/stdin.rs deleted file mode 100644 index c3b9381..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::cmd::dump_buffer; -use crate::cmd::DebugBuffer; -use crate::cmd::OutputError; -use crate::cmd::OutputOkExt; -use crate::cmd::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"); }