summaryrefslogtreecommitdiffstats
path: root/vendor/snapbox/src/cmd.rs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/snapbox/src/cmd.rs')
-rw-r--r--vendor/snapbox/src/cmd.rs1030
1 files changed, 1030 insertions, 0 deletions
diff --git a/vendor/snapbox/src/cmd.rs b/vendor/snapbox/src/cmd.rs
new file mode 100644
index 000000000..72de3563c
--- /dev/null
+++ b/vendor/snapbox/src/cmd.rs
@@ -0,0 +1,1030 @@
+//! Run commands and assert on their behavior
+
+/// Process spawning for testing of non-interactive commands
+#[derive(Debug)]
+pub struct Command {
+ cmd: std::process::Command,
+ stdin: Option<crate::Data>,
+ timeout: Option<std::time::Duration>,
+ _stderr_to_stdout: bool,
+ config: crate::Assert,
+}
+
+/// # Builder API
+impl Command {
+ pub fn new(program: impl AsRef<std::ffi::OsStr>) -> Self {
+ Self {
+ cmd: std::process::Command::new(program),
+ stdin: None,
+ timeout: None,
+ _stderr_to_stdout: false,
+ config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV),
+ }
+ }
+
+ /// Constructs a new `Command` from a `std` `Command`.
+ pub fn from_std(cmd: std::process::Command) -> Self {
+ Self {
+ cmd,
+ stdin: None,
+ timeout: None,
+ _stderr_to_stdout: false,
+ config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV),
+ }
+ }
+
+ /// Customize the assertion behavior
+ pub fn with_assert(mut self, config: crate::Assert) -> Self {
+ self.config = config;
+ self
+ }
+
+ /// Adds an argument to pass to the program.
+ ///
+ /// Only one argument can be passed per use. So instead of:
+ ///
+ /// ```no_run
+ /// # snapbox::cmd::Command::new("sh")
+ /// .arg("-C /path/to/repo")
+ /// # ;
+ /// ```
+ ///
+ /// usage would be:
+ ///
+ /// ```no_run
+ /// # snapbox::cmd::Command::new("sh")
+ /// .arg("-C")
+ /// .arg("/path/to/repo")
+ /// # ;
+ /// ```
+ ///
+ /// To pass multiple arguments see [`args`].
+ ///
+ /// [`args`]: Command::args()
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .arg("-l")
+ /// .arg("-a")
+ /// .assert()
+ /// .success();
+ /// ```
+ pub fn arg(mut self, arg: impl AsRef<std::ffi::OsStr>) -> Self {
+ self.cmd.arg(arg);
+ self
+ }
+
+ /// Adds multiple arguments to pass to the program.
+ ///
+ /// To pass a single argument see [`arg`].
+ ///
+ /// [`arg`]: Command::arg()
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .args(&["-l", "-a"])
+ /// .assert()
+ /// .success();
+ /// ```
+ pub fn args(mut self, args: impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>>) -> Self {
+ self.cmd.args(args);
+ self
+ }
+
+ /// Inserts or updates an environment variable mapping.
+ ///
+ /// Note that environment variable names are case-insensitive (but case-preserving) on Windows,
+ /// and case-sensitive on all other platforms.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .env("PATH", "/bin")
+ /// .assert()
+ /// .failure();
+ /// ```
+ pub fn env(
+ mut self,
+ key: impl AsRef<std::ffi::OsStr>,
+ value: impl AsRef<std::ffi::OsStr>,
+ ) -> Self {
+ self.cmd.env(key, value);
+ self
+ }
+
+ /// Adds or updates multiple environment variable mappings.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ /// use std::process::Stdio;
+ /// use std::env;
+ /// use std::collections::HashMap;
+ ///
+ /// let filtered_env : HashMap<String, String> =
+ /// env::vars().filter(|&(ref k, _)|
+ /// k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH"
+ /// ).collect();
+ ///
+ /// Command::new("printenv")
+ /// .env_clear()
+ /// .envs(&filtered_env)
+ /// .assert()
+ /// .success();
+ /// ```
+ pub fn envs(
+ mut self,
+ vars: impl IntoIterator<Item = (impl AsRef<std::ffi::OsStr>, impl AsRef<std::ffi::OsStr>)>,
+ ) -> Self {
+ self.cmd.envs(vars);
+ self
+ }
+
+ /// Removes an environment variable mapping.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .env_remove("PATH")
+ /// .assert()
+ /// .failure();
+ /// ```
+ pub fn env_remove(mut self, key: impl AsRef<std::ffi::OsStr>) -> Self {
+ self.cmd.env_remove(key);
+ self
+ }
+
+ /// Clears the entire environment map for the child process.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .env_clear()
+ /// .assert()
+ /// .failure();
+ /// ```
+ pub fn env_clear(mut self) -> Self {
+ self.cmd.env_clear();
+ self
+ }
+
+ /// Sets the working directory for the child process.
+ ///
+ /// # Platform-specific behavior
+ ///
+ /// If the program path is relative (e.g., `"./script.sh"`), it's ambiguous
+ /// whether it should be interpreted relative to the parent's working
+ /// directory or relative to `current_dir`. The behavior in this case is
+ /// platform specific and unstable, and it's recommended to use
+ /// [`canonicalize`] to get an absolute program path instead.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .current_dir("/bin")
+ /// .assert()
+ /// .success();
+ /// ```
+ ///
+ /// [`canonicalize`]: std::fs::canonicalize()
+ pub fn current_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
+ self.cmd.current_dir(dir);
+ self
+ }
+
+ /// Write `buffer` to `stdin` when the `Command` is run.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use snapbox::cmd::Command;
+ ///
+ /// let mut cmd = Command::new("cat")
+ /// .arg("-et")
+ /// .stdin("42")
+ /// .assert()
+ /// .stdout_eq("42");
+ /// ```
+ pub fn stdin(mut self, stream: impl Into<crate::Data>) -> Self {
+ self.stdin = Some(stream.into());
+ self
+ }
+
+ /// Error out if a timeout is reached
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .timeout(std::time::Duration::from_secs(1))
+ /// .env("sleep", "100")
+ /// .assert()
+ /// .failure();
+ /// ```
+ #[cfg(feature = "cmd")]
+ pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
+ self.timeout = Some(timeout);
+ self
+ }
+
+ /// Merge `stderr` into `stdout`
+ #[cfg(feature = "cmd")]
+ pub fn stderr_to_stdout(mut self) -> Self {
+ self._stderr_to_stdout = true;
+ self
+ }
+}
+
+/// # Run Command
+impl Command {
+ /// Run the command and assert on the results
+ ///
+ /// ```rust
+ /// use snapbox::cmd::Command;
+ ///
+ /// let mut cmd = Command::new("cat")
+ /// .arg("-et")
+ /// .stdin("42")
+ /// .assert()
+ /// .stdout_eq("42");
+ /// ```
+ #[track_caller]
+ pub fn assert(self) -> OutputAssert {
+ let config = self.config.clone();
+ match self.output() {
+ Ok(output) => OutputAssert::new(output).with_assert(config),
+ Err(err) => {
+ panic!("Failed to spawn: {}", err)
+ }
+ }
+ }
+
+ /// Run the command and capture the `Output`
+ #[cfg(feature = "cmd")]
+ pub fn output(self) -> Result<std::process::Output, std::io::Error> {
+ if self._stderr_to_stdout {
+ self.single_output()
+ } else {
+ self.split_output()
+ }
+ }
+
+ #[cfg(not(feature = "cmd"))]
+ pub fn output(self) -> Result<std::process::Output, std::io::Error> {
+ self.split_output()
+ }
+
+ #[cfg(feature = "cmd")]
+ fn single_output(mut self) -> Result<std::process::Output, std::io::Error> {
+ self.cmd.stdin(std::process::Stdio::piped());
+ let (reader, writer) = os_pipe::pipe()?;
+ let writer_clone = writer.try_clone()?;
+ self.cmd.stdout(writer);
+ self.cmd.stderr(writer_clone);
+ let mut child = self.cmd.spawn()?;
+ // Avoid a deadlock! This parent process is still holding open pipe
+ // writers (inside the Command object), and we have to close those
+ // before we read. Here we do this by dropping the Command object.
+ drop(self.cmd);
+
+ let stdout = process_single_io(
+ &mut child,
+ reader,
+ self.stdin.as_ref().map(|d| d.to_bytes()),
+ )?;
+
+ let status = wait(child, self.timeout)?;
+ let stdout = stdout.join().unwrap().ok().unwrap_or_default();
+
+ Ok(std::process::Output {
+ status,
+ stdout,
+ stderr: Default::default(),
+ })
+ }
+
+ fn split_output(mut self) -> Result<std::process::Output, std::io::Error> {
+ self.cmd.stdin(std::process::Stdio::piped());
+ self.cmd.stdout(std::process::Stdio::piped());
+ self.cmd.stderr(std::process::Stdio::piped());
+ let mut child = self.cmd.spawn()?;
+
+ let (stdout, stderr) =
+ process_split_io(&mut child, self.stdin.as_ref().map(|d| d.to_bytes()))?;
+
+ let status = wait(child, self.timeout)?;
+ let stdout = stdout
+ .and_then(|t| t.join().unwrap().ok())
+ .unwrap_or_default();
+ let stderr = stderr
+ .and_then(|t| t.join().unwrap().ok())
+ .unwrap_or_default();
+
+ Ok(std::process::Output {
+ status,
+ stdout,
+ stderr,
+ })
+ }
+}
+
+fn process_split_io(
+ child: &mut std::process::Child,
+ input: Option<Vec<u8>>,
+) -> std::io::Result<(Option<Stream>, Option<Stream>)> {
+ use std::io::Write;
+
+ let stdin = input.and_then(|i| {
+ child
+ .stdin
+ .take()
+ .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
+ });
+ let stdout = child.stdout.take().map(threaded_read);
+ let stderr = child.stderr.take().map(threaded_read);
+
+ // Finish writing stdin before waiting, because waiting drops stdin.
+ stdin.and_then(|t| t.join().unwrap().ok());
+
+ Ok((stdout, stderr))
+}
+
+#[cfg(feature = "cmd")]
+fn process_single_io(
+ child: &mut std::process::Child,
+ stdout: os_pipe::PipeReader,
+ input: Option<Vec<u8>>,
+) -> std::io::Result<Stream> {
+ use std::io::Write;
+
+ let stdin = input.and_then(|i| {
+ child
+ .stdin
+ .take()
+ .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
+ });
+ let stdout = threaded_read(stdout);
+ debug_assert!(child.stdout.is_none());
+ debug_assert!(child.stderr.is_none());
+
+ // Finish writing stdin before waiting, because waiting drops stdin.
+ stdin.and_then(|t| t.join().unwrap().ok());
+
+ Ok(stdout)
+}
+
+type Stream = std::thread::JoinHandle<Result<Vec<u8>, std::io::Error>>;
+
+fn threaded_read<R>(mut input: R) -> Stream
+where
+ R: std::io::Read + Send + 'static,
+{
+ std::thread::spawn(move || {
+ let mut ret = Vec::new();
+ input.read_to_end(&mut ret).map(|_| ret)
+ })
+}
+
+impl From<std::process::Command> for Command {
+ fn from(cmd: std::process::Command) -> Self {
+ Self::from_std(cmd)
+ }
+}
+
+/// Assert the state of a [`Command`]'s [`Output`].
+///
+/// Create an `OutputAssert` through the [`Command::assert`].
+///
+/// [`Output`]: std::process::Output
+pub struct OutputAssert {
+ output: std::process::Output,
+ config: crate::Assert,
+}
+
+impl OutputAssert {
+ /// Create an `Assert` for a given [`Output`].
+ ///
+ /// [`Output`]: std::process::Output
+ pub fn new(output: std::process::Output) -> Self {
+ Self {
+ output,
+ config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV),
+ }
+ }
+
+ /// Customize the assertion behavior
+ pub fn with_assert(mut self, config: crate::Assert) -> Self {
+ self.config = config;
+ self
+ }
+
+ /// Access the contained [`Output`].
+ ///
+ /// [`Output`]: std::process::Output
+ pub fn get_output(&self) -> &std::process::Output {
+ &self.output
+ }
+
+ /// Ensure the command succeeded.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .assert()
+ /// .success();
+ /// ```
+ #[track_caller]
+ pub fn success(self) -> Self {
+ if !self.output.status.success() {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info("success"),
+ self.config
+ .palette
+ .error(display_exit_status(self.output.status))
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command failed.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("exit", "1")
+ /// .assert()
+ /// .failure();
+ /// ```
+ #[track_caller]
+ pub fn failure(self) -> Self {
+ if self.output.status.success() {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info("failure"),
+ self.config.palette.error("success")
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command aborted before returning a code.
+ #[track_caller]
+ pub fn interrupted(self) -> Self {
+ if self.output.status.code().is_some() {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info("interrupted"),
+ self.config
+ .palette
+ .error(display_exit_status(self.output.status))
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command returned the expected code.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("exit", "42")
+ /// .assert()
+ /// .code(42);
+ /// ```
+ #[track_caller]
+ pub fn code(self, expected: i32) -> Self {
+ if self.output.status.code() != Some(expected) {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info(expected),
+ self.config
+ .palette
+ .error(display_exit_status(self.output.status))
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_eq("hello");
+ /// ```
+ #[track_caller]
+ pub fn stdout_eq(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stdout_eq_inner(expected)
+ }
+
+ #[track_caller]
+ fn stdout_eq_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let (pattern, actual) = self.config.normalize_eq(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stdout")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_eq_path("tests/snapshots/output.txt");
+ /// ```
+ #[track_caller]
+ pub fn stdout_eq_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stdout_eq_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stdout_eq_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_eq(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&crate::path::display_relpath(expected_path)),
+ Some(&"stdout"),
+ expected_path,
+ );
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_matches("he[..]o");
+ /// ```
+ #[track_caller]
+ pub fn stdout_matches(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stdout_matches_inner(expected)
+ }
+
+ #[track_caller]
+ fn stdout_matches_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let (pattern, actual) = self.config.normalize_match(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stdout")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_matches_path("tests/snapshots/output.txt");
+ /// ```
+ #[track_caller]
+ pub fn stdout_matches_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stdout_matches_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stdout_matches_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_match(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&expected_path.display()),
+ Some(&"stdout"),
+ expected_path,
+ );
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_eq("world");
+ /// ```
+ #[track_caller]
+ pub fn stderr_eq(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stderr_eq_inner(expected)
+ }
+
+ #[track_caller]
+ fn stderr_eq_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let (pattern, actual) = self.config.normalize_eq(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stderr")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_eq_path("tests/snapshots/err.txt");
+ /// ```
+ #[track_caller]
+ pub fn stderr_eq_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stderr_eq_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stderr_eq_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_eq(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&expected_path.display()),
+ Some(&"stderr"),
+ expected_path,
+ );
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_matches("wo[..]d");
+ /// ```
+ #[track_caller]
+ pub fn stderr_matches(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stderr_matches_inner(expected)
+ }
+
+ #[track_caller]
+ fn stderr_matches_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let (pattern, actual) = self.config.normalize_match(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stderr")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_matches_path("tests/snapshots/err.txt");
+ /// ```
+ #[track_caller]
+ pub fn stderr_matches_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stderr_matches_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stderr_matches_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_match(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&crate::path::display_relpath(expected_path)),
+ Some(&"stderr"),
+ expected_path,
+ );
+
+ self
+ }
+
+ fn write_status(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
+ writeln!(
+ writer,
+ "Exit status: {}",
+ display_exit_status(self.output.status)
+ )?;
+ Ok(())
+ }
+
+ fn write_stdout(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
+ if !self.output.stdout.is_empty() {
+ writeln!(writer, "stdout:")?;
+ writeln!(writer, "```")?;
+ writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stdout))?;
+ writeln!(writer, "```")?;
+ }
+ Ok(())
+ }
+
+ fn write_stderr(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
+ if !self.output.stderr.is_empty() {
+ writeln!(writer, "stderr:")?;
+ writeln!(writer, "```")?;
+ writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stderr))?;
+ writeln!(writer, "```")?;
+ }
+ Ok(())
+ }
+}
+
+/// Converts an [`std::process::ExitStatus`] to a human-readable value
+#[cfg(not(feature = "cmd"))]
+pub fn display_exit_status(status: std::process::ExitStatus) -> String {
+ basic_exit_status(status)
+}
+
+/// Converts an [`std::process::ExitStatus`] to a human-readable value
+#[cfg(feature = "cmd")]
+pub fn display_exit_status(status: std::process::ExitStatus) -> String {
+ #[cfg(unix)]
+ fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
+ use std::os::unix::process::*;
+
+ let signal = status.signal()?;
+ let name = match signal as libc::c_int {
+ libc::SIGABRT => ", SIGABRT: process abort signal",
+ libc::SIGALRM => ", SIGALRM: alarm clock",
+ libc::SIGFPE => ", SIGFPE: erroneous arithmetic operation",
+ libc::SIGHUP => ", SIGHUP: hangup",
+ libc::SIGILL => ", SIGILL: illegal instruction",
+ libc::SIGINT => ", SIGINT: terminal interrupt signal",
+ libc::SIGKILL => ", SIGKILL: kill",
+ libc::SIGPIPE => ", SIGPIPE: write on a pipe with no one to read",
+ libc::SIGQUIT => ", SIGQUIT: terminal quit signal",
+ libc::SIGSEGV => ", SIGSEGV: invalid memory reference",
+ libc::SIGTERM => ", SIGTERM: termination signal",
+ libc::SIGBUS => ", SIGBUS: access to undefined memory",
+ #[cfg(not(target_os = "haiku"))]
+ libc::SIGSYS => ", SIGSYS: bad system call",
+ libc::SIGTRAP => ", SIGTRAP: trace/breakpoint trap",
+ _ => "",
+ };
+ Some(format!("signal: {}{}", signal, name))
+ }
+
+ #[cfg(windows)]
+ fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
+ use winapi::shared::minwindef::DWORD;
+ use winapi::um::winnt::*;
+
+ let extra = match status.code().unwrap() as DWORD {
+ STATUS_ACCESS_VIOLATION => "STATUS_ACCESS_VIOLATION",
+ STATUS_IN_PAGE_ERROR => "STATUS_IN_PAGE_ERROR",
+ STATUS_INVALID_HANDLE => "STATUS_INVALID_HANDLE",
+ STATUS_INVALID_PARAMETER => "STATUS_INVALID_PARAMETER",
+ STATUS_NO_MEMORY => "STATUS_NO_MEMORY",
+ STATUS_ILLEGAL_INSTRUCTION => "STATUS_ILLEGAL_INSTRUCTION",
+ STATUS_NONCONTINUABLE_EXCEPTION => "STATUS_NONCONTINUABLE_EXCEPTION",
+ STATUS_INVALID_DISPOSITION => "STATUS_INVALID_DISPOSITION",
+ STATUS_ARRAY_BOUNDS_EXCEEDED => "STATUS_ARRAY_BOUNDS_EXCEEDED",
+ STATUS_FLOAT_DENORMAL_OPERAND => "STATUS_FLOAT_DENORMAL_OPERAND",
+ STATUS_FLOAT_DIVIDE_BY_ZERO => "STATUS_FLOAT_DIVIDE_BY_ZERO",
+ STATUS_FLOAT_INEXACT_RESULT => "STATUS_FLOAT_INEXACT_RESULT",
+ STATUS_FLOAT_INVALID_OPERATION => "STATUS_FLOAT_INVALID_OPERATION",
+ STATUS_FLOAT_OVERFLOW => "STATUS_FLOAT_OVERFLOW",
+ STATUS_FLOAT_STACK_CHECK => "STATUS_FLOAT_STACK_CHECK",
+ STATUS_FLOAT_UNDERFLOW => "STATUS_FLOAT_UNDERFLOW",
+ STATUS_INTEGER_DIVIDE_BY_ZERO => "STATUS_INTEGER_DIVIDE_BY_ZERO",
+ STATUS_INTEGER_OVERFLOW => "STATUS_INTEGER_OVERFLOW",
+ STATUS_PRIVILEGED_INSTRUCTION => "STATUS_PRIVILEGED_INSTRUCTION",
+ STATUS_STACK_OVERFLOW => "STATUS_STACK_OVERFLOW",
+ STATUS_DLL_NOT_FOUND => "STATUS_DLL_NOT_FOUND",
+ STATUS_ORDINAL_NOT_FOUND => "STATUS_ORDINAL_NOT_FOUND",
+ STATUS_ENTRYPOINT_NOT_FOUND => "STATUS_ENTRYPOINT_NOT_FOUND",
+ STATUS_CONTROL_C_EXIT => "STATUS_CONTROL_C_EXIT",
+ STATUS_DLL_INIT_FAILED => "STATUS_DLL_INIT_FAILED",
+ STATUS_FLOAT_MULTIPLE_FAULTS => "STATUS_FLOAT_MULTIPLE_FAULTS",
+ STATUS_FLOAT_MULTIPLE_TRAPS => "STATUS_FLOAT_MULTIPLE_TRAPS",
+ STATUS_REG_NAT_CONSUMPTION => "STATUS_REG_NAT_CONSUMPTION",
+ STATUS_HEAP_CORRUPTION => "STATUS_HEAP_CORRUPTION",
+ STATUS_STACK_BUFFER_OVERRUN => "STATUS_STACK_BUFFER_OVERRUN",
+ STATUS_ASSERTION_FAILURE => "STATUS_ASSERTION_FAILURE",
+ _ => return None,
+ };
+ Some(extra.to_owned())
+ }
+
+ if let Some(extra) = detailed_exit_status(status) {
+ format!("{} ({})", basic_exit_status(status), extra)
+ } else {
+ basic_exit_status(status)
+ }
+}
+
+fn basic_exit_status(status: std::process::ExitStatus) -> String {
+ if let Some(code) = status.code() {
+ code.to_string()
+ } else {
+ "interrupted".to_owned()
+ }
+}
+
+#[cfg(feature = "cmd")]
+fn wait(
+ mut child: std::process::Child,
+ timeout: Option<std::time::Duration>,
+) -> std::io::Result<std::process::ExitStatus> {
+ if let Some(timeout) = timeout {
+ wait_timeout::ChildExt::wait_timeout(&mut child, timeout)
+ .transpose()
+ .unwrap_or_else(|| {
+ let _ = child.kill();
+ child.wait()
+ })
+ } else {
+ child.wait()
+ }
+}
+
+#[cfg(not(feature = "cmd"))]
+fn wait(
+ mut child: std::process::Child,
+ _timeout: Option<std::time::Duration>,
+) -> std::io::Result<std::process::ExitStatus> {
+ child.wait()
+}
+
+pub use snapbox_macros::cargo_bin;
+
+/// Look up the path to a cargo-built binary within an integration test.
+///
+/// **NOTE:** Prefer [`cargo_bin!`] as this makes assumptions about cargo
+pub fn cargo_bin(name: &str) -> std::path::PathBuf {
+ let file_name = format!("{}{}", name, std::env::consts::EXE_SUFFIX);
+ let target_dir = target_dir();
+ target_dir.join(&file_name)
+}
+
+// Adapted from
+// https://github.com/rust-lang/cargo/blob/485670b3983b52289a2f353d589c57fae2f60f82/tests/testsuite/support/mod.rs#L507
+fn target_dir() -> std::path::PathBuf {
+ std::env::current_exe()
+ .ok()
+ .map(|mut path| {
+ path.pop();
+ if path.ends_with("deps") {
+ path.pop();
+ }
+ path
+ })
+ .unwrap()
+}