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