Skip to main content

snapbox/
cmd.rs

1//! Run commands and assert on their behavior
2
3#[cfg(feature = "color")]
4use anstream::panic;
5
6use crate::IntoData;
7
8/// Process spawning for testing of non-interactive commands
9#[derive(Debug)]
10pub struct Command {
11    cmd: std::process::Command,
12    stdin: Option<crate::Data>,
13    timeout: Option<std::time::Duration>,
14    _stderr_to_stdout: bool,
15    config: crate::Assert,
16}
17
18/// # Builder API
19impl Command {
20    pub fn new(program: impl AsRef<std::ffi::OsStr>) -> Self {
21        Self {
22            cmd: std::process::Command::new(program),
23            stdin: None,
24            timeout: None,
25            _stderr_to_stdout: false,
26            config: crate::Assert::new().action_env(crate::assert::DEFAULT_ACTION_ENV),
27        }
28    }
29
30    /// Constructs a new `Command` from a `std` `Command`.
31    pub fn from_std(cmd: std::process::Command) -> Self {
32        Self {
33            cmd,
34            stdin: None,
35            timeout: None,
36            _stderr_to_stdout: false,
37            config: crate::Assert::new().action_env(crate::assert::DEFAULT_ACTION_ENV),
38        }
39    }
40
41    /// Customize the assertion behavior
42    pub fn with_assert(mut self, config: crate::Assert) -> Self {
43        self.config = config;
44        self
45    }
46
47    /// Adds an argument to pass to the program.
48    ///
49    /// Only one argument can be passed per use. So instead of:
50    ///
51    /// ```no_run
52    /// # snapbox::cmd::Command::new("sh")
53    /// .arg("-C /path/to/repo")
54    /// # ;
55    /// ```
56    ///
57    /// usage would be:
58    ///
59    /// ```no_run
60    /// # snapbox::cmd::Command::new("sh")
61    /// .arg("-C")
62    /// .arg("/path/to/repo")
63    /// # ;
64    /// ```
65    ///
66    /// To pass multiple arguments see [`args`].
67    ///
68    /// [`args`]: Command::args()
69    ///
70    /// # Examples
71    ///
72    /// Basic usage:
73    ///
74    /// ```no_run
75    /// use snapbox::cmd::Command;
76    ///
77    /// Command::new("ls")
78    ///         .arg("-l")
79    ///         .arg("-a")
80    ///         .assert()
81    ///         .success();
82    /// ```
83    pub fn arg(mut self, arg: impl AsRef<std::ffi::OsStr>) -> Self {
84        self.cmd.arg(arg);
85        self
86    }
87
88    /// Adds multiple arguments to pass to the program.
89    ///
90    /// To pass a single argument see [`arg`].
91    ///
92    /// [`arg`]: Command::arg()
93    ///
94    /// # Examples
95    ///
96    /// Basic usage:
97    ///
98    /// ```no_run
99    /// use snapbox::cmd::Command;
100    ///
101    /// Command::new("ls")
102    ///         .args(&["-l", "-a"])
103    ///         .assert()
104    ///         .success();
105    /// ```
106    pub fn args(mut self, args: impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>>) -> Self {
107        self.cmd.args(args);
108        self
109    }
110
111    /// Inserts or updates an environment variable mapping.
112    ///
113    /// Note that environment variable names are case-insensitive (but case-preserving) on Windows,
114    /// and case-sensitive on all other platforms.
115    ///
116    /// # Examples
117    ///
118    /// Basic usage:
119    ///
120    /// ```no_run
121    /// use snapbox::cmd::Command;
122    ///
123    /// Command::new("ls")
124    ///         .env("PATH", "/bin")
125    ///         .assert()
126    ///         .failure();
127    /// ```
128    pub fn env(
129        mut self,
130        key: impl AsRef<std::ffi::OsStr>,
131        value: impl AsRef<std::ffi::OsStr>,
132    ) -> Self {
133        self.cmd.env(key, value);
134        self
135    }
136
137    /// Adds or updates multiple environment variable mappings.
138    ///
139    /// # Examples
140    ///
141    /// Basic usage:
142    ///
143    /// ```no_run
144    /// use snapbox::cmd::Command;
145    /// use std::process::Stdio;
146    /// use std::env;
147    /// use std::collections::HashMap;
148    ///
149    /// let filtered_env : HashMap<String, String> =
150    ///     env::vars().filter(|&(ref k, _)|
151    ///         k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH"
152    ///     ).collect();
153    ///
154    /// Command::new("printenv")
155    ///         .env_clear()
156    ///         .envs(&filtered_env)
157    ///         .assert()
158    ///         .success();
159    /// ```
160    pub fn envs(
161        mut self,
162        vars: impl IntoIterator<Item = (impl AsRef<std::ffi::OsStr>, impl AsRef<std::ffi::OsStr>)>,
163    ) -> Self {
164        self.cmd.envs(vars);
165        self
166    }
167
168    /// Removes an environment variable mapping.
169    ///
170    /// # Examples
171    ///
172    /// Basic usage:
173    ///
174    /// ```no_run
175    /// use snapbox::cmd::Command;
176    ///
177    /// Command::new("ls")
178    ///         .env_remove("PATH")
179    ///         .assert()
180    ///         .failure();
181    /// ```
182    pub fn env_remove(mut self, key: impl AsRef<std::ffi::OsStr>) -> Self {
183        self.cmd.env_remove(key);
184        self
185    }
186
187    /// Clears the entire environment map for the child process.
188    ///
189    /// # Examples
190    ///
191    /// Basic usage:
192    ///
193    /// ```no_run
194    /// use snapbox::cmd::Command;
195    ///
196    /// Command::new("ls")
197    ///         .env_clear()
198    ///         .assert()
199    ///         .failure();
200    /// ```
201    pub fn env_clear(mut self) -> Self {
202        self.cmd.env_clear();
203        self
204    }
205
206    /// Sets the working directory for the child process.
207    ///
208    /// # Platform-specific behavior
209    ///
210    /// If the program path is relative (e.g., `"./script.sh"`), it's ambiguous
211    /// whether it should be interpreted relative to the parent's working
212    /// directory or relative to `current_dir`. The behavior in this case is
213    /// platform specific and unstable, and it's recommended to use
214    /// [`canonicalize`] to get an absolute program path instead.
215    ///
216    /// # Examples
217    ///
218    /// Basic usage:
219    ///
220    /// ```no_run
221    /// use snapbox::cmd::Command;
222    ///
223    /// Command::new("ls")
224    ///         .current_dir("/bin")
225    ///         .assert()
226    ///         .success();
227    /// ```
228    ///
229    /// [`canonicalize`]: std::fs::canonicalize()
230    pub fn current_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
231        self.cmd.current_dir(dir);
232        self
233    }
234
235    /// Write `buffer` to `stdin` when the `Command` is run.
236    ///
237    /// # Examples
238    ///
239    /// ```rust
240    /// use snapbox::cmd::Command;
241    ///
242    /// let mut cmd = Command::new("cat")
243    ///     .arg("-et")
244    ///     .stdin("42")
245    ///     .assert()
246    ///     .stdout_eq("42");
247    /// ```
248    pub fn stdin(mut self, stream: impl IntoData) -> Self {
249        self.stdin = Some(stream.into_data());
250        self
251    }
252
253    /// Error out if a timeout is reached
254    ///
255    /// ```rust,no_run
256    /// use snapbox::cmd::Command;
257    /// use snapbox::cmd::cargo_bin;
258    ///
259    /// let assert = Command::new(cargo_bin("snap-fixture"))
260    ///     .timeout(std::time::Duration::from_secs(1))
261    ///     .env("sleep", "100")
262    ///     .assert()
263    ///     .failure();
264    /// ```
265    #[cfg(feature = "cmd")]
266    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
267        self.timeout = Some(timeout);
268        self
269    }
270
271    /// Merge `stderr` into `stdout`
272    #[cfg(feature = "cmd")]
273    pub fn stderr_to_stdout(mut self) -> Self {
274        self._stderr_to_stdout = true;
275        self
276    }
277}
278
279/// # Run Command
280impl Command {
281    /// Run the command and assert on the results
282    ///
283    /// ```rust
284    /// use snapbox::cmd::Command;
285    ///
286    /// let mut cmd = Command::new("cat")
287    ///     .arg("-et")
288    ///     .stdin("42")
289    ///     .assert()
290    ///     .stdout_eq("42");
291    /// ```
292    #[track_caller]
293    #[must_use]
294    pub fn assert(self) -> OutputAssert {
295        let config = self.config.clone();
296        match self.output() {
297            Ok(output) => OutputAssert::new(output).with_assert(config),
298            Err(err) => {
299                panic!("Failed to spawn: {}", err)
300            }
301        }
302    }
303
304    /// Run the command and capture the `Output`
305    #[cfg(feature = "cmd")]
306    pub fn output(self) -> Result<std::process::Output, std::io::Error> {
307        if self._stderr_to_stdout {
308            self.single_output()
309        } else {
310            self.split_output()
311        }
312    }
313
314    #[cfg(not(feature = "cmd"))]
315    pub fn output(self) -> Result<std::process::Output, std::io::Error> {
316        self.split_output()
317    }
318
319    #[cfg(feature = "cmd")]
320    fn single_output(mut self) -> Result<std::process::Output, std::io::Error> {
321        self.cmd.stdin(std::process::Stdio::piped());
322        let (reader, writer) = os_pipe::pipe()?;
323        let writer_clone = writer.try_clone()?;
324        self.cmd.stdout(writer);
325        self.cmd.stderr(writer_clone);
326        let mut child = self.cmd.spawn()?;
327        // Avoid a deadlock! This parent process is still holding open pipe
328        // writers (inside the Command object), and we have to close those
329        // before we read. Here we do this by dropping the Command object.
330        drop(self.cmd);
331
332        let stdin = self
333            .stdin
334            .as_ref()
335            .map(|d| d.to_bytes())
336            .transpose()
337            .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
338        let stdout = process_single_io(&mut child, reader, stdin)?;
339
340        let status = wait(child, self.timeout)?;
341        let stdout = stdout.join().unwrap().ok().unwrap_or_default();
342
343        Ok(std::process::Output {
344            status,
345            stdout,
346            stderr: Default::default(),
347        })
348    }
349
350    fn split_output(mut self) -> Result<std::process::Output, std::io::Error> {
351        self.cmd.stdin(std::process::Stdio::piped());
352        self.cmd.stdout(std::process::Stdio::piped());
353        self.cmd.stderr(std::process::Stdio::piped());
354        let mut child = self.cmd.spawn()?;
355
356        let stdin = self
357            .stdin
358            .as_ref()
359            .map(|d| d.to_bytes())
360            .transpose()
361            .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
362        let (stdout, stderr) = process_split_io(&mut child, stdin)?;
363
364        let status = wait(child, self.timeout)?;
365        let stdout = stdout
366            .and_then(|t| t.join().unwrap().ok())
367            .unwrap_or_default();
368        let stderr = stderr
369            .and_then(|t| t.join().unwrap().ok())
370            .unwrap_or_default();
371
372        Ok(std::process::Output {
373            status,
374            stdout,
375            stderr,
376        })
377    }
378}
379
380fn process_split_io(
381    child: &mut std::process::Child,
382    input: Option<Vec<u8>>,
383) -> std::io::Result<(Option<Stream>, Option<Stream>)> {
384    use std::io::Write;
385
386    let stdin = input.and_then(|i| {
387        child
388            .stdin
389            .take()
390            .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
391    });
392    let stdout = child.stdout.take().map(threaded_read);
393    let stderr = child.stderr.take().map(threaded_read);
394
395    // Finish writing stdin before waiting, because waiting drops stdin.
396    stdin.and_then(|t| t.join().unwrap().ok());
397
398    Ok((stdout, stderr))
399}
400
401#[cfg(feature = "cmd")]
402fn process_single_io(
403    child: &mut std::process::Child,
404    stdout: os_pipe::PipeReader,
405    input: Option<Vec<u8>>,
406) -> std::io::Result<Stream> {
407    use std::io::Write;
408
409    let stdin = input.and_then(|i| {
410        child
411            .stdin
412            .take()
413            .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
414    });
415    let stdout = threaded_read(stdout);
416    debug_assert!(child.stdout.is_none());
417    debug_assert!(child.stderr.is_none());
418
419    // Finish writing stdin before waiting, because waiting drops stdin.
420    stdin.and_then(|t| t.join().unwrap().ok());
421
422    Ok(stdout)
423}
424
425type Stream = std::thread::JoinHandle<Result<Vec<u8>, std::io::Error>>;
426
427fn threaded_read<R>(mut input: R) -> Stream
428where
429    R: std::io::Read + Send + 'static,
430{
431    std::thread::spawn(move || {
432        let mut ret = Vec::new();
433        input.read_to_end(&mut ret).map(|_| ret)
434    })
435}
436
437impl From<std::process::Command> for Command {
438    fn from(cmd: std::process::Command) -> Self {
439        Self::from_std(cmd)
440    }
441}
442
443/// Assert the state of a [`Command`]'s [`Output`].
444///
445/// Create an `OutputAssert` through the [`Command::assert`].
446///
447/// [`Output`]: std::process::Output
448pub struct OutputAssert {
449    output: std::process::Output,
450    config: crate::Assert,
451}
452
453impl OutputAssert {
454    /// Create an `Assert` for a given [`Output`].
455    ///
456    /// [`Output`]: std::process::Output
457    pub fn new(output: std::process::Output) -> Self {
458        Self {
459            output,
460            config: crate::Assert::new().action_env(crate::assert::DEFAULT_ACTION_ENV),
461        }
462    }
463
464    /// Customize the assertion behavior
465    pub fn with_assert(mut self, config: crate::Assert) -> Self {
466        self.config = config;
467        self
468    }
469
470    /// Access the contained [`Output`].
471    ///
472    /// [`Output`]: std::process::Output
473    pub fn get_output(&self) -> &std::process::Output {
474        &self.output
475    }
476
477    /// Ensure the command succeeded.
478    ///
479    /// ```rust,no_run
480    /// use snapbox::cmd::Command;
481    /// use snapbox::cmd::cargo_bin;
482    ///
483    /// let assert = Command::new(cargo_bin("snap-fixture"))
484    ///     .assert()
485    ///     .success();
486    /// ```
487    #[track_caller]
488    pub fn success(self) -> Self {
489        if !self.output.status.success() {
490            let desc = format!(
491                "Expected {}, was {}",
492                self.config.palette.info("success"),
493                self.config
494                    .palette
495                    .error(display_exit_status(self.output.status))
496            );
497
498            use std::fmt::Write;
499            let mut buf = String::new();
500            writeln!(&mut buf, "{desc}").unwrap();
501            self.write_stdout(&mut buf).unwrap();
502            self.write_stderr(&mut buf).unwrap();
503            panic!("{}", buf);
504        }
505        self
506    }
507
508    /// Ensure the command failed.
509    ///
510    /// ```rust,no_run
511    /// use snapbox::cmd::Command;
512    /// use snapbox::cmd::cargo_bin;
513    ///
514    /// let assert = Command::new(cargo_bin("snap-fixture"))
515    ///     .env("exit", "1")
516    ///     .assert()
517    ///     .failure();
518    /// ```
519    #[track_caller]
520    pub fn failure(self) -> Self {
521        if self.output.status.success() {
522            let desc = format!(
523                "Expected {}, was {}",
524                self.config.palette.info("failure"),
525                self.config.palette.error("success")
526            );
527
528            use std::fmt::Write;
529            let mut buf = String::new();
530            writeln!(&mut buf, "{desc}").unwrap();
531            self.write_stdout(&mut buf).unwrap();
532            self.write_stderr(&mut buf).unwrap();
533            panic!("{}", buf);
534        }
535        self
536    }
537
538    /// Ensure the command aborted before returning a code.
539    #[track_caller]
540    pub fn interrupted(self) -> Self {
541        if self.output.status.code().is_some() {
542            let desc = format!(
543                "Expected {}, was {}",
544                self.config.palette.info("interrupted"),
545                self.config
546                    .palette
547                    .error(display_exit_status(self.output.status))
548            );
549
550            use std::fmt::Write;
551            let mut buf = String::new();
552            writeln!(&mut buf, "{desc}").unwrap();
553            self.write_stdout(&mut buf).unwrap();
554            self.write_stderr(&mut buf).unwrap();
555            panic!("{}", buf);
556        }
557        self
558    }
559
560    /// Ensure the command returned the expected code.
561    ///
562    /// ```rust,no_run
563    /// use snapbox::cmd::Command;
564    /// use snapbox::cmd::cargo_bin;
565    ///
566    /// let assert = Command::new(cargo_bin("snap-fixture"))
567    ///     .env("exit", "42")
568    ///     .assert()
569    ///     .code(42);
570    /// ```
571    #[track_caller]
572    pub fn code(self, expected: i32) -> Self {
573        if self.output.status.code() != Some(expected) {
574            let desc = format!(
575                "Expected {}, was {}",
576                self.config.palette.info(expected),
577                self.config
578                    .palette
579                    .error(display_exit_status(self.output.status))
580            );
581
582            use std::fmt::Write;
583            let mut buf = String::new();
584            writeln!(&mut buf, "{desc}").unwrap();
585            self.write_stdout(&mut buf).unwrap();
586            self.write_stderr(&mut buf).unwrap();
587            panic!("{}", buf);
588        }
589        self
590    }
591
592    /// Ensure the command wrote the expected data to `stdout`.
593    ///
594    /// By default [`filters`][crate::filter] are applied, including:
595    /// - `...` is a line-wildcard when on a line by itself
596    /// - `[..]` is a character-wildcard when inside a line
597    /// - `[EXE]` matches `.exe` on Windows
598    /// - `"{...}"` is a JSON value wildcard
599    /// - `"...": "{...}"` is a JSON key-value wildcard
600    /// - `\` to `/`
601    /// - Newlines
602    ///
603    /// To limit this to newline normalization for text, call [`Data::raw`][crate::Data::raw] on `expected`.
604    ///
605    /// # Examples
606    ///
607    /// ```rust,no_run
608    /// use snapbox::cmd::Command;
609    /// use snapbox::cmd::cargo_bin;
610    ///
611    /// let assert = Command::new(cargo_bin("snap-fixture"))
612    ///     .env("stdout", "hello")
613    ///     .env("stderr", "world")
614    ///     .assert()
615    ///     .stdout_eq("he[..]o");
616    /// ```
617    ///
618    /// Can combine this with [`file!`][crate::file]
619    /// ```rust,no_run
620    /// use snapbox::cmd::Command;
621    /// use snapbox::cmd::cargo_bin;
622    /// use snapbox::file;
623    ///
624    /// let assert = Command::new(cargo_bin("snap-fixture"))
625    ///     .env("stdout", "hello")
626    ///     .env("stderr", "world")
627    ///     .assert()
628    ///     .stdout_eq(file!["stdout.log"]);
629    /// ```
630    #[track_caller]
631    pub fn stdout_eq(self, expected: impl IntoData) -> Self {
632        let expected = expected.into_data();
633        self.stdout_eq_inner(expected)
634    }
635
636    #[track_caller]
637    #[deprecated(since = "0.6.0", note = "Replaced with `OutputAssert::stdout_eq`")]
638    pub fn stdout_eq_(self, expected: impl IntoData) -> Self {
639        self.stdout_eq(expected)
640    }
641
642    #[track_caller]
643    fn stdout_eq_inner(self, expected: crate::Data) -> Self {
644        let actual = self.output.stdout.as_slice().into_data();
645        if let Err(err) = self.config.try_eq(Some(&"stdout"), actual, expected) {
646            err.panic();
647        }
648
649        self
650    }
651
652    /// Ensure the command wrote the expected data to `stderr`.
653    ///
654    /// By default [`filters`][crate::filter] are applied, including:
655    /// - `...` is a line-wildcard when on a line by itself
656    /// - `[..]` is a character-wildcard when inside a line
657    /// - `[EXE]` matches `.exe` on Windows
658    /// - `"{...}"` is a JSON value wildcard
659    /// - `"...": "{...}"` is a JSON key-value wildcard
660    /// - `\` to `/`
661    /// - Newlines
662    ///
663    /// To limit this to newline normalization for text, call [`Data::raw`][crate::Data::raw] on `expected`.
664    ///
665    /// # Examples
666    ///
667    /// ```rust,no_run
668    /// use snapbox::cmd::Command;
669    /// use snapbox::cmd::cargo_bin;
670    ///
671    /// let assert = Command::new(cargo_bin("snap-fixture"))
672    ///     .env("stdout", "hello")
673    ///     .env("stderr", "world")
674    ///     .assert()
675    ///     .stderr_eq("wo[..]d");
676    /// ```
677    ///
678    /// Can combine this with [`file!`][crate::file]
679    /// ```rust,no_run
680    /// use snapbox::cmd::Command;
681    /// use snapbox::cmd::cargo_bin;
682    /// use snapbox::file;
683    ///
684    /// let assert = Command::new(cargo_bin("snap-fixture"))
685    ///     .env("stdout", "hello")
686    ///     .env("stderr", "world")
687    ///     .assert()
688    ///     .stderr_eq(file!["stderr.log"]);
689    /// ```
690    #[track_caller]
691    pub fn stderr_eq(self, expected: impl IntoData) -> Self {
692        let expected = expected.into_data();
693        self.stderr_eq_inner(expected)
694    }
695
696    #[track_caller]
697    #[deprecated(since = "0.6.0", note = "Replaced with `OutputAssert::stderr_eq`")]
698    pub fn stderr_eq_(self, expected: impl IntoData) -> Self {
699        self.stderr_eq(expected)
700    }
701
702    #[track_caller]
703    fn stderr_eq_inner(self, expected: crate::Data) -> Self {
704        let actual = self.output.stderr.as_slice().into_data();
705        if let Err(err) = self.config.try_eq(Some(&"stderr"), actual, expected) {
706            err.panic();
707        }
708
709        self
710    }
711
712    fn write_stdout(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
713        if !self.output.stdout.is_empty() {
714            writeln!(writer, "stdout:")?;
715            writeln!(writer, "```")?;
716            writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stdout))?;
717            writeln!(writer, "```")?;
718        }
719        Ok(())
720    }
721
722    fn write_stderr(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
723        if !self.output.stderr.is_empty() {
724            writeln!(writer, "stderr:")?;
725            writeln!(writer, "```")?;
726            writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stderr))?;
727            writeln!(writer, "```")?;
728        }
729        Ok(())
730    }
731}
732
733/// Converts an [`std::process::ExitStatus`]  to a human-readable value
734#[cfg(not(feature = "cmd"))]
735pub fn display_exit_status(status: std::process::ExitStatus) -> String {
736    basic_exit_status(status)
737}
738
739/// Converts an [`std::process::ExitStatus`]  to a human-readable value
740#[cfg(feature = "cmd")]
741pub fn display_exit_status(status: std::process::ExitStatus) -> String {
742    #[cfg(unix)]
743    fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
744        use std::os::unix::process::ExitStatusExt;
745
746        let signal = status.signal()?;
747        let name = match signal as libc::c_int {
748            libc::SIGABRT => ", SIGABRT: process abort signal",
749            libc::SIGALRM => ", SIGALRM: alarm clock",
750            libc::SIGFPE => ", SIGFPE: erroneous arithmetic operation",
751            libc::SIGHUP => ", SIGHUP: hangup",
752            libc::SIGILL => ", SIGILL: illegal instruction",
753            libc::SIGINT => ", SIGINT: terminal interrupt signal",
754            libc::SIGKILL => ", SIGKILL: kill",
755            libc::SIGPIPE => ", SIGPIPE: write on a pipe with no one to read",
756            libc::SIGQUIT => ", SIGQUIT: terminal quit signal",
757            libc::SIGSEGV => ", SIGSEGV: invalid memory reference",
758            libc::SIGTERM => ", SIGTERM: termination signal",
759            libc::SIGBUS => ", SIGBUS: access to undefined memory",
760            #[cfg(not(target_os = "haiku"))]
761            libc::SIGSYS => ", SIGSYS: bad system call",
762            libc::SIGTRAP => ", SIGTRAP: trace/breakpoint trap",
763            _ => "",
764        };
765        Some(format!("signal: {signal}{name}"))
766    }
767
768    #[cfg(windows)]
769    fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
770        use windows_sys::Win32::Foundation::*;
771
772        let extra = match status.code().unwrap() as NTSTATUS {
773            STATUS_ACCESS_VIOLATION => "STATUS_ACCESS_VIOLATION",
774            STATUS_IN_PAGE_ERROR => "STATUS_IN_PAGE_ERROR",
775            STATUS_INVALID_HANDLE => "STATUS_INVALID_HANDLE",
776            STATUS_INVALID_PARAMETER => "STATUS_INVALID_PARAMETER",
777            STATUS_NO_MEMORY => "STATUS_NO_MEMORY",
778            STATUS_ILLEGAL_INSTRUCTION => "STATUS_ILLEGAL_INSTRUCTION",
779            STATUS_NONCONTINUABLE_EXCEPTION => "STATUS_NONCONTINUABLE_EXCEPTION",
780            STATUS_INVALID_DISPOSITION => "STATUS_INVALID_DISPOSITION",
781            STATUS_ARRAY_BOUNDS_EXCEEDED => "STATUS_ARRAY_BOUNDS_EXCEEDED",
782            STATUS_FLOAT_DENORMAL_OPERAND => "STATUS_FLOAT_DENORMAL_OPERAND",
783            STATUS_FLOAT_DIVIDE_BY_ZERO => "STATUS_FLOAT_DIVIDE_BY_ZERO",
784            STATUS_FLOAT_INEXACT_RESULT => "STATUS_FLOAT_INEXACT_RESULT",
785            STATUS_FLOAT_INVALID_OPERATION => "STATUS_FLOAT_INVALID_OPERATION",
786            STATUS_FLOAT_OVERFLOW => "STATUS_FLOAT_OVERFLOW",
787            STATUS_FLOAT_STACK_CHECK => "STATUS_FLOAT_STACK_CHECK",
788            STATUS_FLOAT_UNDERFLOW => "STATUS_FLOAT_UNDERFLOW",
789            STATUS_INTEGER_DIVIDE_BY_ZERO => "STATUS_INTEGER_DIVIDE_BY_ZERO",
790            STATUS_INTEGER_OVERFLOW => "STATUS_INTEGER_OVERFLOW",
791            STATUS_PRIVILEGED_INSTRUCTION => "STATUS_PRIVILEGED_INSTRUCTION",
792            STATUS_STACK_OVERFLOW => "STATUS_STACK_OVERFLOW",
793            STATUS_DLL_NOT_FOUND => "STATUS_DLL_NOT_FOUND",
794            STATUS_ORDINAL_NOT_FOUND => "STATUS_ORDINAL_NOT_FOUND",
795            STATUS_ENTRYPOINT_NOT_FOUND => "STATUS_ENTRYPOINT_NOT_FOUND",
796            STATUS_CONTROL_C_EXIT => "STATUS_CONTROL_C_EXIT",
797            STATUS_DLL_INIT_FAILED => "STATUS_DLL_INIT_FAILED",
798            STATUS_FLOAT_MULTIPLE_FAULTS => "STATUS_FLOAT_MULTIPLE_FAULTS",
799            STATUS_FLOAT_MULTIPLE_TRAPS => "STATUS_FLOAT_MULTIPLE_TRAPS",
800            STATUS_REG_NAT_CONSUMPTION => "STATUS_REG_NAT_CONSUMPTION",
801            STATUS_HEAP_CORRUPTION => "STATUS_HEAP_CORRUPTION",
802            STATUS_STACK_BUFFER_OVERRUN => "STATUS_STACK_BUFFER_OVERRUN",
803            STATUS_ASSERTION_FAILURE => "STATUS_ASSERTION_FAILURE",
804            _ => return None,
805        };
806        Some(extra.to_owned())
807    }
808
809    if let Some(extra) = detailed_exit_status(status) {
810        format!("{} ({})", basic_exit_status(status), extra)
811    } else {
812        basic_exit_status(status)
813    }
814}
815
816fn basic_exit_status(status: std::process::ExitStatus) -> String {
817    if let Some(code) = status.code() {
818        code.to_string()
819    } else {
820        "interrupted".to_owned()
821    }
822}
823
824#[cfg(feature = "cmd")]
825fn wait(
826    mut child: std::process::Child,
827    timeout: Option<std::time::Duration>,
828) -> std::io::Result<std::process::ExitStatus> {
829    if let Some(timeout) = timeout {
830        wait_timeout::ChildExt::wait_timeout(&mut child, timeout)
831            .transpose()
832            .unwrap_or_else(|| {
833                let _ = child.kill();
834                child.wait()
835            })
836    } else {
837        child.wait()
838    }
839}
840
841#[cfg(not(feature = "cmd"))]
842fn wait(
843    mut child: std::process::Child,
844    _timeout: Option<std::time::Duration>,
845) -> std::io::Result<std::process::ExitStatus> {
846    child.wait()
847}
848
849#[doc(inline)]
850pub use crate::cargo_bin;
851
852/// Look up the path to a cargo-built binary within an integration test
853///
854/// Cargo support:
855/// - `>1.94`: works
856/// - `>=1.91,<=1.93`: works with default `build-dir`
857/// - `<=1.92`: works
858///
859/// # Panic
860///
861/// Panics if no binary is found
862pub fn cargo_bin(name: &str) -> std::path::PathBuf {
863    cargo_bin_opt(name).unwrap_or_else(|| missing_cargo_bin(name))
864}
865
866/// Look up the path to a cargo-built binary within an integration test
867///
868/// Returns `None` if the binary doesn't exist
869///
870/// Cargo support:
871/// - `>1.94`: works
872/// - `>=1.91,<=1.93`: works with default `build-dir`
873/// - `<=1.92`: works
874pub fn cargo_bin_opt(name: &str) -> Option<std::path::PathBuf> {
875    let env_var = format!("{CARGO_BIN_EXE_}{name}");
876    std::env::var_os(env_var)
877        .map(|p| p.into())
878        .or_else(|| legacy_cargo_bin(name))
879}
880
881/// Return all the name and path for all binaries built by Cargo
882///
883/// Cargo support:
884/// - `>1.94`: works
885pub fn cargo_bins() -> impl Iterator<Item = (String, std::path::PathBuf)> {
886    std::env::vars_os()
887        .filter_map(|(k, v)| {
888            k.into_string()
889                .ok()
890                .map(|k| (k, std::path::PathBuf::from(v)))
891        })
892        .filter_map(|(k, v)| k.strip_prefix(CARGO_BIN_EXE_).map(|s| (s.to_owned(), v)))
893}
894
895const CARGO_BIN_EXE_: &str = "CARGO_BIN_EXE_";
896
897fn missing_cargo_bin(name: &str) -> ! {
898    let possible_names: Vec<_> = cargo_bins().map(|(k, _)| k).collect();
899    if possible_names.is_empty() {
900        panic!("`CARGO_BIN_EXE_{name}` is unset
901help: if this is running within a unit test, move it to an integration test to gain access to `CARGO_BIN_EXE_{name}`")
902    } else {
903        let mut names = String::new();
904        for (i, name) in possible_names.iter().enumerate() {
905            use std::fmt::Write as _;
906            if i != 0 {
907                let _ = write!(&mut names, ", ");
908            }
909            let _ = write!(&mut names, "\"{name}\"");
910        }
911        panic!(
912            "`CARGO_BIN_EXE_{name}` is unset
913help: available binary names are {names}"
914        )
915    }
916}
917
918fn legacy_cargo_bin(name: &str) -> Option<std::path::PathBuf> {
919    let target_dir = target_dir()?;
920    let bin_path = target_dir.join(format!("{}{}", name, std::env::consts::EXE_SUFFIX));
921    if !bin_path.exists() {
922        return None;
923    }
924    Some(bin_path)
925}
926
927// Adapted from
928// https://github.com/rust-lang/cargo/blob/485670b3983b52289a2f353d589c57fae2f60f82/tests/testsuite/support/mod.rs#L507
929fn target_dir() -> Option<std::path::PathBuf> {
930    let mut path = std::env::current_exe().ok()?;
931    let _test_bin_name = path.pop();
932    if path.ends_with("deps") {
933        let _deps = path.pop();
934    }
935    Some(path)
936}
937
938#[cfg(feature = "examples")]
939pub use examples::{compile_example, compile_examples};
940
941#[cfg(feature = "examples")]
942pub(crate) mod examples {
943    /// Prepare an example for testing
944    ///
945    /// Unlike `cargo_bin!`, this does not inherit all of the current compiler settings.  It
946    /// will match the current target and profile but will not get feature flags.  Pass those arguments
947    /// to the compiler via `args`.
948    ///
949    /// ## Example
950    ///
951    /// ```rust,no_run
952    /// snapbox::cmd::compile_example("snap-example-fixture", []);
953    /// ```
954    #[cfg(feature = "examples")]
955    pub fn compile_example<'a>(
956        target_name: &str,
957        args: impl IntoIterator<Item = &'a str>,
958    ) -> crate::assert::Result<std::path::PathBuf> {
959        crate::debug!("Compiling example {}", target_name);
960        let messages = escargot::CargoBuild::new()
961            .current_target()
962            .current_release()
963            .example(target_name)
964            .args(args)
965            .exec()
966            .map_err(|e| crate::assert::Error::new(e.to_string()))?;
967        for message in messages {
968            let message = message.map_err(|e| crate::assert::Error::new(e.to_string()))?;
969            let message = message
970                .decode()
971                .map_err(|e| crate::assert::Error::new(e.to_string()))?;
972            crate::debug!("Message: {:?}", message);
973            if let Some(bin) = decode_example_message(&message) {
974                let (name, bin) = bin?;
975                assert_eq!(target_name, name);
976                return bin;
977            }
978        }
979
980        Err(crate::assert::Error::new(format!(
981            "Unknown error building example {target_name}"
982        )))
983    }
984
985    /// Prepare all examples for testing
986    ///
987    /// Unlike `cargo_bin!`, this does not inherit all of the current compiler settings.  It
988    /// will match the current target and profile but will not get feature flags.  Pass those arguments
989    /// to the compiler via `args`.
990    ///
991    /// ## Example
992    ///
993    /// ```rust,no_run
994    /// let examples = snapbox::cmd::compile_examples([]).unwrap().collect::<Vec<_>>();
995    /// ```
996    #[cfg(feature = "examples")]
997    pub fn compile_examples<'a>(
998        args: impl IntoIterator<Item = &'a str>,
999    ) -> crate::assert::Result<
1000        impl Iterator<Item = (String, crate::assert::Result<std::path::PathBuf>)>,
1001    > {
1002        crate::debug!("Compiling examples");
1003        let mut examples = std::collections::BTreeMap::new();
1004
1005        let messages = escargot::CargoBuild::new()
1006            .current_target()
1007            .current_release()
1008            .examples()
1009            .args(args)
1010            .exec()
1011            .map_err(|e| crate::assert::Error::new(e.to_string()))?;
1012        for message in messages {
1013            let message = message.map_err(|e| crate::assert::Error::new(e.to_string()))?;
1014            let message = message
1015                .decode()
1016                .map_err(|e| crate::assert::Error::new(e.to_string()))?;
1017            crate::debug!("Message: {:?}", message);
1018            if let Some(bin) = decode_example_message(&message) {
1019                let (name, bin) = bin?;
1020                examples.insert(name.to_owned(), bin);
1021            }
1022        }
1023
1024        Ok(examples.into_iter())
1025    }
1026
1027    #[allow(clippy::type_complexity)]
1028    fn decode_example_message<'m>(
1029        message: &'m escargot::format::Message<'_>,
1030    ) -> Option<crate::assert::Result<(&'m str, crate::assert::Result<std::path::PathBuf>)>> {
1031        match message {
1032            escargot::format::Message::CompilerMessage(msg) => {
1033                let level = msg.message.level;
1034                if level == escargot::format::diagnostic::DiagnosticLevel::Ice
1035                    || level == escargot::format::diagnostic::DiagnosticLevel::Error
1036                {
1037                    let output = msg
1038                        .message
1039                        .rendered
1040                        .as_deref()
1041                        .unwrap_or_else(|| msg.message.message.as_ref())
1042                        .to_owned();
1043                    if is_example_target(&msg.target) {
1044                        let bin = Err(crate::assert::Error::new(output));
1045                        Some(Ok((msg.target.name.as_ref(), bin)))
1046                    } else {
1047                        Some(Err(crate::assert::Error::new(output)))
1048                    }
1049                } else {
1050                    None
1051                }
1052            }
1053            escargot::format::Message::CompilerArtifact(artifact) => {
1054                if !artifact.profile.test && is_example_target(&artifact.target) {
1055                    let path = artifact
1056                        .executable
1057                        .clone()
1058                        .expect("cargo is new enough for this to be present");
1059                    let bin = Ok(path.into_owned());
1060                    Some(Ok((artifact.target.name.as_ref(), bin)))
1061                } else {
1062                    None
1063                }
1064            }
1065            _ => None,
1066        }
1067    }
1068
1069    fn is_example_target(target: &escargot::format::Target<'_>) -> bool {
1070        target.crate_types == ["bin"] && target.kind == ["example"]
1071    }
1072}
1073
1074#[test]
1075#[should_panic = "`CARGO_BIN_EXE_non-existent` is unset
1076help: if this is running within a unit test, move it to an integration test to gain access to `CARGO_BIN_EXE_non-existent`"]
1077fn cargo_bin_in_unit_test() {
1078    cargo_bin("non-existent");
1079}