tryrun 0.5.3

This crate is no longer maintained.
Documentation
#![deny(
    warnings,
    missing_docs,
    rustdoc::all,
    clippy::pedantic,
    clippy::missing_inline_in_public_items,
    clippy::dbg_macro
)]
#![forbid(unsafe_code)]
#![allow(
    rustdoc::missing_doc_code_examples,
    clippy::non_ascii_literal,
    clippy::inline_always
)]
#![deny(clippy::semicolon_if_nothing_returned)]
#![cfg_attr(nightly, feature(str_internals, unboxed_closures, fn_traits))]
#![doc(test(attr(deny(warnings), forbid(unsafe_code))))]
//! UI testing for arbitrary [`Command`]s.
//!
//! ## Examples
//! All the `.stdout` files listed below can be found in the [`tests/ui`](
//! https://github.com/hyd-dev/tryrun/tree/HEAD/tests/ui) directory in this crate's repository.
//!
//! ### Test that `cargo build -q` runs successfully and has empty output without normalization
//! ```
//! # use tryrun::prelude::*;
//! Command::cargo("build").arg("-q").run_pass(OutputStr::empty(), normalize::noop());
//! ```
//!
//! ### Test that `cargo miri test -q` runs successfully and has expected stdout and empty stderr without normalization
#![cfg_attr(windows, doc = "```ignore")] // FIXME: Fails with strange DLL error.
#![cfg_attr(not(windows), doc = "```")]
//! # use tryrun::prelude::*;
//! Command::cargo_miri("test")
//! #    .env_remove("LD_LIBRARY_PATH")
//! #    .env_remove("DYLD_FALLBACK_LIBRARY_PATH")
//!     .args(&["-q", "--lib", "--target-dir=target/miri"])
//!     .run_pass(OutputStr::stdout("tests/ui/cargo_miri_test.stdout"), normalize::noop());
//! ```
//!
//! ### Test that `rustc -V` runs successfully and has expected stdout after normalization and empty stderr
//! ```
//! # use tryrun::prelude::*;
//! Command::rustc().arg("-V").run_pass(
//!    OutputStr::stdout("tests/ui/rustc_version.stdout"),
//!    normalize::stdout(|output| {
//!        output.truncate("rustc 1.".len());
//!        output.push(b'\n');
//!        output
//!    }),
//! );
//! ```
//!
//! ### Test that `rustdoc -V` runs successfully and has expected stdout after normalization and empty stderr
//! ```
//! # use tryrun::prelude::*;
//! Command::rustdoc().arg("-V").run_pass(
//!    OutputStr::stdout("tests/ui/rustdoc_version.stdout"),
//!    normalize::stdout(|output| {
//!        output.truncate("rustdoc 1.".len());
//!        output.push(b'\n');
//!        output
//!    }),
//! );
//! ```
//!
//! ### Test that `clippy-driver -V` runs successfully and has expected stdout after normalization and empty stderr
//! ```
//! # use tryrun::prelude::*;
//! Command::clippy().arg("-V").run_pass(
//!     OutputStr::stdout("tests/ui/clippy_version.stdout"),
//!     normalize::stdout(|output| {
//!         output.truncate("clippy".len());
//!         output.push(b'\n');
//!         output
//!     }),
//! );
//! ```
//!
//! ### Test that `false` runs unsucessfully and has empty output without normalization
//! ```
//! # use tryrun::prelude::*;
//! Command::new("false").run_fail(OutputStr::empty(), normalize::noop());
//! ```
//!
//! ### Test that `false garbage` also runs unsucessfully and has empty output without normalization
//! ```
//! # use tryrun::prelude::*;
//! Command::new("false").arg("garbage").run_fail(OutputStr::empty(), normalize::noop());
//! ```
//!
//! ### Blessing
//! This will update all used stdout and stderr files:
//! ```shell
//! TRYRUN=bless cargo test
//! ```
//!
//! ###
//! You can see some of the unit tests of this
//! [`src/lib.rs`](https://github.com/hyd-dev/tryrun/blob/HEAD/src/lib.rs) for more examples.
//! Also see this crate's [own UI tests](https://github.com/hyd-dev/tryrun/blob/HEAD/tests/ui.rs)
//! for more *advanced* examples.
//!
//! ## Dependencies
//! To display a colourful diff, the [`git`](https://www.git-scm.com) command needs to be installed,
//! otherwise no helpful output will be shown for test failures.

use sealed::Sealed;
use std::{
    env,
    ffi::OsStr,
    fmt::Display,
    fs::{self, File},
    io::prelude::*,
    path::Path,
    process::{Command, ExitStatus, Stdio},
};

mod sealed {
    use std::ffi::OsStr;

    pub trait Sealed {
        fn new(program: &str) -> Self;
        fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self;
    }
}

/// Normalizer to normalize stdout and stderr.
pub trait Normalizer {
    /// Normalizes stdout. The returned value of this function will be used to compare stdout.
    ///
    /// The default implementation does not do any normalization:
    /// ```
    /// # use tryrun::prelude::*;
    /// struct DefaultNormalizer;
    /// impl Normalizer for DefaultNormalizer {}
    /// assert_eq!(DefaultNormalizer.normalize_stdout(&mut b"foo".to_vec()), b"foo");
    /// ```
    #[inline(always)]
    fn normalize_stdout<'a>(&mut self, stdout: &'a mut Vec<u8>) -> &'a [u8] {
        stdout
    }

    /// Normalizes stderr. The returned value of this function will be used to compare stderr.
    ///
    /// The default implementation does not do any normalization:
    /// ```
    /// # use tryrun::prelude::*;
    /// struct DefaultNormalizer;
    /// impl Normalizer for DefaultNormalizer {}
    /// assert_eq!(DefaultNormalizer.normalize_stderr(&mut b"ar".to_vec()), b"ar");
    /// ```
    #[inline(always)]
    fn normalize_stderr<'a>(&mut self, stderr: &'a mut Vec<u8>) -> &'a [u8] {
        stderr
    }
}

mod normalizer {
    use super::Normalizer;

    #[derive(Debug, Clone, Copy)]
    pub struct Noop;

    impl Normalizer for Noop {}

    #[derive(Debug, Clone, Copy)]
    pub struct Fn<F>(pub(super) F);

    impl<F: FnMut(&mut Vec<u8>) -> &[u8]> Normalizer for Fn<F> {
        #[inline(always)]
        fn normalize_stdout<'a>(&mut self, stdout: &'a mut Vec<u8>) -> &'a [u8] {
            self.0(stdout)
        }

        #[inline(always)]
        fn normalize_stderr<'a>(&mut self, stderr: &'a mut Vec<u8>) -> &'a [u8] {
            self.0(stderr)
        }
    }

    #[derive(Debug, Clone, Copy)]
    pub struct StdOut<F>(pub(super) F);

    impl<F: FnMut(&mut Vec<u8>) -> &[u8]> Normalizer for StdOut<F> {
        #[inline(always)]
        fn normalize_stdout<'a>(&mut self, stdout: &'a mut Vec<u8>) -> &'a [u8] {
            self.0(stdout)
        }
    }

    #[derive(Debug, Clone, Copy)]
    pub struct StdErr<F>(pub(super) F);

    impl<F: FnMut(&mut Vec<u8>) -> &[u8]> Normalizer for StdErr<F> {
        #[inline(always)]
        fn normalize_stderr<'a>(&mut self, stderr: &'a mut Vec<u8>) -> &'a [u8] {
            self.0(stderr)
        }
    }
}

/// Implementation for mutable references.
///
/// ```
/// # use tryrun::prelude::*;
/// fn assert_normalizer(_: impl Normalizer) {}
/// assert_normalizer(&mut normalize::noop());
/// ```
impl<N: Normalizer + ?Sized> Normalizer for &mut N {
    #[track_caller]
    #[inline(always)]
    fn normalize_stdout<'a>(&mut self, stdout: &'a mut Vec<u8>) -> &'a [u8] {
        (**self).normalize_stdout(stdout)
    }

    #[track_caller]
    #[inline(always)]
    fn normalize_stderr<'a>(&mut self, stderr: &'a mut Vec<u8>) -> &'a [u8] {
        (**self).normalize_stderr(stderr)
    }
}

/// Normalizing helper functions.
///
/// ```
/// # #[allow(unused_imports)]
/// use tryrun::normalize;
/// ```
pub mod normalize {
    use super::normalizer::{self, Noop, StdErr, StdOut};

    /// Returns an instance of [`super::Normalizer`] that does nothing.
    ///
    /// ## Example
    /// ```
    /// # use tryrun::prelude::*;
    /// let mut normalizer = normalize::noop();
    /// assert_eq!(normalizer.normalize_stdout(&mut b"foo".to_vec()), b"foo");
    /// assert!(normalizer.normalize_stdout(&mut Vec::new()).is_empty());
    /// assert_eq!(normalizer.normalize_stderr(&mut b"ar".to_vec()), b"ar");
    /// assert!(normalizer.normalize_stderr(&mut Vec::new()).is_empty());
    /// ```
    #[must_use]
    #[inline(always)]
    pub const fn noop() -> Noop {
        Noop
    }

    /// Returns an instance of [`super::Normalizer`] that normalizes both stdout and stderr
    /// using the given closure (or function).
    ///
    /// ## Example
    /// ```
    /// # use tryrun::prelude::*;
    /// let mut count: usize = 0;
    /// let mut normalizer = normalize::closure(|output| {
    ///     assert_eq!(output, b"foo");
    ///     count = count.checked_add(1).unwrap();
    ///     b"ar"
    /// });
    /// assert_eq!(normalizer.normalize_stdout(&mut b"foo".to_vec()), b"ar");
    /// assert_eq!(normalizer.normalize_stderr(&mut b"foo".to_vec()), b"ar");
    /// assert_eq!(count, 2);
    /// ```
    #[must_use]
    #[inline(always)]
    pub fn closure<F: FnMut(&mut Vec<u8>) -> &[u8]>(f: F) -> normalizer::Fn<F> {
        normalizer::Fn(f)
    }

    /// Like [`closure`], but only normalizes stdout.
    ///
    /// ## Example
    /// ```
    /// # use tryrun::prelude::*;
    /// let mut flag = Some(());
    /// let mut normalizer = normalize::stdout(|output| {
    ///     assert_eq!(output, b"foo");
    ///     flag.take().unwrap();
    ///     b"ar"
    /// });
    /// assert_eq!(normalizer.normalize_stdout(&mut b"foo".to_vec()), b"ar");
    /// assert_eq!(normalizer.normalize_stderr(&mut b"foo".to_vec()), b"foo");
    /// assert!(flag.is_none());
    /// ```
    #[must_use]
    #[inline(always)]
    pub fn stdout<F: FnMut(&mut Vec<u8>) -> &[u8]>(f: F) -> StdOut<F> {
        StdOut(f)
    }

    /// Like [`closure`], but only normalizes stderr.
    ///
    /// ## Example
    /// ```
    /// # use tryrun::prelude::*;
    /// let mut flag = Some(());
    /// let mut normalizer = normalize::stderr(|output| {
    ///     assert_eq!(output, b"foo");
    ///     flag.take().unwrap();
    ///     b"ar"
    /// });
    /// assert_eq!(normalizer.normalize_stdout(&mut b"foo".to_vec()), b"foo");
    /// assert_eq!(normalizer.normalize_stderr(&mut b"foo".to_vec()), b"ar");
    /// assert!(flag.is_none());
    /// ```
    #[must_use]
    #[inline(always)]
    pub fn stderr<F: FnMut(&mut Vec<u8>) -> &[u8]>(f: F) -> StdErr<F> {
        StdErr(f)
    }

    mod tests {
        fn _normalizers_copy() {
            fn assert_copy(_: impl Copy) {}
            assert_copy(super::closure(|o| o));
            assert_copy(super::stdout(|o| o));
            assert_copy(super::stderr(|o| o));
        }

        #[cfg(nightly)]
        fn _normalizers_debug() {
            use std::fmt::Debug;
            fn assert_debug(_: impl Debug) {}
            #[derive(Debug)]
            struct Debuggable;
            impl<'a> FnOnce<(&'a mut Vec<u8>,)> for Debuggable {
                type Output = &'a [u8];
                extern "rust-call" fn call_once(
                    self,
                    (output,): (&'a mut Vec<u8>,),
                ) -> Self::Output {
                    output
                }
            }
            impl<'a> FnMut<(&'a mut Vec<u8>,)> for Debuggable {
                extern "rust-call" fn call_mut(
                    &mut self,
                    (output,): (&'a mut Vec<u8>,),
                ) -> Self::Output {
                    output
                }
            }
            assert_debug(super::closure(Debuggable));
            assert_debug(super::stdout(Debuggable));
            assert_debug(super::stderr(Debuggable));
        }
    }
}

/// Path to files that store expected [`Command`] stdout and stderr.
///
/// [`None`] means expecting stdout or stderr to be empty. Note that providing an empty file does
/// not work and will resulting an "output is empty" assertion failure.
#[derive(Default, Debug, Clone, Copy)]
pub struct Output<StdOut, StdErr> {
    /// Path to the stdout file.
    pub stdout: Option<StdOut>,
    /// Path to the stderr file.
    pub stderr: Option<StdErr>,
}

impl<StdOut, StdErr> Output<StdOut, StdErr> {
    /// Expects [empty](None) output.
    ///
    /// ## Example
    /// ```
    /// # use tryrun::prelude::*;
    /// const EMPTY: OutputStr = OutputStr::empty();
    /// assert!(EMPTY.stdout.is_none());
    /// assert!(EMPTY.stderr.is_none());
    /// ```
    #[must_use]
    #[inline(always)]
    pub const fn empty() -> Self {
        Self {
            stdout: None,
            stderr: None,
        }
    }

    /// Expects stdout to match the specified file and [empty](None) stderr.
    ///
    /// ## Example
    /// ```
    /// # use tryrun::prelude::*;
    /// const STDOUT_ONLY: OutputStr = OutputStr::stdout("foo/bar.stdout");
    /// assert_eq!(STDOUT_ONLY.stdout.unwrap(), "foo/bar.stdout");
    /// assert!(STDOUT_ONLY.stderr.is_none());
    /// ```
    #[must_use]
    #[inline(always)]
    pub const fn stdout(stdout: StdOut) -> Self {
        Self {
            stdout: Some(stdout),
            stderr: None,
        }
    }

    /// Expects stderr to match the specified file and [empty](None) stdout.
    ///
    /// ## Example
    /// ```
    /// # use tryrun::prelude::*;
    /// const STDERR_ONLY: OutputStr = OutputStr::stderr("foo/bar.stderr");
    /// assert!(STDERR_ONLY.stdout.is_none());
    /// assert_eq!(STDERR_ONLY.stderr.unwrap(), "foo/bar.stderr");
    /// ```
    #[must_use]
    #[inline(always)]
    pub const fn stderr(stderr: StdErr) -> Self {
        Self {
            stdout: None,
            stderr: Some(stderr),
        }
    }
}

/// A convenience alias of [`struct@Output`] with `&'static str`.
///
/// ## Example
/// ```
/// # use tryrun::prelude::*;
/// OutputStr {
///     stdout: Some("a static string"),
///     stderr: Some("a static string"),
/// }
/// # ;
/// ```
pub type OutputStr = Output<&'static str, &'static str>;

/// Creates an [`struct@Output`] that expects stdout to match `$base_path.stdout`
/// and stderr to match `$base_path.stderr`.
///
/// ## Example
/// ```
/// # use tryrun::prelude::*;
/// const STDOUT_STDERR: OutputStr = tryrun::output!(concat!(env!("OUT_DIR"), "/foo"));
/// assert_eq!(STDOUT_STDERR.stdout.unwrap(), concat!(env!("OUT_DIR"), "/foo.stdout"));
/// assert_eq!(STDOUT_STDERR.stderr.unwrap(), concat!(env!("OUT_DIR"), "/foo.stderr"));
/// ```
#[macro_export]
macro_rules! output {
    ($base_path:expr) => {
        $crate::Output {
            stdout: Some(concat!($base_path, ".stdout")),
            stderr: Some(concat!($base_path, ".stderr")),
        }
    };
}

macro_rules! Output {
    () => {
        Output<impl AsRef<Path>, impl AsRef<Path>>
    };
}

/// An extension trait for [`Command`].
pub trait CommandExt: Sealed {
    /// Creates a [`Command`] that runs `cargo $subcommand`, with flags provided by
    /// [`Self::cargo_args`].
    #[must_use]
    #[inline(always)]
    fn cargo(subcommand: impl AsRef<OsStr>) -> Self
    where
        Self: Sized,
    {
        let mut cargo = Self::new(env!("CARGO"));
        cargo.arg(subcommand).cargo_args();
        cargo
    }

    /// Creates a [`Command`] that runs `cargo miri $subcommand`, with flags provided by
    /// [`Self::cargo_args`].
    #[must_use]
    #[inline(always)]
    fn cargo_miri(subcommand: impl AsRef<OsStr>) -> Self
    where
        Self: Sized,
    {
        let mut cargo_miri = Self::new(env!("CARGO"));
        cargo_miri.arg("miri").arg(subcommand).cargo_args();
        cargo_miri
    }

    /// Adds some appropriate flags to a `cargo` command.
    fn cargo_args(&mut self) -> &mut Self;

    /// Adds some appropriate flags to a `rustc`-like command (such as `rustc` itself, `rustdoc`,
    /// `clippy-driver` or `miri`).
    fn rustc_like(&mut self) -> &mut Self;

    /// Creates a new [`Command`] that runs `rustc` with flags provided by [`Self::rustc_like`].
    #[must_use]
    #[inline(always)]
    fn rustc() -> Self
    where
        Self: Sized,
    {
        let mut rustc = Self::new(env!("TRYRUN_RUSTC"));
        rustc.rustc_like();
        rustc
    }

    /// Creates a new [`Command`] that runs `clippy-driver` with flags provided by
    /// [`Self::rustc_like`].
    #[must_use]
    #[inline(always)]
    fn clippy() -> Self
    where
        Self: Sized,
    {
        let mut clippy = Self::new("clippy-driver");
        clippy.rustc_like();
        clippy
    }

    /// Creates a new [`Command`] that runs `rustdoc` with flags provided by [`Self::rustc_like`].
    #[must_use]
    #[inline(always)]
    fn rustdoc() -> Self
    where
        Self: Sized,
    {
        let mut rustdoc = Self::new(env!("TRYRUN_RUSTDOC"));
        rustdoc.rustc_like();
        rustdoc
    }

    /// Convenience method to run a [`Command`] and discard its output.
    ///
    /// Equivalent to `self.std{in,out,err}(Stdio::null()).status().unwrap()`.
    fn probe(&mut self) -> ExitStatus;

    /// Tries to run the specified command, and tests that it has the expected stdout and
    /// stderr output.
    ///
    /// The `check_status` function is used to assert the [`ExitStatus`] of the command.
    ///
    /// This method is intended to be used in a `#[test]` function. It panics if the command output
    /// mismatches, a system error is encountered, or the `check_status` function returns [`false`].
    ///
    /// [Blessing the output can be requested by setting the `TRYRUN` environment variable to
    /// `bless`.](crate#blessing)
    ///
    /// ## Implementation detail
    /// If the output mismatches, [`git`](https://www.git-scm.com) will be spawned to obtain a
    /// colourful diff.
    ///
    /// **Note:** this detail is subject to change and should not be relied on.
    fn try_run(
        &mut self,
        output: Output!(),
        normalizer: impl Normalizer,
        check_status: impl FnOnce(ExitStatus) -> bool,
    );

    /// Equivalent to [`Self::try_run`] with `|s| s.success()` as `check_status`.
    #[track_caller]
    #[inline(always)]
    fn run_pass(&mut self, output: Output!(), normalizer: impl Normalizer) {
        self.try_run(output, normalizer, |s| s.success());
    }

    /// Equivalent to [`Self::exit_with_code(output, normalizer, 1)`](Self::exit_with_code).
    #[track_caller]
    #[inline(always)]
    fn run_fail(&mut self, output: Output!(), normalizer: impl Normalizer) {
        self.exit_with_code(output, normalizer, 1);
    }

    /// Equivalent to [`Self::try_run`] with `|s| s.code() == Some(code)` as `check_status`.
    #[track_caller]
    #[inline(always)]
    fn exit_with_code(&mut self, output: Output!(), normalizer: impl Normalizer, code: i32) {
        self.try_run(output, normalizer, |s| s.code() == Some(code));
    }
}

impl Sealed for Command {
    #[inline(always)]
    fn new(program: &str) -> Self {
        Self::new(program)
    }

    #[inline(always)]
    fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
        self.arg(arg)
    }
}

#[cfg(nightly)]
#[inline(always)]
fn display_utf8_lossy(s: &[u8]) -> impl Display + '_ {
    use core::str::lossy::Utf8Lossy;
    Utf8Lossy::from_bytes(s)
}

#[cfg(not(nightly))]
#[inline(always)]
fn display_utf8_lossy(s: &[u8]) -> impl Display + '_ {
    String::from_utf8_lossy(s)
}

#[inline(always)]
fn bless() -> bool {
    env::var_os("TRYRUN").as_deref() == Some("bless".as_ref())
}

#[track_caller]
fn compare(expected: Option<&Path>, actual: &[u8], desc: &str) -> bool {
    if let Some(expected) = expected {
        if actual.is_empty() {
            eprintln!("error: {} of the command is empty", desc);
            return false;
        }
        if bless() {
            fs::write(expected, actual).expect("failed to bless output");
        } else {
            let file = match File::open(expected) {
                Ok(file) => file,
                Err(e) => {
                    eprintln!(
                        "failed to open {} file {:?}: {}\n\
                         -------- normalized actual {0} --------\n\
                         {}",
                        desc,
                        expected,
                        e,
                        display_utf8_lossy(actual)
                    );
                    return false;
                }
            };
            if !file
                .bytes()
                .map(|b| b.expect("failed to read the output file"))
                .eq(actual.iter().copied())
            {
                let mut git = Command::new(
                    env::var_os("GIT")
                        .as_deref()
                        .unwrap_or_else(|| "git".as_ref()),
                )
                .args(&["diff", "--no-index", "--color", "--"])
                .arg(expected)
                .arg("-")
                .stdin(Stdio::piped())
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .spawn()
                .expect("failed to spawn git");
                git.stdin
                    .take()
                    .unwrap()
                    .write_all(actual)
                    .unwrap_or_else(|e| {
                        drop(git.kill());
                        git.wait()
                            .expect("failed to wait git to exit; it may become a zombie process");
                        panic!("failed to write actual command output to git: {}", e);
                    });
                let output = git.wait_with_output().expect("failed to wait git to exit");
                if output.stderr.is_empty() {
                    if output.stdout.is_empty() {
                        eprintln!(
                            "error: command {} mismatched, but unfortunately a diff can't be \
                             provided; here is the normalized actual output in bytes anyway: {:?} \
                             (it's expected to match file {:?})",
                            desc, actual, expected
                        );
                    } else {
                        eprintln!(
                            "error: command {} mismatched:\n{}",
                            desc,
                            display_utf8_lossy(&output.stdout)
                        );
                    }
                } else {
                    panic!(
                        "\ngit diff failed [{}]\n\
                         -------- stdout --------\n\
                         {}\
                         -------- stderr --------\n\
                         {}\
                         -------- normalized actual command {} \
                                  (expected to match file {:?}) --------\n\
                         {}",
                        output.status,
                        display_utf8_lossy(&output.stdout),
                        display_utf8_lossy(&output.stderr),
                        desc,
                        expected,
                        display_utf8_lossy(actual)
                    );
                }
                return false;
            }
        }
    } else if !actual.is_empty() {
        eprintln!(
            "error: expected the command to have empty {}, but it printed this (normalized):\n{}",
            desc,
            display_utf8_lossy(actual)
        );
        return false;
    }
    true
}

impl CommandExt for Command {
    #[inline(always)]
    fn cargo_args(&mut self) -> &mut Self {
        #[cfg(release)]
        self.arg("--release");
        self
    }

    #[inline(always)]
    fn rustc_like(&mut self) -> &mut Self {
        self.args(&[concat!("-Copt-level=", env!("TRYRUN_OPT_LEVEL"))])
    }

    #[track_caller]
    #[inline(always)]
    fn probe(&mut self) -> ExitStatus {
        self.stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .expect("failed to spawn command")
    }

    #[track_caller]
    #[inline(always)]
    fn try_run(
        &mut self,
        output: Output!(),
        mut normalizer: impl Normalizer,
        check_status: impl FnOnce(ExitStatus) -> bool,
    ) {
        let mut command = self.output().expect("failed to get output of command");
        assert!(
            check_status(command.status),
            "\nunexpected command exit status [{}]\n\
             -------- command --------\n\
             {:?}\n\
             -------- stdout --------\n\
             {}\
             -------- stderr --------\n\
             {}",
            command.status,
            self,
            display_utf8_lossy(&command.stdout),
            display_utf8_lossy(&command.stderr)
        );
        let stdout = compare(
            output.stdout.as_ref().map(AsRef::as_ref),
            normalizer.normalize_stdout(&mut command.stdout),
            "stdout",
        );
        let stderr = compare(
            output.stderr.as_ref().map(AsRef::as_ref),
            normalizer.normalize_stderr(&mut command.stderr),
            "stderr",
        );
        assert!(
            stdout && stderr,
            "actual command output does not match the expectation: {:?}",
            self
        );
    }
}

/// The prelude.
///
/// ```
/// # #[allow(unused_imports)]
/// use tryrun::prelude::*;
/// ```
pub mod prelude {
    pub use super::{normalize, CommandExt as _, Normalizer, Output, OutputStr};
    pub use std::process::Command;
}

#[cfg(test)]
mod tests {
    use super::{normalize, CommandExt, Normalizer, Output, OutputStr};
    use std::{
        env::consts,
        fmt::Debug,
        path::{Path, PathBuf},
        process::Command,
        str,
    };

    #[test]
    fn compare() {
        assert!(super::compare(None, &[], ""));
        assert!(!super::compare(None, b"not empty", ""));
        if cfg!(not(miri)) && !super::bless() {
            assert!(!super::compare(Path::new(file!()).into(), b"actual", ""));
        }
    }

    fn strip_exe() -> impl Normalizer {
        normalize::stderr(|output| {
            *output = str::from_utf8(output)
                .unwrap()
                .replace(consts::EXE_SUFFIX, "")
                .into_bytes();
            output
        })
    }

    #[test]
    #[cfg_attr(miri, ignore)]
    fn cargo_release() {
        let mut cargo = Command::cargo("build");
        cargo.args(&["-q", "--release"]);
        if cfg!(release) {
            cargo.run_fail(
                Output {
                    stdout: None::<PathBuf>,
                    stderr: Path::new("tests/ui/cargo_release.stderr").into(),
                },
                strip_exe(),
            );
        } else {
            cargo.run_pass(OutputStr::empty(), normalize::noop());
        }
    }

    // All `cargo miri` invocations have to be placed only here, since running them in parallel
    // may result in "Blocking waiting on file lock of ... sysroot" message.
    #[test]
    #[cfg(not(windows))] // FIXME: Fails with strange DLL error.
    #[cfg_attr(miri, ignore)]
    fn cargo_miri() {
        let mut cargo_miri = Command::cargo_miri("test");
        cargo_miri
            .env_remove("LD_LIBRARY_PATH")
            .env_remove("DYLD_FALLBACK_LIBRARY_PATH")
            .args(&["-q", "--release", "--lib", "--target-dir=target/miri"]);
        if cfg!(release) {
            cargo_miri.run_fail(
                OutputStr::stderr("tests/ui/cargo_miri_release.stderr"),
                strip_exe(),
            );
        } else {
            cargo_miri.run_pass(
                OutputStr::stdout("tests/ui/cargo_miri_test.stdout"),
                normalize::noop(),
            );
        }
    }

    #[track_caller]
    fn assert_rustc_like_args(cmd: impl Debug, program: impl Debug) {
        assert_eq!(
            format!("{:?}", cmd),
            format!(
                concat!(r#"{:?} "-Copt-level="#, env!("TRYRUN_OPT_LEVEL"), '"'),
                program
            )
        );
    }

    #[track_caller]
    fn assert_debug_eq(left: impl Debug, right: impl Debug) {
        assert_eq!(format!("{:?}", left), format!("{:?}", right));
    }

    const RELEASE: &str = if cfg!(release) { r#" "--release""# } else { "" };

    #[test]
    #[cfg_attr(miri, ignore)]
    fn cargo_args() {
        assert_debug_eq(
            Command::cargo(""),
            Command::new(env!("CARGO")).arg("").cargo_args(),
        );
        assert_eq!(
            format!("{:?}", Command::cargo("test")),
            format!(r#"{:?} "test"{}"#, env!("CARGO"), RELEASE)
        );
    }

    #[test]
    #[cfg_attr(miri, ignore)]
    fn rustc_args() {
        assert_debug_eq(
            Command::rustc(),
            Command::new(env!("TRYRUN_RUSTC")).rustc_like(),
        );
        assert_rustc_like_args(Command::rustc(), env!("TRYRUN_RUSTC"));
    }

    #[test]
    fn clippy_args() {
        assert_debug_eq(
            Command::clippy(),
            Command::new("clippy-driver").rustc_like(),
        );
        assert_rustc_like_args(Command::clippy(), "clippy-driver");
    }

    #[test]
    #[cfg_attr(miri, ignore)]
    fn rustdoc_args() {
        assert_debug_eq(
            Command::rustdoc(),
            Command::new(env!("TRYRUN_RUSTDOC")).rustc_like(),
        );
        assert_rustc_like_args(Command::rustdoc(), env!("TRYRUN_RUSTDOC"));
    }

    #[test]
    fn rustc_like_args() {
        assert_eq!(
            format!(
                "{:?}",
                Command::new("hello").arg("world").rustc_like().arg("!")
            ),
            concat!(
                r#""hello" "world" "-Copt-level="#,
                env!("TRYRUN_OPT_LEVEL"),
                r#"" "!""#
            )
        );
    }

    #[test]
    #[cfg_attr(miri, ignore)]
    fn rustdoc_fake_empty() {
        Command::rustdoc()
            .arg("--invalid")
            .run_fail(OutputStr::empty(), normalize::stderr(|_| &[]));
    }
}