| use std::ffi::OsStr; |
| use std::io::Write; |
| use std::path::Path; |
| use std::process::{Command as StdCommand, ExitStatus, Output, Stdio}; |
| use std::{ffi, panic}; |
| |
| use build_helper::drop_bomb::DropBomb; |
| |
| use crate::util::{handle_failed_output, verbose_print_command}; |
| use crate::{ |
| assert_contains, assert_contains_regex, assert_equals, assert_not_contains, |
| assert_not_contains_regex, |
| }; |
| |
| /// This is a custom command wrapper that simplifies working with commands and makes it easier to |
| /// ensure that we check the exit status of executed processes. |
| /// |
| /// # A [`Command`] must be executed exactly once |
| /// |
| /// A [`Command`] is armed by a [`DropBomb`] on construction to enforce that it will be executed. If |
| /// a [`Command`] is constructed but never executed, the drop bomb will explode and cause the test |
| /// to panic. Execution methods [`run`] and [`run_fail`] will defuse the drop bomb. A test |
| /// containing constructed but never executed commands is dangerous because it can give a false |
| /// sense of confidence. |
| /// |
| /// Each [`Command`] invocation can also only be executed once, because we want to enforce |
| /// `std{in,out,err}` config via [`std::process::Stdio`] but [`std::process::Stdio`] is not |
| /// cloneable. |
| /// |
| /// In this sense, [`Command`] exhibits linear type semantics but enforced at run-time. |
| /// |
| /// [`run`]: Self::run |
| /// [`run_fail`]: Self::run_fail |
| /// [`run_unchecked`]: Self::run_unchecked |
| #[derive(Debug)] |
| pub struct Command { |
| cmd: StdCommand, |
| // Convience for providing a quick stdin buffer. |
| stdin_buf: Option<Box<[u8]>>, |
| |
| // Configurations for child process's std{in,out,err} handles. |
| stdin: Option<Stdio>, |
| stdout: Option<Stdio>, |
| stderr: Option<Stdio>, |
| |
| // Emulate linear type semantics. |
| drop_bomb: DropBomb, |
| already_executed: bool, |
| |
| context: String, |
| } |
| |
| impl Command { |
| #[track_caller] |
| pub fn new<P: AsRef<OsStr>>(program: P) -> Self { |
| let program = program.as_ref(); |
| Self { |
| cmd: StdCommand::new(program), |
| stdin_buf: None, |
| drop_bomb: DropBomb::arm(program), |
| stdin: None, |
| stdout: None, |
| stderr: None, |
| already_executed: false, |
| context: String::new(), |
| } |
| } |
| |
| // Internal-only. |
| pub(crate) fn into_raw_command(mut self) -> std::process::Command { |
| self.drop_bomb.defuse(); |
| self.cmd |
| } |
| |
| pub(crate) fn get_context(&self) -> &str { |
| &self.context |
| } |
| |
| /// Appends context to the command, to provide a better error message if the command fails. |
| pub fn context(&mut self, ctx: &str) -> &mut Self { |
| self.context.push_str(&format!("{ctx}\n")); |
| self |
| } |
| |
| /// Specify a stdin input buffer. This is a convenience helper, |
| pub fn stdin_buf<I: AsRef<[u8]>>(&mut self, input: I) -> &mut Self { |
| self.stdin_buf = Some(input.as_ref().to_vec().into_boxed_slice()); |
| self |
| } |
| |
| /// Configuration for the child process’s standard input (stdin) handle. |
| /// |
| /// See [`std::process::Command::stdin`]. |
| pub fn stdin<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self { |
| self.stdin = Some(cfg.into()); |
| self |
| } |
| |
| /// Configuration for the child process’s standard output (stdout) handle. |
| /// |
| /// See [`std::process::Command::stdout`]. |
| pub fn stdout<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self { |
| self.stdout = Some(cfg.into()); |
| self |
| } |
| |
| /// Configuration for the child process’s standard error (stderr) handle. |
| /// |
| /// See [`std::process::Command::stderr`]. |
| pub fn stderr<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self { |
| self.stderr = Some(cfg.into()); |
| self |
| } |
| |
| /// Specify an environment variable. |
| pub fn env<K, V>(&mut self, key: K, value: V) -> &mut Self |
| where |
| K: AsRef<ffi::OsStr>, |
| V: AsRef<ffi::OsStr>, |
| { |
| self.cmd.env(key, value); |
| self |
| } |
| |
| /// Remove an environmental variable. |
| pub fn env_remove<K>(&mut self, key: K) -> &mut Self |
| where |
| K: AsRef<ffi::OsStr>, |
| { |
| self.cmd.env_remove(key); |
| self |
| } |
| |
| /// Generic command argument provider. Prefer specific helper methods if possible. |
| /// Note that for some executables, arguments might be platform specific. For C/C++ |
| /// compilers, arguments might be platform *and* compiler specific. |
| pub fn arg<S>(&mut self, arg: S) -> &mut Self |
| where |
| S: AsRef<ffi::OsStr>, |
| { |
| self.cmd.arg(arg); |
| self |
| } |
| |
| /// Generic command arguments provider. Prefer specific helper methods if possible. |
| /// Note that for some executables, arguments might be platform specific. For C/C++ |
| /// compilers, arguments might be platform *and* compiler specific. |
| pub fn args<S, V>(&mut self, args: V) -> &mut Self |
| where |
| S: AsRef<ffi::OsStr>, |
| V: AsRef<[S]>, |
| { |
| self.cmd.args(args.as_ref()); |
| self |
| } |
| |
| /// Inspect what the underlying [`std::process::Command`] is up to the |
| /// current construction. |
| pub fn inspect<I>(&self, inspector: I) -> &Self |
| where |
| I: FnOnce(&StdCommand), |
| { |
| inspector(&self.cmd); |
| self |
| } |
| |
| /// Set the path where the command will be run. |
| pub fn current_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self { |
| self.cmd.current_dir(path); |
| self |
| } |
| |
| /// Set an auxiliary stream passed to the process, besides the stdio streams. |
| /// |
| /// # Notes |
| /// |
| /// Use with caution! Ideally, only set one aux fd; if there are multiple, their old `fd` may |
| /// overlap with another's `new_fd`, and may break. The caller must make sure this is not the |
| /// case. This function is only "safe" because the safety requirements are practically not |
| /// possible to uphold. |
| #[cfg(unix)] |
| pub fn set_aux_fd<F: Into<std::os::fd::OwnedFd>>( |
| &mut self, |
| new_fd: std::os::fd::RawFd, |
| fd: F, |
| ) -> &mut Self { |
| use std::mem; |
| // NOTE: If more than 1 auxiliary file descriptor is needed, this function should be |
| // rewritten. |
| use std::os::fd::AsRawFd; |
| use std::os::unix::process::CommandExt; |
| |
| let cvt = |x| if x == -1 { Err(std::io::Error::last_os_error()) } else { Ok(()) }; |
| |
| // Ensure fd stays open until the fork. |
| let fd = mem::ManuallyDrop::new(fd.into()); |
| let fd = fd.as_raw_fd(); |
| |
| if fd == new_fd { |
| // If the new file descriptor is already the same as fd, just turn off `FD_CLOEXEC`. |
| let fd_flags = { |
| let ret = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) }; |
| if ret < 0 { |
| panic!("failed to read fd flags: {}", std::io::Error::last_os_error()); |
| } |
| ret |
| }; |
| // Clear `FD_CLOEXEC`. |
| let fd_flags = fd_flags & !libc::FD_CLOEXEC; |
| |
| // SAFETY(io-safety): `fd` is already owned. |
| cvt(unsafe { libc::fcntl(fd, libc::F_SETFD, fd_flags as libc::c_int) }) |
| .expect("disabling CLOEXEC failed"); |
| } |
| let pre_exec = move || { |
| if fd.as_raw_fd() != new_fd { |
| // SAFETY(io-safety): it's the caller's responsibility that we won't override the |
| // target fd. |
| cvt(unsafe { libc::dup2(fd, new_fd) })?; |
| } |
| Ok(()) |
| }; |
| // SAFETY(pre-exec-safe): `dup2` is pre-exec-safe. |
| unsafe { self.cmd.pre_exec(pre_exec) }; |
| self |
| } |
| |
| /// Run the constructed command and assert that it is successfully run. |
| /// |
| /// By default, std{in,out,err} are [`Stdio::piped()`]. |
| #[track_caller] |
| pub fn run(&mut self) -> CompletedProcess { |
| let output = self.command_output(); |
| if !output.status().success() { |
| handle_failed_output(&self, output, panic::Location::caller().line()); |
| } else { |
| verbose_print_command(self, &output); |
| } |
| output |
| } |
| |
| /// Run the constructed command and assert that it does not successfully run. |
| /// |
| /// By default, std{in,out,err} are [`Stdio::piped()`]. |
| #[track_caller] |
| pub fn run_fail(&mut self) -> CompletedProcess { |
| let output = self.command_output(); |
| if output.status().success() { |
| handle_failed_output(&self, output, panic::Location::caller().line()); |
| } else { |
| verbose_print_command(self, &output); |
| } |
| output |
| } |
| |
| /// Run the command but do not check its exit status. Only use if you explicitly don't care |
| /// about the exit status. |
| /// |
| /// Prefer to use [`Self::run`] and [`Self::run_fail`] whenever possible. |
| #[track_caller] |
| pub fn run_unchecked(&mut self) -> CompletedProcess { |
| self.command_output() |
| } |
| |
| #[track_caller] |
| fn command_output(&mut self) -> CompletedProcess { |
| if self.already_executed { |
| panic!("command was already executed"); |
| } else { |
| self.already_executed = true; |
| } |
| |
| self.drop_bomb.defuse(); |
| // let's make sure we piped all the input and outputs |
| self.cmd.stdin(self.stdin.take().unwrap_or(Stdio::piped())); |
| self.cmd.stdout(self.stdout.take().unwrap_or(Stdio::piped())); |
| self.cmd.stderr(self.stderr.take().unwrap_or(Stdio::piped())); |
| |
| let output = if let Some(input) = &self.stdin_buf { |
| let mut child = self.cmd.spawn().unwrap(); |
| |
| { |
| let mut stdin = child.stdin.take().unwrap(); |
| stdin.write_all(input.as_ref()).unwrap(); |
| } |
| |
| child.wait_with_output().expect("failed to get output of finished process") |
| } else { |
| self.cmd.output().expect("failed to get output of finished process") |
| }; |
| output.into() |
| } |
| } |
| |
| /// Represents the result of an executed process. |
| /// The various `assert_` helper methods should preferably be used for |
| /// checking the contents of stdout/stderr. |
| pub struct CompletedProcess { |
| output: Output, |
| } |
| |
| impl CompletedProcess { |
| #[must_use] |
| #[track_caller] |
| pub fn stdout(&self) -> Vec<u8> { |
| self.output.stdout.clone() |
| } |
| |
| #[must_use] |
| #[track_caller] |
| pub fn stdout_utf8(&self) -> String { |
| String::from_utf8(self.output.stdout.clone()).expect("stdout is not valid UTF-8") |
| } |
| |
| #[must_use] |
| #[track_caller] |
| pub fn invalid_stdout_utf8(&self) -> String { |
| String::from_utf8_lossy(&self.output.stdout.clone()).to_string() |
| } |
| |
| #[must_use] |
| #[track_caller] |
| pub fn stderr(&self) -> Vec<u8> { |
| self.output.stderr.clone() |
| } |
| |
| #[must_use] |
| #[track_caller] |
| pub fn stderr_utf8(&self) -> String { |
| String::from_utf8(self.output.stderr.clone()).expect("stderr is not valid UTF-8") |
| } |
| |
| #[must_use] |
| #[track_caller] |
| pub fn invalid_stderr_utf8(&self) -> String { |
| String::from_utf8_lossy(&self.output.stderr.clone()).to_string() |
| } |
| |
| #[must_use] |
| pub fn status(&self) -> ExitStatus { |
| self.output.status |
| } |
| |
| /// Checks that trimmed `stdout` matches trimmed `expected`. |
| #[track_caller] |
| pub fn assert_stdout_equals<S: AsRef<str>>(&self, expected: S) -> &Self { |
| assert_equals(self.stdout_utf8().trim(), expected.as_ref().trim()); |
| self |
| } |
| |
| /// Checks that `stdout` does not contain `unexpected`. |
| #[track_caller] |
| pub fn assert_stdout_not_contains<S: AsRef<str>>(&self, unexpected: S) -> &Self { |
| assert_not_contains(&self.stdout_utf8(), unexpected); |
| self |
| } |
| |
| /// Checks that `stdout` does not contain the regex pattern `unexpected`. |
| #[track_caller] |
| pub fn assert_stdout_not_contains_regex<S: AsRef<str>>(&self, unexpected: S) -> &Self { |
| assert_not_contains_regex(&self.stdout_utf8(), unexpected); |
| self |
| } |
| |
| /// Checks that `stdout` contains `expected`. |
| #[track_caller] |
| pub fn assert_stdout_contains<S: AsRef<str>>(&self, expected: S) -> &Self { |
| assert_contains(&self.stdout_utf8(), expected); |
| self |
| } |
| |
| /// Checks that `stdout` contains the regex pattern `expected`. |
| #[track_caller] |
| pub fn assert_stdout_contains_regex<S: AsRef<str>>(&self, expected: S) -> &Self { |
| assert_contains_regex(&self.stdout_utf8(), expected); |
| self |
| } |
| |
| /// Checks that trimmed `stderr` matches trimmed `expected`. |
| #[track_caller] |
| pub fn assert_stderr_equals<S: AsRef<str>>(&self, expected: S) -> &Self { |
| assert_equals(self.stderr_utf8().trim(), expected.as_ref().trim()); |
| self |
| } |
| |
| /// Checks that `stderr` contains `expected`. |
| #[track_caller] |
| pub fn assert_stderr_contains<S: AsRef<str>>(&self, expected: S) -> &Self { |
| assert_contains(&self.stderr_utf8(), expected); |
| self |
| } |
| |
| /// Checks that `stderr` contains the regex pattern `expected`. |
| #[track_caller] |
| pub fn assert_stderr_contains_regex<S: AsRef<str>>(&self, expected: S) -> &Self { |
| assert_contains_regex(&self.stderr_utf8(), expected); |
| self |
| } |
| |
| /// Checks that `stderr` does not contain `unexpected`. |
| #[track_caller] |
| pub fn assert_stderr_not_contains<S: AsRef<str>>(&self, unexpected: S) -> &Self { |
| assert_not_contains(&self.stderr_utf8(), unexpected); |
| self |
| } |
| |
| /// Checks that `stderr` doesn't contain the Internal Compiler Error message. |
| #[track_caller] |
| pub fn assert_not_ice(&self) -> &Self { |
| self.assert_stderr_not_contains("error: the compiler unexpectedly panicked. this is a bug"); |
| self |
| } |
| |
| /// Checks that `stderr` does not contain the regex pattern `unexpected`. |
| #[track_caller] |
| pub fn assert_stderr_not_contains_regex<S: AsRef<str>>(&self, unexpected: S) -> &Self { |
| assert_not_contains_regex(&self.stderr_utf8(), unexpected); |
| self |
| } |
| |
| /// Check the **exit status** of the process. On Unix, this is *not* the **wait status**. |
| /// |
| /// See [`std::process::ExitStatus::code`]. This is not to be confused with |
| /// [`std::process::ExitCode`]. |
| #[track_caller] |
| pub fn assert_exit_code(&self, code: i32) -> &Self { |
| assert_eq!(self.output.status.code(), Some(code)); |
| self |
| } |
| } |
| |
| impl From<Output> for CompletedProcess { |
| fn from(output: Output) -> Self { |
| Self { output } |
| } |
| } |