//! Run commands and assert on their behavior #[cfg(feature = "color")] use anstream::panic; /// Process spawning for testing of non-interactive commands #[derive(Debug)] pub struct Command { cmd: std::process::Command, stdin: Option, timeout: Option, _stderr_to_stdout: bool, config: crate::Assert, } /// # Builder API impl Command { pub fn new(program: impl AsRef) -> 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) -> 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>) -> 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, value: impl AsRef, ) -> 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 = /// 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, impl AsRef)>, ) -> 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) -> 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) -> 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) -> 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 { if self._stderr_to_stdout { self.single_output() } else { self.split_output() } } #[cfg(not(feature = "cmd"))] pub fn output(self) -> Result { self.split_output() } #[cfg(feature = "cmd")] fn single_output(mut self) -> Result { 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 { 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>, ) -> std::io::Result<(Option, Option)> { 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>, ) -> std::io::Result { 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, std::io::Error>>; fn threaded_read(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 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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 { 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 { use windows_sys::Win32::Foundation::*; let extra = match status.code().unwrap() as NTSTATUS { 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::io::Result { 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::io::Result { 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() } #[cfg(feature = "examples")] pub use examples::{compile_example, compile_examples}; #[cfg(feature = "examples")] pub(crate) mod examples { /// Prepare an example for testing /// /// Unlike `cargo_bin!`, this does not inherit all of the current compiler settings. It /// will match the current target and profile but will not get feature flags. Pass those arguments /// to the compiler via `args`. /// /// ## Example /// /// ```rust,no_run /// snapbox::cmd::compile_example("snap-example-fixture", []); /// ``` #[cfg(feature = "examples")] pub fn compile_example<'a>( target_name: &str, args: impl IntoIterator, ) -> Result { crate::debug!("Compiling example {}", target_name); let messages = escargot::CargoBuild::new() .current_target() .current_release() .example(target_name) .args(args) .exec() .map_err(|e| crate::Error::new(e.to_string()))?; for message in messages { let message = message.map_err(|e| crate::Error::new(e.to_string()))?; let message = message .decode() .map_err(|e| crate::Error::new(e.to_string()))?; crate::debug!("Message: {:?}", message); if let Some(bin) = decode_example_message(&message) { let (name, bin) = bin?; assert_eq!(target_name, name); return bin; } } Err(crate::Error::new(format!( "Unknown error building example {}", target_name ))) } /// Prepare all examples for testing /// /// Unlike `cargo_bin!`, this does not inherit all of the current compiler settings. It /// will match the current target and profile but will not get feature flags. Pass those arguments /// to the compiler via `args`. /// /// ## Example /// /// ```rust,no_run /// let examples = snapbox::cmd::compile_examples([]).unwrap().collect::>(); /// ``` #[cfg(feature = "examples")] pub fn compile_examples<'a>( args: impl IntoIterator, ) -> Result< impl Iterator)>, crate::Error, > { crate::debug!("Compiling examples"); let mut examples = std::collections::BTreeMap::new(); let messages = escargot::CargoBuild::new() .current_target() .current_release() .examples() .args(args) .exec() .map_err(|e| crate::Error::new(e.to_string()))?; for message in messages { let message = message.map_err(|e| crate::Error::new(e.to_string()))?; let message = message .decode() .map_err(|e| crate::Error::new(e.to_string()))?; crate::debug!("Message: {:?}", message); if let Some(bin) = decode_example_message(&message) { let (name, bin) = bin?; examples.insert(name.to_owned(), bin); } } Ok(examples.into_iter()) } #[allow(clippy::type_complexity)] fn decode_example_message<'m>( message: &'m escargot::format::Message, ) -> Option), crate::Error>> { match message { escargot::format::Message::CompilerMessage(msg) => { let level = msg.message.level; if level == escargot::format::diagnostic::DiagnosticLevel::Ice || level == escargot::format::diagnostic::DiagnosticLevel::Error { let output = msg .message .rendered .as_deref() .unwrap_or_else(|| msg.message.message.as_ref()) .to_owned(); if is_example_target(&msg.target) { let bin = Err(crate::Error::new(output)); Some(Ok((msg.target.name.as_ref(), bin))) } else { Some(Err(crate::Error::new(output))) } } else { None } } escargot::format::Message::CompilerArtifact(artifact) => { if !artifact.profile.test && is_example_target(&artifact.target) { let path = artifact .executable .clone() .expect("cargo is new enough for this to be present"); let bin = Ok(path.into_owned()); Some(Ok((artifact.target.name.as_ref(), bin))) } else { None } } _ => None, } } fn is_example_target(target: &escargot::format::Target) -> bool { target.crate_types == ["bin"] && target.kind == ["example"] } }