blob: 750764e303cce062edf8e183f2bc2426dd412b98 [file] [log] [blame]
//! [`std::process::Command`][Command] customized for testing.
//!
//! [Command]: std::process::Command
use std::ffi;
use std::io;
use std::io::{Read, Write};
use std::ops::Deref;
use std::path;
use std::process;
use crate::assert::Assert;
use crate::assert::OutputAssertExt;
use crate::output::DebugBuffer;
use crate::output::DebugBytes;
use crate::output::OutputError;
use crate::output::OutputOkExt;
use crate::output::OutputResult;
/// [`std::process::Command`][Command] customized for testing.
///
/// [Command]: std::process::Command
#[derive(Debug)]
pub struct Command {
cmd: process::Command,
stdin: Option<bstr::BString>,
timeout: Option<std::time::Duration>,
}
impl Command {
/// Constructs a new `Command` from a `std` `Command`.
pub fn from_std(cmd: process::Command) -> Self {
Self {
cmd,
stdin: None,
timeout: None,
}
}
/// Create a `Command` to run a specific binary of the current crate.
///
/// See the [`cargo` module documentation][crate::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);
/// ```
///
pub fn cargo_bin<S: AsRef<str>>(name: S) -> Result<Self, crate::cargo::CargoError> {
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<S>(&mut self, buffer: S) -> &mut Self
where
S: Into<Vec<u8>>,
{
self.stdin = Some(bstr::BString::from(buffer.into()));
self
}
/// Error out if a timeout is reached
///
/// ```rust,no_run
/// use assert_cmd::Command;
///
/// let assert = Command::cargo_bin("bin_fixture")
/// .unwrap()
/// .timeout(std::time::Duration::from_secs(1))
/// .env("sleep", "100")
/// .assert();
/// assert.failure();
/// ```
pub fn timeout(&mut self, timeout: std::time::Duration) -> &mut Self {
self.timeout = Some(timeout);
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]: std::env::current_dir()
/// [Command_current_dir]: std::process::Command::current_dir()
pub fn pipe_stdin<P>(&mut self, file: P) -> io::Result<&mut Self>
where
P: AsRef<path::Path>,
{
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());
/// ```
///
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();
/// ```
///
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]: std::process::Output
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`]: std::process::Output
pub fn assert(&mut self) -> Assert {
OutputAssertExt::assert(self)
}
}
/// Mirror [`std::process::Command`][Command]'s API
///
/// [Command]: std::process::Command
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<S: AsRef<ffi::OsStr>>(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`]: Command::args()
///
/// # Examples
///
/// Basic usage:
///
/// ```no_run
/// use assert_cmd::Command;
///
/// Command::new("ls")
/// .arg("-l")
/// .arg("-a")
/// .unwrap();
/// ```
pub fn arg<S: AsRef<ffi::OsStr>>(&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`]: Command::arg()
///
/// # Examples
///
/// Basic usage:
///
/// ```no_run
/// use assert_cmd::Command;
///
/// Command::new("ls")
/// .args(&["-l", "-a"])
/// .unwrap();
/// ```
pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<ffi::OsStr>,
{
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<K, V>(&mut self, key: K, val: V) -> &mut Self
where
K: AsRef<ffi::OsStr>,
V: AsRef<ffi::OsStr>,
{
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<String, String> =
/// 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<I, K, V>(&mut self, vars: I) -> &mut Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<ffi::OsStr>,
V: AsRef<ffi::OsStr>,
{
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<K: AsRef<ffi::OsStr>>(&mut self, key: K) -> &mut Self {
self.cmd.env_remove(key);
self
}
/// Clears the entire environment map for the child process.
///
/// # Examples
///
/// Basic usage:
///
/// ```no_run
/// use assert_cmd::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`]: std::fs::canonicalize()
pub fn current_dir<P: AsRef<path::Path>>(&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<process::Output> {
let spawn = self.spawn()?;
Self::wait_with_input_output(spawn, self.stdin.as_deref().cloned(), self.timeout)
}
/// If `input`, write it to `child`'s stdin while also reading `child`'s
/// stdout and stderr, then wait on `child` and return its status and output.
///
/// This was lifted from `std::process::Child::wait_with_output` and modified
/// to also write to stdin.
fn wait_with_input_output(
mut child: process::Child,
input: Option<Vec<u8>>,
timeout: Option<std::time::Duration>,
) -> io::Result<process::Output> {
let stdin = input.and_then(|i| {
child
.stdin
.take()
.map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
});
fn read<R>(mut input: R) -> std::thread::JoinHandle<io::Result<Vec<u8>>>
where
R: Read + Send + 'static,
{
std::thread::spawn(move || {
let mut ret = Vec::new();
input.read_to_end(&mut ret).map(|_| ret)
})
}
let stdout = child.stdout.take().map(read);
let stderr = child.stderr.take().map(read);
// Finish writing stdin before waiting, because waiting drops stdin.
stdin.and_then(|t| t.join().unwrap().ok());
let status = if let Some(timeout) = timeout {
wait_timeout::ChildExt::wait_timeout(&mut child, timeout)
.transpose()
.unwrap_or_else(|| {
let _ = child.kill();
child.wait()
})
} else {
child.wait()
}?;
let stdout = stdout
.and_then(|t| t.join().unwrap().ok())
.unwrap_or_default();
let stderr = stderr
.and_then(|t| t.join().unwrap().ok())
.unwrap_or_default();
Ok(process::Output {
status,
stdout,
stderr,
})
}
fn spawn(&mut self) -> io::Result<process::Child> {
// 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());
self.cmd.spawn()
}
}
impl From<process::Command> 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.deref().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,
DebugBytes::new(stdin),
DebugBytes::new(&output.stdout)
)
} else {
panic!(
"Completed successfully:\ncommand=`{:?}`\nstdout=```{}```",
self.cmd,
DebugBytes::new(&output.stdout)
)
}
}
Err(err) => err,
}
}
}
impl<'c> OutputAssertExt for &'c mut Command {
fn assert(self) -> Assert {
let output = match self.output() {
Ok(output) => output,
Err(err) => {
panic!("Failed to spawn {:?}: {}", self, err);
}
};
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.deref().clone()))
} else {
assert
}
}
}