Skip to main content

assert_cmd/
cmd.rs

1//! [`std::process::Command`] customized for testing.
2
3use std::ffi;
4use std::io;
5use std::io::{Read, Write};
6use std::ops::Deref;
7use std::path;
8use std::process;
9
10use crate::assert::Assert;
11use crate::assert::OutputAssertExt;
12use crate::output::DebugBuffer;
13use crate::output::DebugBytes;
14use crate::output::OutputError;
15use crate::output::OutputOkExt;
16use crate::output::OutputResult;
17
18/// [`std::process::Command`] customized for testing.
19#[derive(Debug)]
20pub struct Command {
21    cmd: process::Command,
22    stdin: Option<bstr::BString>,
23    timeout: Option<std::time::Duration>,
24}
25
26impl Command {
27    /// Constructs a new `Command` from a `std` `Command`.
28    pub fn from_std(cmd: process::Command) -> Self {
29        Self {
30            cmd,
31            stdin: None,
32            timeout: None,
33        }
34    }
35
36    /// Create a `Command` to run a specific binary of the current crate.
37    ///
38    /// See the [`cargo` module documentation][crate::cargo] for caveats and workarounds.
39    ///
40    /// Cargo support:
41    /// - `>1.94`: works
42    /// - `>=1.91,<=1.93`: works with default `build-dir`
43    /// - `<=1.92`: works
44    ///
45    /// # Panic
46    ///
47    /// Panicks if no binary is found
48    ///
49    /// # Examples
50    ///
51    /// ```rust,no_run
52    /// use assert_cmd::Command;
53    /// use assert_cmd::pkg_name;
54    ///
55    /// let mut cmd = Command::cargo_bin(pkg_name!())
56    ///     .unwrap();
57    /// let output = cmd.unwrap();
58    /// println!("{:?}", output);
59    /// ```
60    ///
61    /// ```rust,no_run
62    /// use assert_cmd::Command;
63    ///
64    /// let mut cmd = Command::cargo_bin("bin_fixture")
65    ///     .unwrap();
66    /// let output = cmd.unwrap();
67    /// println!("{:?}", output);
68    /// ```
69    ///
70    pub fn cargo_bin<S: AsRef<str>>(name: S) -> Result<Self, crate::cargo::CargoError> {
71        let cmd = crate::cargo::cargo_bin_cmd(name)?;
72        Ok(Self::from_std(cmd))
73    }
74
75    /// Write `buffer` to `stdin` when the `Command` is run.
76    ///
77    /// # Examples
78    ///
79    /// ```rust
80    /// use assert_cmd::Command;
81    ///
82    /// let mut cmd = Command::new("cat")
83    ///     .arg("-et")
84    ///     .write_stdin("42")
85    ///     .assert()
86    ///     .stdout("42");
87    /// ```
88    pub fn write_stdin<S>(&mut self, buffer: S) -> &mut Self
89    where
90        S: Into<Vec<u8>>,
91    {
92        self.stdin = Some(bstr::BString::from(buffer.into()));
93        self
94    }
95
96    /// Error out if a timeout is reached
97    ///
98    /// ```rust,no_run
99    /// use assert_cmd::Command;
100    ///
101    /// let assert = Command::cargo_bin("bin_fixture")
102    ///     .unwrap()
103    ///     .timeout(std::time::Duration::from_secs(1))
104    ///     .env("sleep", "100")
105    ///     .assert();
106    /// assert.failure();
107    /// ```
108    pub fn timeout(&mut self, timeout: std::time::Duration) -> &mut Self {
109        self.timeout = Some(timeout);
110        self
111    }
112
113    /// Write `path`s content to `stdin` when the `Command` is run.
114    ///
115    /// Paths are relative to the [`env::current_dir`][env_current_dir] and not
116    /// [`Command::current_dir`][Command_current_dir].
117    ///
118    /// [env_current_dir]: std::env::current_dir()
119    /// [Command_current_dir]: std::process::Command::current_dir()
120    pub fn pipe_stdin<P>(&mut self, file: P) -> io::Result<&mut Self>
121    where
122        P: AsRef<path::Path>,
123    {
124        let buffer = std::fs::read(file)?;
125        Ok(self.write_stdin(buffer))
126    }
127
128    /// Run a `Command`, returning an [`OutputResult`].
129    ///
130    /// # Examples
131    ///
132    /// ```rust
133    /// use assert_cmd::Command;
134    ///
135    /// let result = Command::new("echo")
136    ///     .args(&["42"])
137    ///     .ok();
138    /// assert!(result.is_ok());
139    /// ```
140    ///
141    pub fn ok(&mut self) -> OutputResult {
142        OutputOkExt::ok(self)
143    }
144
145    /// Run a `Command`, unwrapping the [`OutputResult`].
146    ///
147    /// # Examples
148    ///
149    /// ```rust
150    /// use assert_cmd::Command;
151    ///
152    /// let output = Command::new("echo")
153    ///     .args(&["42"])
154    ///     .unwrap();
155    /// ```
156    ///
157    #[track_caller]
158    pub fn unwrap(&mut self) -> process::Output {
159        OutputOkExt::unwrap(self)
160    }
161
162    /// Run a `Command`, unwrapping the error in the [`OutputResult`].
163    ///
164    /// # Examples
165    ///
166    /// ```rust,no_run
167    /// use assert_cmd::Command;
168    ///
169    /// let err = Command::new("a-command")
170    ///     .args(&["--will-fail"])
171    ///     .unwrap_err();
172    /// ```
173    ///
174    /// [Output]: std::process::Output
175    #[track_caller]
176    pub fn unwrap_err(&mut self) -> OutputError {
177        OutputOkExt::unwrap_err(self)
178    }
179
180    /// Run a `Command` and make assertions on the [`Output`].
181    ///
182    /// # Examples
183    ///
184    /// ```rust,no_run
185    /// use assert_cmd::Command;
186    ///
187    /// let mut cmd = Command::cargo_bin("bin_fixture")
188    ///     .unwrap()
189    ///     .assert()
190    ///     .success();
191    /// ```
192    ///
193    /// [`Output`]: std::process::Output
194    #[must_use]
195    #[track_caller]
196    pub fn assert(&mut self) -> Assert {
197        OutputAssertExt::assert(self)
198    }
199}
200
201/// Mirror [`std::process::Command`]'s API
202impl Command {
203    /// Constructs a new `Command` for launching the program at
204    /// path `program`, with the following default configuration:
205    ///
206    /// * No arguments to the program
207    /// * Inherit the current process's environment
208    /// * Inherit the current process's working directory
209    /// * Inherit stdin/stdout/stderr for `spawn` or `status`, but create pipes for `output`
210    ///
211    /// Builder methods are provided to change these defaults and
212    /// otherwise configure the process.
213    ///
214    /// If `program` is not an absolute path, the `PATH` will be searched in
215    /// an OS-defined way.
216    ///
217    /// The search path to be used may be controlled by setting the
218    /// `PATH` environment variable on the Command,
219    /// but this has some implementation limitations on Windows
220    /// (see issue #37519).
221    ///
222    /// # Examples
223    ///
224    /// Basic usage:
225    ///
226    /// ```no_run
227    /// use assert_cmd::Command;
228    ///
229    /// Command::new("sh").unwrap();
230    /// ```
231    pub fn new<S: AsRef<ffi::OsStr>>(program: S) -> Self {
232        let cmd = process::Command::new(program);
233        Self::from_std(cmd)
234    }
235
236    /// Adds an argument to pass to the program.
237    ///
238    /// Only one argument can be passed per use. So instead of:
239    ///
240    /// ```no_run
241    /// # assert_cmd::Command::new("sh")
242    /// .arg("-C /path/to/repo")
243    /// # ;
244    /// ```
245    ///
246    /// usage would be:
247    ///
248    /// ```no_run
249    /// # assert_cmd::Command::new("sh")
250    /// .arg("-C")
251    /// .arg("/path/to/repo")
252    /// # ;
253    /// ```
254    ///
255    /// To pass multiple arguments see [`args`].
256    ///
257    /// [`args`]: Command::args()
258    ///
259    /// # Examples
260    ///
261    /// Basic usage:
262    ///
263    /// ```no_run
264    /// use assert_cmd::Command;
265    ///
266    /// Command::new("ls")
267    ///         .arg("-l")
268    ///         .arg("-a")
269    ///         .unwrap();
270    /// ```
271    pub fn arg<S: AsRef<ffi::OsStr>>(&mut self, arg: S) -> &mut Self {
272        self.cmd.arg(arg);
273        self
274    }
275
276    /// Adds multiple arguments to pass to the program.
277    ///
278    /// To pass a single argument see [`arg`].
279    ///
280    /// [`arg`]: Command::arg()
281    ///
282    /// # Examples
283    ///
284    /// Basic usage:
285    ///
286    /// ```no_run
287    /// use assert_cmd::Command;
288    ///
289    /// Command::new("ls")
290    ///         .args(&["-l", "-a"])
291    ///         .unwrap();
292    /// ```
293    pub fn args<I, S>(&mut self, args: I) -> &mut Self
294    where
295        I: IntoIterator<Item = S>,
296        S: AsRef<ffi::OsStr>,
297    {
298        self.cmd.args(args);
299        self
300    }
301
302    /// Inserts or updates an environment variable mapping.
303    ///
304    /// Note that environment variable names are case-insensitive (but case-preserving) on Windows,
305    /// and case-sensitive on all other platforms.
306    ///
307    /// # Examples
308    ///
309    /// Basic usage:
310    ///
311    /// ```no_run
312    /// use assert_cmd::Command;
313    ///
314    /// Command::new("ls")
315    ///         .env("PATH", "/bin")
316    ///         .unwrap_err();
317    /// ```
318    pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
319    where
320        K: AsRef<ffi::OsStr>,
321        V: AsRef<ffi::OsStr>,
322    {
323        self.cmd.env(key, val);
324        self
325    }
326
327    /// Adds or updates multiple environment variable mappings.
328    ///
329    /// # Examples
330    ///
331    /// Basic usage:
332    ///
333    /// ```no_run
334    /// use assert_cmd::Command;
335    /// use std::process::Stdio;
336    /// use std::env;
337    /// use std::collections::HashMap;
338    ///
339    /// let filtered_env : HashMap<String, String> =
340    ///     env::vars().filter(|&(ref k, _)|
341    ///         k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH"
342    ///     ).collect();
343    ///
344    /// Command::new("printenv")
345    ///         .env_clear()
346    ///         .envs(&filtered_env)
347    ///         .unwrap();
348    /// ```
349    pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
350    where
351        I: IntoIterator<Item = (K, V)>,
352        K: AsRef<ffi::OsStr>,
353        V: AsRef<ffi::OsStr>,
354    {
355        self.cmd.envs(vars);
356        self
357    }
358
359    /// Removes an environment variable mapping.
360    ///
361    /// # Examples
362    ///
363    /// Basic usage:
364    ///
365    /// ```no_run
366    /// use assert_cmd::Command;
367    ///
368    /// Command::new("ls")
369    ///         .env_remove("PATH")
370    ///         .unwrap_err();
371    /// ```
372    pub fn env_remove<K: AsRef<ffi::OsStr>>(&mut self, key: K) -> &mut Self {
373        self.cmd.env_remove(key);
374        self
375    }
376
377    /// Clears the entire environment map for the child process.
378    ///
379    /// # Examples
380    ///
381    /// Basic usage:
382    ///
383    /// ```no_run
384    /// use assert_cmd::Command;
385    ///
386    /// Command::new("ls")
387    ///         .env_clear()
388    ///         .unwrap_err();
389    /// ```
390    pub fn env_clear(&mut self) -> &mut Self {
391        self.cmd.env_clear();
392        self
393    }
394
395    /// Sets the working directory for the child process.
396    ///
397    /// # Platform-specific behavior
398    ///
399    /// If the program path is relative (e.g., `"./script.sh"`), it's ambiguous
400    /// whether it should be interpreted relative to the parent's working
401    /// directory or relative to `current_dir`. The behavior in this case is
402    /// platform specific and unstable, and it's recommended to use
403    /// [`canonicalize`] to get an absolute program path instead.
404    ///
405    /// # Examples
406    ///
407    /// Basic usage:
408    ///
409    /// ```no_run
410    /// use assert_cmd::Command;
411    ///
412    /// Command::new("ls")
413    ///         .current_dir("/bin")
414    ///         .unwrap();
415    /// ```
416    ///
417    /// [`canonicalize`]: std::fs::canonicalize()
418    pub fn current_dir<P: AsRef<path::Path>>(&mut self, dir: P) -> &mut Self {
419        self.cmd.current_dir(dir);
420        self
421    }
422
423    /// Executes the `Command` as a child process, waiting for it to finish and collecting all of its
424    /// output.
425    ///
426    /// By default, stdout and stderr are captured (and used to provide the resulting output).
427    /// Stdin is not inherited from the parent and any attempt by the child process to read from
428    /// the stdin stream will result in the stream immediately closing.
429    ///
430    /// # Examples
431    ///
432    /// ```should_panic
433    /// use assert_cmd::Command;
434    /// use std::io::{self, Write};
435    /// let output = Command::new("/bin/cat")
436    ///                      .arg("file.txt")
437    ///                      .output()
438    ///                      .expect("failed to execute process");
439    ///
440    /// println!("status: {}", output.status);
441    /// io::stdout().write_all(&output.stdout).unwrap();
442    /// io::stderr().write_all(&output.stderr).unwrap();
443    ///
444    /// assert!(output.status.success());
445    /// ```
446    pub fn output(&mut self) -> io::Result<process::Output> {
447        let spawn = self.spawn()?;
448        Self::wait_with_input_output(spawn, self.stdin.as_deref().cloned(), self.timeout)
449    }
450
451    /// If `input`, write it to `child`'s stdin while also reading `child`'s
452    /// stdout and stderr, then wait on `child` and return its status and output.
453    ///
454    /// This was lifted from `std::process::Child::wait_with_output` and modified
455    /// to also write to stdin.
456    fn wait_with_input_output(
457        mut child: process::Child,
458        input: Option<Vec<u8>>,
459        timeout: Option<std::time::Duration>,
460    ) -> io::Result<process::Output> {
461        #![allow(clippy::unwrap_used, reason = "changes behavior in some tests")]
462
463        fn read<R>(mut input: R) -> std::thread::JoinHandle<io::Result<Vec<u8>>>
464        where
465            R: Read + Send + 'static,
466        {
467            std::thread::spawn(move || {
468                let mut ret = Vec::new();
469                input.read_to_end(&mut ret).map(|_| ret)
470            })
471        }
472
473        let stdin = input.and_then(|i| {
474            child
475                .stdin
476                .take()
477                .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
478        });
479        let stdout = child.stdout.take().map(read);
480        let stderr = child.stderr.take().map(read);
481
482        // Finish writing stdin before waiting, because waiting drops stdin.
483        stdin.and_then(|t| t.join().unwrap().ok());
484        let status = if let Some(timeout) = timeout {
485            wait_timeout::ChildExt::wait_timeout(&mut child, timeout)
486                .transpose()
487                .unwrap_or_else(|| {
488                    let _ = child.kill();
489                    child.wait()
490                })
491        } else {
492            child.wait()
493        }?;
494
495        let stdout = stdout
496            .and_then(|t| t.join().unwrap().ok())
497            .unwrap_or_default();
498        let stderr = stderr
499            .and_then(|t| t.join().unwrap().ok())
500            .unwrap_or_default();
501
502        Ok(process::Output {
503            status,
504            stdout,
505            stderr,
506        })
507    }
508
509    fn spawn(&mut self) -> io::Result<process::Child> {
510        // stdout/stderr should only be piped for `output` according to `process::Command::new`.
511        self.cmd.stdin(process::Stdio::piped());
512        self.cmd.stdout(process::Stdio::piped());
513        self.cmd.stderr(process::Stdio::piped());
514
515        self.cmd.spawn()
516    }
517
518    /// Returns the path to the program that was given to [`Command::new`].
519    ///
520    /// # Examples
521    ///
522    /// Basic usage:
523    ///
524    /// ```rust
525    /// use assert_cmd::Command;
526    ///
527    /// let cmd = Command::new("echo");
528    /// assert_eq!(cmd.get_program(), "echo");
529    /// ```
530    pub fn get_program(&self) -> &ffi::OsStr {
531        self.cmd.get_program()
532    }
533
534    /// Returns an iterator of the arguments that will be passed to the program.
535    ///
536    /// This does not include the path to the program as the first argument;
537    /// it only includes the arguments specified with [`Command::arg`] and
538    /// [`Command::args`].
539    ///
540    /// # Examples
541    ///
542    /// Basic usage:
543    ///
544    /// ```rust
545    /// use std::ffi::OsStr;
546    /// use assert_cmd::Command;
547    ///
548    /// let mut cmd = Command::new("echo");
549    /// cmd.arg("first").arg("second");
550    /// let args: Vec<&OsStr> = cmd.get_args().collect();
551    /// assert_eq!(args, &["first", "second"]);
552    /// ```
553    pub fn get_args(&self) -> process::CommandArgs<'_> {
554        self.cmd.get_args()
555    }
556
557    /// Returns an iterator of the environment variables explicitly set for the child process.
558    ///
559    /// Environment variables explicitly set using [`Command::env`], [`Command::envs`], and
560    /// [`Command::env_remove`] can be retrieved with this method.
561    ///
562    /// Note that this output does not include environment variables inherited from the parent
563    /// process.
564    ///
565    /// Each element is a tuple key/value pair `(&OsStr, Option<&OsStr>)`. A [`None`] value
566    /// indicates its key was explicitly removed via [`Command::env_remove`]. The associated key for
567    /// the [`None`] value will no longer inherit from its parent process.
568    ///
569    /// An empty iterator can indicate that no explicit mappings were added or that
570    /// [`Command::env_clear`] was called. After calling [`Command::env_clear`], the child process
571    /// will not inherit any environment variables from its parent process.
572    ///
573    /// # Examples
574    ///
575    /// Basic usage:
576    ///
577    /// ```rust
578    /// use std::ffi::OsStr;
579    /// use assert_cmd::Command;
580    ///
581    /// let mut cmd = Command::new("ls");
582    /// cmd.env("TERM", "dumb").env_remove("TZ");
583    /// let envs: Vec<(&OsStr, Option<&OsStr>)> = cmd.get_envs().collect();
584    /// assert_eq!(envs, &[
585    ///     (OsStr::new("TERM"), Some(OsStr::new("dumb"))),
586    ///     (OsStr::new("TZ"), None)
587    /// ]);
588    /// ```
589    pub fn get_envs(&self) -> process::CommandEnvs<'_> {
590        self.cmd.get_envs()
591    }
592
593    /// Returns the working directory for the child process.
594    ///
595    /// This returns [`None`] if the working directory will not be changed.
596    ///
597    /// # Examples
598    ///
599    /// Basic usage:
600    ///
601    /// ```rust
602    /// use std::path::Path;
603    /// use assert_cmd::Command;
604    ///
605    /// let mut cmd = Command::new("ls");
606    /// assert_eq!(cmd.get_current_dir(), None);
607    /// cmd.current_dir("/bin");
608    /// assert_eq!(cmd.get_current_dir(), Some(Path::new("/bin")));
609    /// ```
610    pub fn get_current_dir(&self) -> Option<&path::Path> {
611        self.cmd.get_current_dir()
612    }
613}
614
615impl From<process::Command> for Command {
616    fn from(cmd: process::Command) -> Self {
617        Self::from_std(cmd)
618    }
619}
620
621impl OutputOkExt for &mut Command {
622    fn ok(self) -> OutputResult {
623        let output = self.output().map_err(OutputError::with_cause)?;
624        if output.status.success() {
625            Ok(output)
626        } else {
627            let error = OutputError::new(output).set_cmd(format!("{:?}", self.cmd));
628            let error = if let Some(stdin) = self.stdin.as_ref() {
629                error.set_stdin(stdin.deref().clone())
630            } else {
631                error
632            };
633            Err(error)
634        }
635    }
636
637    #[track_caller]
638    fn unwrap_err(self) -> OutputError {
639        match self.ok() {
640            Ok(output) => {
641                if let Some(stdin) = self.stdin.as_ref() {
642                    panic!(
643                        "Completed successfully:\ncommand=`{:?}`\nstdin=```{}```\nstdout=```{}```",
644                        self.cmd,
645                        DebugBytes::new(stdin),
646                        DebugBytes::new(&output.stdout)
647                    )
648                } else {
649                    panic!(
650                        "Completed successfully:\ncommand=`{:?}`\nstdout=```{}```",
651                        self.cmd,
652                        DebugBytes::new(&output.stdout)
653                    )
654                }
655            }
656            Err(err) => err,
657        }
658    }
659}
660
661impl OutputAssertExt for &mut Command {
662    #[track_caller]
663    fn assert(self) -> Assert {
664        let output = match self.output() {
665            Ok(output) => output,
666            Err(err) => {
667                panic!("Failed to spawn {self:?}: {err}");
668            }
669        };
670        let assert = Assert::new(output).append_context("command", format!("{:?}", self.cmd));
671        if let Some(stdin) = self.stdin.as_ref() {
672            assert.append_context("stdin", DebugBuffer::new(stdin.deref().clone()))
673        } else {
674            assert
675        }
676    }
677}