diff options
Diffstat (limited to 'vendor/snapbox/src/cmd.rs')
-rw-r--r-- | vendor/snapbox/src/cmd.rs | 1030 |
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() +} |