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}