blob: 6d489d079bf0b558ef3675e8fc7b26c31a78ee3d [file] [log] [blame] [edit]
use regex::bytes::Regex;
use spanned::Spanned;
#[cfg(feature = "rustc")]
use crate::{
aux_builds::AuxBuilder, custom_flags::run::Run, custom_flags::rustfix::RustfixMode,
custom_flags::Flag, filter::Match, rustc_stderr,
};
use crate::{
diagnostics::Diagnostics,
parser::CommandParserFunc,
per_test_config::{Comments, Condition},
CommandBuilder,
};
pub use color_eyre;
use color_eyre::eyre::Result;
use std::{
collections::BTreeMap,
num::NonZeroUsize,
path::{Path, PathBuf},
sync::{atomic::AtomicBool, Arc},
};
mod args;
pub use args::{Args, Format};
#[derive(Debug, Clone)]
/// Central datastructure containing all information to run the tests.
pub struct Config {
/// Host triple; usually will be auto-detected.
pub host: Option<String>,
/// `None` to run on the host, otherwise a target triple
pub target: Option<String>,
/// The folder in which to start searching for .rs files
pub root_dir: PathBuf,
/// The binary to actually execute.
pub program: CommandBuilder,
/// What to do in case the stdout/stderr output differs from the expected one.
pub output_conflict_handling: OutputConflictHandling,
/// The recommended command to bless failing tests.
pub bless_command: Option<String>,
/// Where to dump files like the binaries compiled from tests.
/// Defaults to `target/ui` in the current directory.
pub out_dir: PathBuf,
/// Skip test files whose names contain any of these entries.
pub skip_files: Vec<String>,
/// Only test files whose names contain any of these entries.
pub filter_files: Vec<String>,
/// Override the number of threads to use.
pub threads: Option<NonZeroUsize>,
/// Nextest emulation: only list the test itself, not its components.
pub list: bool,
/// Only run the tests that are ignored.
pub run_only_ignored: bool,
/// Filters must match exactly instead of just checking for substrings.
pub filter_exact: bool,
/// The default settings settable via `@` comments
pub comment_defaults: Comments,
/// The symbol(s) that signify the start of a comment.
pub comment_start: &'static str,
/// Custom comment parsers
pub custom_comments: BTreeMap<&'static str, CommandParserFunc>,
/// Custom diagnostic extractor (invoked on the output of tests)
pub diagnostic_extractor: fn(&Path, &[u8]) -> Diagnostics,
/// An atomic bool that can be set to `true` to abort all tests.
/// Will not cancel child processes, but if set from a Ctrl+C handler,
/// the pressing of Ctrl+C will already have cancelled child processes.
pub abort_check: Arc<AtomicBool>,
}
impl Config {
/// Create a blank configuration that doesn't do anything interesting
pub fn dummy() -> Self {
let mut comment_defaults = Comments::default();
comment_defaults.base().require_annotations = Spanned::dummy(true).into();
Self {
host: Default::default(),
target: Default::default(),
root_dir: Default::default(),
program: CommandBuilder::cmd(""),
output_conflict_handling: OutputConflictHandling::Error,
bless_command: Default::default(),
out_dir: Default::default(),
skip_files: Default::default(),
filter_files: Default::default(),
threads: Default::default(),
list: Default::default(),
run_only_ignored: Default::default(),
filter_exact: Default::default(),
comment_defaults,
comment_start: "//",
custom_comments: Default::default(),
diagnostic_extractor: |_, _| Diagnostics {
rendered: Default::default(),
messages: Default::default(),
messages_from_unknown_file_or_line: Default::default(),
},
abort_check: Default::default(),
}
}
/// Create a configuration for testing the output of running
/// `rustc` on the test files.
#[cfg(feature = "rustc")]
pub fn rustc(root_dir: impl Into<PathBuf>) -> Self {
use crate::custom_flags::edition::Edition;
let mut comment_defaults = Comments::default();
#[derive(Debug)]
struct NeedsAsmSupport;
impl Flag for NeedsAsmSupport {
fn must_be_unique(&self) -> bool {
true
}
fn clone_inner(&self) -> Box<dyn Flag> {
Box::new(NeedsAsmSupport)
}
fn test_condition(&self, config: &Config) -> bool {
let target = config.target.as_ref().unwrap();
static ASM_SUPPORTED_ARCHS: &[&str] = &[
"x86", "x86_64", "arm", "aarch64", "riscv32",
"riscv64",
// These targets require an additional asm_experimental_arch feature.
// "nvptx64", "hexagon", "mips", "mips64", "spirv", "wasm32",
];
!ASM_SUPPORTED_ARCHS.iter().any(|arch| target.contains(arch))
}
}
comment_defaults
.base()
.add_custom("edition", Edition("2021".into()));
comment_defaults
.base()
.add_custom("rustfix", RustfixMode::MachineApplicable);
let filters = vec![
(Match::PathBackslash, b"/".to_vec()),
#[cfg(windows)]
(Match::Exact(vec![b'\r']), b"".to_vec()),
#[cfg(windows)]
(Match::Exact(br"\\?\".to_vec()), b"".to_vec()),
];
comment_defaults
.base()
.normalize_stderr
.clone_from(&filters);
comment_defaults.base().normalize_stdout = filters;
comment_defaults.base().exit_status = Spanned::dummy(1).into();
comment_defaults.base().require_annotations = Spanned::dummy(true).into();
let mut config = Self {
host: None,
target: None,
root_dir: root_dir.into(),
program: CommandBuilder::rustc(),
output_conflict_handling: OutputConflictHandling::Error,
bless_command: None,
out_dir: std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().unwrap().join("target"))
.join("ui"),
skip_files: Vec::new(),
filter_files: Vec::new(),
threads: None,
list: false,
run_only_ignored: false,
filter_exact: false,
comment_defaults,
comment_start: "//",
custom_comments: Default::default(),
diagnostic_extractor: rustc_stderr::process,
abort_check: Default::default(),
};
config
.custom_comments
.insert("no-rustfix", |parser, _args, span| {
// args are ignored (can be used as comment)
parser.set_custom_once("no-rustfix", (), span);
});
config
.custom_comments
.insert("edition", |parser, args, _span| {
parser.set_custom_once("edition", Edition((*args).into()), args.span());
});
config
.custom_comments
.insert("needs-asm-support", |parser, _args, span| {
// args are ignored (can be used as comment)
parser.set_custom_once("needs-asm-support", NeedsAsmSupport, span);
});
config.custom_comments.insert("run", |parser, args, span| {
let set = |exit_code| {
parser.set_custom_once("run", Run { exit_code }, args.span());
parser.exit_status = Spanned::new(0, span.clone()).into();
parser.require_annotations = Spanned::new(false, span.clone()).into();
let prev = parser
.custom
.insert("no-rustfix", Spanned::new(vec![Box::new(())], span.clone()));
parser.check(span, prev.is_none(), "`run` implies `no-rustfix`");
};
if args.is_empty() {
set(0);
} else {
match args.content.parse() {
Ok(exit_code) => {
set(exit_code);
}
Err(err) => parser.error(args.span(), err.to_string()),
}
}
});
config.custom_comments.insert("aux-build", |parser, args, span| {
let name = match args.split_once(":") {
Some((name, rest)) => {
parser.error(rest.span(), "proc macros are now auto-detected, you can remove the `:proc-macro` after the file name");
name
},
None => args,
};
parser
.add_custom_spanned("aux-build", AuxBuilder { aux_file: name.map(|n| n.into())}, span);
});
config
}
/// Create a configuration for testing the output of running
/// `cargo` on the test `Cargo.toml` files.
#[cfg(feature = "rustc")]
pub fn cargo(root_dir: impl Into<PathBuf>) -> Self {
let mut this = Self {
program: CommandBuilder::cargo(),
custom_comments: Default::default(),
diagnostic_extractor: rustc_stderr::process_cargo,
comment_start: "#",
..Self::rustc(root_dir)
};
this.comment_defaults.base().custom.clear();
this
}
/// Populate the config with the values from parsed command line arguments.
pub fn with_args(&mut self, args: &Args) {
let Args {
ref filters,
check,
bless,
list,
exact,
ignored,
format: _,
threads,
ref skip,
} = *args;
self.threads = threads.or(self.threads);
self.filter_files.extend_from_slice(filters);
self.skip_files.extend_from_slice(skip);
self.run_only_ignored = ignored;
self.filter_exact = exact;
self.list = list;
if check {
self.output_conflict_handling = OutputConflictHandling::Error;
} else if bless {
self.output_conflict_handling = OutputConflictHandling::Bless;
}
}
/// Replace all occurrences of a path in stderr/stdout with a byte string.
#[track_caller]
pub fn path_filter(&mut self, path: &Path, replacement: &'static (impl AsRef<[u8]> + ?Sized)) {
self.path_stderr_filter(path, replacement);
self.path_stdout_filter(path, replacement);
}
/// Replace all occurrences of a path in stderr with a byte string.
#[track_caller]
pub fn path_stderr_filter(
&mut self,
path: &Path,
replacement: &'static (impl AsRef<[u8]> + ?Sized),
) {
let pattern = path.canonicalize().unwrap();
self.comment_defaults.base().normalize_stderr.push((
pattern.parent().unwrap().into(),
replacement.as_ref().to_owned(),
));
}
/// Replace all occurrences of a path in stdout with a byte string.
#[track_caller]
pub fn path_stdout_filter(
&mut self,
path: &Path,
replacement: &'static (impl AsRef<[u8]> + ?Sized),
) {
let pattern = path.canonicalize().unwrap();
self.comment_defaults.base().normalize_stdout.push((
pattern.parent().unwrap().into(),
replacement.as_ref().to_owned(),
));
}
/// Replace all occurrences of a regex pattern in stderr/stdout with a byte string.
#[track_caller]
pub fn filter(&mut self, pattern: &str, replacement: &'static (impl AsRef<[u8]> + ?Sized)) {
self.stderr_filter(pattern, replacement);
self.stdout_filter(pattern, replacement);
}
/// Replace all occurrences of a regex pattern in stderr with a byte string.
#[track_caller]
pub fn stderr_filter(
&mut self,
pattern: &str,
replacement: &'static (impl AsRef<[u8]> + ?Sized),
) {
self.comment_defaults.base().normalize_stderr.push((
Regex::new(pattern).unwrap().into(),
replacement.as_ref().to_owned(),
));
}
/// Replace all occurrences of a regex pattern in stdout with a byte string.
#[track_caller]
pub fn stdout_filter(
&mut self,
pattern: &str,
replacement: &'static (impl AsRef<[u8]> + ?Sized),
) {
self.comment_defaults.base().normalize_stdout.push((
Regex::new(pattern).unwrap().into(),
replacement.as_ref().to_owned(),
));
}
/// Make sure we have the host and target triples.
pub fn fill_host_and_target(&mut self) -> Result<()> {
if self.host.is_none() {
self.host = Some(
rustc_version::VersionMeta::for_command(std::process::Command::new(
&self.program.program,
))
.map_err(|err| {
color_eyre::eyre::Report::new(err).wrap_err(format!(
"failed to parse rustc version info: {}",
self.program.display().to_string().replace('\\', "/")
))
})?
.host,
);
}
if self.target.is_none() {
self.target = Some(self.host.clone().unwrap());
}
Ok(())
}
/// Check whether the host is the specified string
pub fn host_matches_target(&self) -> bool {
self.host.as_ref().expect("host should have been filled in")
== self
.target
.as_ref()
.expect("target should have been filled in")
}
pub(crate) fn get_pointer_width(&self) -> u8 {
// Taken 1:1 from compiletest-rs
fn get_pointer_width(triple: &str) -> u8 {
if (triple.contains("64")
&& !triple.ends_with("gnux32")
&& !triple.ends_with("gnu_ilp32"))
|| triple.starts_with("s390x")
{
64
} else if triple.starts_with("avr") {
16
} else {
32
}
}
get_pointer_width(self.target.as_ref().unwrap())
}
pub(crate) fn test_condition(&self, condition: &Condition) -> bool {
let target = self.target.as_ref().unwrap();
match condition {
Condition::Bitwidth(bits) => bits.iter().any(|bits| self.get_pointer_width() == *bits),
Condition::Target(t) => t.iter().any(|t| target.contains(&**t)),
Condition::Host(t) => t.iter().any(|t| self.host.as_ref().unwrap().contains(&**t)),
Condition::OnHost => self.host_matches_target(),
}
}
/// Returns whether according to the in-file conditions, this file should be run.
pub fn test_file_conditions(&self, comments: &Comments, revision: &str) -> bool {
if comments
.for_revision(revision)
.flat_map(|r| r.ignore.iter())
.any(|c| self.test_condition(c))
{
return self.run_only_ignored;
}
if comments.for_revision(revision).any(|r| {
r.custom
.values()
.any(|flags| flags.content.iter().any(|flag| flag.test_condition(self)))
}) {
return self.run_only_ignored;
}
comments
.for_revision(revision)
.flat_map(|r| r.only.iter())
.all(|c| self.test_condition(c))
^ self.run_only_ignored
}
pub(crate) fn aborted(&self) -> bool {
self.abort_check.load(std::sync::atomic::Ordering::Relaxed)
}
}
#[derive(Debug, Clone)]
/// The different options for what to do when stdout/stderr files differ from the actual output.
pub enum OutputConflictHandling {
/// Fail the test when mismatches are found, if provided the command string
/// in [`Config::bless_command`] will be suggested as a way to bless the
/// test.
Error,
/// Ignore mismatches in the stderr/stdout files.
Ignore,
/// Instead of erroring if the stderr/stdout differs from the expected
/// automatically replace it with the found output (after applying filters).
Bless,
}