#![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))))]
#![cfg_attr(windows, doc = "```ignore")] #![cfg_attr(not(windows), doc = "```")]
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;
}
}
pub trait Normalizer {
#[inline(always)]
fn normalize_stdout<'a>(&mut self, stdout: &'a mut Vec<u8>) -> &'a [u8] {
stdout
}
#[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)
}
}
}
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)
}
}
pub mod normalize {
use super::normalizer::{self, Noop, StdErr, StdOut};
#[must_use]
#[inline(always)]
pub const fn noop() -> Noop {
Noop
}
#[must_use]
#[inline(always)]
pub fn closure<F: FnMut(&mut Vec<u8>) -> &[u8]>(f: F) -> normalizer::Fn<F> {
normalizer::Fn(f)
}
#[must_use]
#[inline(always)]
pub fn stdout<F: FnMut(&mut Vec<u8>) -> &[u8]>(f: F) -> StdOut<F> {
StdOut(f)
}
#[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));
}
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct Output<StdOut, StdErr> {
pub stdout: Option<StdOut>,
pub stderr: Option<StdErr>,
}
impl<StdOut, StdErr> Output<StdOut, StdErr> {
#[must_use]
#[inline(always)]
pub const fn empty() -> Self {
Self {
stdout: None,
stderr: None,
}
}
#[must_use]
#[inline(always)]
pub const fn stdout(stdout: StdOut) -> Self {
Self {
stdout: Some(stdout),
stderr: None,
}
}
#[must_use]
#[inline(always)]
pub const fn stderr(stderr: StdErr) -> Self {
Self {
stdout: None,
stderr: Some(stderr),
}
}
}
pub type OutputStr = Output<&'static str, &'static str>;
#[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>>
};
}
pub trait CommandExt: Sealed {
#[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
}
#[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
}
fn cargo_args(&mut self) -> &mut Self;
fn rustc_like(&mut self) -> &mut Self;
#[must_use]
#[inline(always)]
fn rustc() -> Self
where
Self: Sized,
{
let mut rustc = Self::new(env!("TRYRUN_RUSTC"));
rustc.rustc_like();
rustc
}
#[must_use]
#[inline(always)]
fn clippy() -> Self
where
Self: Sized,
{
let mut clippy = Self::new("clippy-driver");
clippy.rustc_like();
clippy
}
#[must_use]
#[inline(always)]
fn rustdoc() -> Self
where
Self: Sized,
{
let mut rustdoc = Self::new(env!("TRYRUN_RUSTDOC"));
rustdoc.rustc_like();
rustdoc
}
fn probe(&mut self) -> ExitStatus;
fn try_run(
&mut self,
output: Output!(),
normalizer: impl Normalizer,
check_status: impl FnOnce(ExitStatus) -> bool,
);
#[track_caller]
#[inline(always)]
fn run_pass(&mut self, output: Output!(), normalizer: impl Normalizer) {
self.try_run(output, normalizer, |s| s.success());
}
#[track_caller]
#[inline(always)]
fn run_fail(&mut self, output: Output!(), normalizer: impl Normalizer) {
self.exit_with_code(output, normalizer, 1);
}
#[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
);
}
}
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());
}
}
#[test]
#[cfg(not(windows))] #[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(|_| &[]));
}
}