blob: bb09c03ef5b7294517e609390bc414a77924a252 [file] [log] [blame]
#![crate_name = "compiletest"]
// The `test` crate is the only unstable feature
// allowed here, just to share similar code.
#![feature(test)]
extern crate test;
#[cfg(test)]
mod tests;
pub mod common;
pub mod compute_diff;
pub mod errors;
pub mod header;
mod json;
mod raise_fd_limit;
mod read2;
pub mod runtest;
pub mod util;
use crate::common::{expected_output_path, output_base_dir, output_relative_path, UI_EXTENSIONS};
use crate::common::{Config, Debugger, Mode, PassMode, TestPaths};
use crate::util::logv;
use build_helper::git::{get_git_modified_files, get_git_untracked_files};
use core::panic;
use getopts::Options;
use lazycell::AtomicLazyCell;
use std::collections::BTreeSet;
use std::ffi::OsString;
use std::fs;
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::SystemTime;
use std::{env, vec};
use test::ColorConfig;
use tracing::*;
use walkdir::WalkDir;
use self::header::{make_test_description, EarlyProps};
use crate::header::HeadersCache;
use std::sync::Arc;
pub fn parse_config(args: Vec<String>) -> Config {
let mut opts = Options::new();
opts.reqopt("", "compile-lib-path", "path to host shared libraries", "PATH")
.reqopt("", "run-lib-path", "path to target shared libraries", "PATH")
.reqopt("", "rustc-path", "path to rustc to use for compiling", "PATH")
.optopt("", "rustdoc-path", "path to rustdoc to use for compiling", "PATH")
.optopt("", "rust-demangler-path", "path to rust-demangler to use in tests", "PATH")
.optopt("", "coverage-dump-path", "path to coverage-dump to use in tests", "PATH")
.reqopt("", "python", "path to python to use for doc tests", "PATH")
.optopt("", "jsondocck-path", "path to jsondocck to use for doc tests", "PATH")
.optopt("", "jsondoclint-path", "path to jsondoclint to use for doc tests", "PATH")
.optopt("", "valgrind-path", "path to Valgrind executable for Valgrind tests", "PROGRAM")
.optflag("", "force-valgrind", "fail if Valgrind tests cannot be run under Valgrind")
.optopt("", "run-clang-based-tests-with", "path to Clang executable", "PATH")
.optopt("", "llvm-filecheck", "path to LLVM's FileCheck binary", "DIR")
.reqopt("", "src-base", "directory to scan for test files", "PATH")
.reqopt("", "build-base", "directory to deposit test outputs", "PATH")
.reqopt("", "sysroot-base", "directory containing the compiler sysroot", "PATH")
.reqopt("", "stage-id", "the target-stage identifier", "stageN-TARGET")
.reqopt(
"",
"mode",
"which sort of compile tests to run",
"run-pass-valgrind | pretty | debug-info | codegen | rustdoc \
| rustdoc-json | codegen-units | incremental | run-make | ui | js-doc-test | mir-opt | assembly",
)
.reqopt(
"",
"suite",
"which suite of compile tests to run. used for nicer error reporting.",
"SUITE",
)
.optopt(
"",
"pass",
"force {check,build,run}-pass tests to this mode.",
"check | build | run",
)
.optopt("", "run", "whether to execute run-* tests", "auto | always | never")
.optflag("", "ignored", "run tests marked as ignored")
.optmulti("", "skip", "skip tests matching SUBSTRING. Can be passed multiple times", "SUBSTRING")
.optflag("", "exact", "filters match exactly")
.optopt(
"",
"runtool",
"supervisor program to run tests under \
(eg. emulator, valgrind)",
"PROGRAM",
)
.optmulti("", "host-rustcflags", "flags to pass to rustc for host", "FLAGS")
.optmulti("", "target-rustcflags", "flags to pass to rustc for target", "FLAGS")
.optflag("", "optimize-tests", "run tests with optimizations enabled")
.optflag("", "verbose", "run tests verbosely, showing all output")
.optflag(
"",
"bless",
"overwrite stderr/stdout files instead of complaining about a mismatch",
)
.optflag("", "quiet", "print one character per test instead of one line")
.optopt("", "color", "coloring: auto, always, never", "WHEN")
.optflag("", "json", "emit json output instead of plaintext output")
.optopt("", "logfile", "file to log test execution to", "FILE")
.optopt("", "target", "the target to build for", "TARGET")
.optopt("", "host", "the host to build for", "HOST")
.optopt("", "cdb", "path to CDB to use for CDB debuginfo tests", "PATH")
.optopt("", "gdb", "path to GDB to use for GDB debuginfo tests", "PATH")
.optopt("", "lldb-version", "the version of LLDB used", "VERSION STRING")
.optopt("", "llvm-version", "the version of LLVM used", "VERSION STRING")
.optflag("", "system-llvm", "is LLVM the system LLVM")
.optopt("", "android-cross-path", "Android NDK standalone path", "PATH")
.optopt("", "adb-path", "path to the android debugger", "PATH")
.optopt("", "adb-test-dir", "path to tests for the android debugger", "PATH")
.optopt("", "lldb-python-dir", "directory containing LLDB's python module", "PATH")
.reqopt("", "cc", "path to a C compiler", "PATH")
.reqopt("", "cxx", "path to a C++ compiler", "PATH")
.reqopt("", "cflags", "flags for the C compiler", "FLAGS")
.reqopt("", "cxxflags", "flags for the CXX compiler", "FLAGS")
.optopt("", "ar", "path to an archiver", "PATH")
.optopt("", "target-linker", "path to a linker for the target", "PATH")
.optopt("", "host-linker", "path to a linker for the host", "PATH")
.reqopt("", "llvm-components", "list of LLVM components built in", "LIST")
.optopt("", "llvm-bin-dir", "Path to LLVM's `bin` directory", "PATH")
.optopt("", "nodejs", "the name of nodejs", "PATH")
.optopt("", "npm", "the name of npm", "PATH")
.optopt("", "remote-test-client", "path to the remote test client", "PATH")
.optopt(
"",
"compare-mode",
"mode describing what file the actual ui output will be compared to",
"COMPARE MODE",
)
.optflag(
"",
"rustfix-coverage",
"enable this to generate a Rustfix coverage file, which is saved in \
`./<build_base>/rustfix_missing_coverage.txt`",
)
.optflag("", "force-rerun", "rerun tests even if the inputs are unchanged")
.optflag("", "only-modified", "only run tests that result been modified")
.optflag("", "nocapture", "")
.optflag("h", "help", "show this message")
.reqopt("", "channel", "current Rust channel", "CHANNEL")
.optflag("", "git-hash", "run tests which rely on commit version being compiled into the binaries")
.optopt("", "edition", "default Rust edition", "EDITION")
.reqopt("", "git-repository", "name of the git repository", "ORG/REPO")
.reqopt("", "nightly-branch", "name of the git branch for nightly", "BRANCH");
let (argv0, args_) = args.split_first().unwrap();
if args.len() == 1 || args[1] == "-h" || args[1] == "--help" {
let message = format!("Usage: {} [OPTIONS] [TESTNAME...]", argv0);
println!("{}", opts.usage(&message));
println!();
panic!()
}
let matches = &match opts.parse(args_) {
Ok(m) => m,
Err(f) => panic!("{:?}", f),
};
if matches.opt_present("h") || matches.opt_present("help") {
let message = format!("Usage: {} [OPTIONS] [TESTNAME...]", argv0);
println!("{}", opts.usage(&message));
println!();
panic!()
}
fn opt_path(m: &getopts::Matches, nm: &str) -> PathBuf {
match m.opt_str(nm) {
Some(s) => PathBuf::from(&s),
None => panic!("no option (=path) found for {}", nm),
}
}
fn make_absolute(path: PathBuf) -> PathBuf {
if path.is_relative() { env::current_dir().unwrap().join(path) } else { path }
}
let target = opt_str2(matches.opt_str("target"));
let android_cross_path = opt_path(matches, "android-cross-path");
let (cdb, cdb_version) = analyze_cdb(matches.opt_str("cdb"), &target);
let (gdb, gdb_version, gdb_native_rust) =
analyze_gdb(matches.opt_str("gdb"), &target, &android_cross_path);
let (lldb_version, lldb_native_rust) = matches
.opt_str("lldb-version")
.as_deref()
.and_then(extract_lldb_version)
.map(|(v, b)| (Some(v), b))
.unwrap_or((None, false));
let color = match matches.opt_str("color").as_deref() {
Some("auto") | None => ColorConfig::AutoColor,
Some("always") => ColorConfig::AlwaysColor,
Some("never") => ColorConfig::NeverColor,
Some(x) => panic!("argument for --color must be auto, always, or never, but found `{}`", x),
};
let llvm_version =
matches.opt_str("llvm-version").as_deref().and_then(header::extract_llvm_version).or_else(
|| header::extract_llvm_version_from_binary(&matches.opt_str("llvm-filecheck")?),
);
let src_base = opt_path(matches, "src-base");
let run_ignored = matches.opt_present("ignored");
let mode = matches.opt_str("mode").unwrap().parse().expect("invalid mode");
let has_tidy = if mode == Mode::Rustdoc {
Command::new("tidy")
.arg("--version")
.stdout(Stdio::null())
.status()
.map_or(false, |status| status.success())
} else {
// Avoid spawning an external command when we know tidy won't be used.
false
};
Config {
bless: matches.opt_present("bless"),
compile_lib_path: make_absolute(opt_path(matches, "compile-lib-path")),
run_lib_path: make_absolute(opt_path(matches, "run-lib-path")),
rustc_path: opt_path(matches, "rustc-path"),
rustdoc_path: matches.opt_str("rustdoc-path").map(PathBuf::from),
rust_demangler_path: matches.opt_str("rust-demangler-path").map(PathBuf::from),
coverage_dump_path: matches.opt_str("coverage-dump-path").map(PathBuf::from),
python: matches.opt_str("python").unwrap(),
jsondocck_path: matches.opt_str("jsondocck-path"),
jsondoclint_path: matches.opt_str("jsondoclint-path"),
valgrind_path: matches.opt_str("valgrind-path"),
force_valgrind: matches.opt_present("force-valgrind"),
run_clang_based_tests_with: matches.opt_str("run-clang-based-tests-with"),
llvm_filecheck: matches.opt_str("llvm-filecheck").map(PathBuf::from),
llvm_bin_dir: matches.opt_str("llvm-bin-dir").map(PathBuf::from),
src_base,
build_base: opt_path(matches, "build-base"),
sysroot_base: opt_path(matches, "sysroot-base"),
stage_id: matches.opt_str("stage-id").unwrap(),
mode,
suite: matches.opt_str("suite").unwrap(),
debugger: None,
run_ignored,
filters: matches.free.clone(),
skip: matches.opt_strs("skip"),
filter_exact: matches.opt_present("exact"),
force_pass_mode: matches.opt_str("pass").map(|mode| {
mode.parse::<PassMode>()
.unwrap_or_else(|_| panic!("unknown `--pass` option `{}` given", mode))
}),
run: matches.opt_str("run").and_then(|mode| match mode.as_str() {
"auto" => None,
"always" => Some(true),
"never" => Some(false),
_ => panic!("unknown `--run` option `{}` given", mode),
}),
logfile: matches.opt_str("logfile").map(|s| PathBuf::from(&s)),
runtool: matches.opt_str("runtool"),
host_rustcflags: matches.opt_strs("host-rustcflags"),
target_rustcflags: matches.opt_strs("target-rustcflags"),
optimize_tests: matches.opt_present("optimize-tests"),
target,
host: opt_str2(matches.opt_str("host")),
cdb,
cdb_version,
gdb,
gdb_version,
gdb_native_rust,
lldb_version,
lldb_native_rust,
llvm_version,
system_llvm: matches.opt_present("system-llvm"),
android_cross_path,
adb_path: opt_str2(matches.opt_str("adb-path")),
adb_test_dir: opt_str2(matches.opt_str("adb-test-dir")),
adb_device_status: opt_str2(matches.opt_str("target")).contains("android")
&& "(none)" != opt_str2(matches.opt_str("adb-test-dir"))
&& !opt_str2(matches.opt_str("adb-test-dir")).is_empty(),
lldb_python_dir: matches.opt_str("lldb-python-dir"),
verbose: matches.opt_present("verbose"),
format: match (matches.opt_present("quiet"), matches.opt_present("json")) {
(true, true) => panic!("--quiet and --json are incompatible"),
(true, false) => test::OutputFormat::Terse,
(false, true) => test::OutputFormat::Json,
(false, false) => test::OutputFormat::Pretty,
},
only_modified: matches.opt_present("only-modified"),
color,
remote_test_client: matches.opt_str("remote-test-client").map(PathBuf::from),
compare_mode: matches
.opt_str("compare-mode")
.map(|s| s.parse().expect("invalid --compare-mode provided")),
rustfix_coverage: matches.opt_present("rustfix-coverage"),
has_tidy,
channel: matches.opt_str("channel").unwrap(),
git_hash: matches.opt_present("git-hash"),
edition: matches.opt_str("edition"),
cc: matches.opt_str("cc").unwrap(),
cxx: matches.opt_str("cxx").unwrap(),
cflags: matches.opt_str("cflags").unwrap(),
cxxflags: matches.opt_str("cxxflags").unwrap(),
ar: matches.opt_str("ar").unwrap_or_else(|| String::from("ar")),
target_linker: matches.opt_str("target-linker"),
host_linker: matches.opt_str("host-linker"),
llvm_components: matches.opt_str("llvm-components").unwrap(),
nodejs: matches.opt_str("nodejs"),
npm: matches.opt_str("npm"),
force_rerun: matches.opt_present("force-rerun"),
target_cfgs: AtomicLazyCell::new(),
nocapture: matches.opt_present("nocapture"),
git_repository: matches.opt_str("git-repository").unwrap(),
nightly_branch: matches.opt_str("nightly-branch").unwrap(),
}
}
pub fn log_config(config: &Config) {
let c = config;
logv(c, "configuration:".to_string());
logv(c, format!("compile_lib_path: {:?}", config.compile_lib_path));
logv(c, format!("run_lib_path: {:?}", config.run_lib_path));
logv(c, format!("rustc_path: {:?}", config.rustc_path.display()));
logv(c, format!("rustdoc_path: {:?}", config.rustdoc_path));
logv(c, format!("rust_demangler_path: {:?}", config.rust_demangler_path));
logv(c, format!("src_base: {:?}", config.src_base.display()));
logv(c, format!("build_base: {:?}", config.build_base.display()));
logv(c, format!("stage_id: {}", config.stage_id));
logv(c, format!("mode: {}", config.mode));
logv(c, format!("run_ignored: {}", config.run_ignored));
logv(c, format!("filters: {:?}", config.filters));
logv(c, format!("skip: {:?}", config.skip));
logv(c, format!("filter_exact: {}", config.filter_exact));
logv(
c,
format!("force_pass_mode: {}", opt_str(&config.force_pass_mode.map(|m| format!("{}", m))),),
);
logv(c, format!("runtool: {}", opt_str(&config.runtool)));
logv(c, format!("host-rustcflags: {:?}", config.host_rustcflags));
logv(c, format!("target-rustcflags: {:?}", config.target_rustcflags));
logv(c, format!("target: {}", config.target));
logv(c, format!("host: {}", config.host));
logv(c, format!("android-cross-path: {:?}", config.android_cross_path.display()));
logv(c, format!("adb_path: {:?}", config.adb_path));
logv(c, format!("adb_test_dir: {:?}", config.adb_test_dir));
logv(c, format!("adb_device_status: {}", config.adb_device_status));
logv(c, format!("ar: {}", config.ar));
logv(c, format!("target-linker: {:?}", config.target_linker));
logv(c, format!("host-linker: {:?}", config.host_linker));
logv(c, format!("verbose: {}", config.verbose));
logv(c, format!("format: {:?}", config.format));
logv(c, "\n".to_string());
}
pub fn opt_str(maybestr: &Option<String>) -> &str {
match *maybestr {
None => "(none)",
Some(ref s) => s,
}
}
pub fn opt_str2(maybestr: Option<String>) -> String {
match maybestr {
None => "(none)".to_owned(),
Some(s) => s,
}
}
pub fn run_tests(config: Arc<Config>) {
// If we want to collect rustfix coverage information,
// we first make sure that the coverage file does not exist.
// It will be created later on.
if config.rustfix_coverage {
let mut coverage_file_path = config.build_base.clone();
coverage_file_path.push("rustfix_missing_coverage.txt");
if coverage_file_path.exists() {
if let Err(e) = fs::remove_file(&coverage_file_path) {
panic!("Could not delete {} due to {}", coverage_file_path.display(), e)
}
}
}
// sadly osx needs some file descriptor limits raised for running tests in
// parallel (especially when we have lots and lots of child processes).
// For context, see #8904
unsafe {
raise_fd_limit::raise_fd_limit();
}
// Prevent issue #21352 UAC blocking .exe containing 'patch' etc. on Windows
// If #11207 is resolved (adding manifest to .exe) this becomes unnecessary
env::set_var("__COMPAT_LAYER", "RunAsInvoker");
// Let tests know which target they're running as
env::set_var("TARGET", &config.target);
let opts = test_opts(&config);
let mut configs = Vec::new();
if let Mode::DebugInfo = config.mode {
// Debugging emscripten code doesn't make sense today
if !config.target.contains("emscripten") {
configs.extend(configure_cdb(&config));
configs.extend(configure_gdb(&config));
configs.extend(configure_lldb(&config));
}
} else {
configs.push(config.clone());
};
let mut tests = Vec::new();
for c in configs {
let mut found_paths = BTreeSet::new();
make_tests(c, &mut tests, &mut found_paths);
check_overlapping_tests(&found_paths);
}
tests.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice()));
let res = test::run_tests_console(&opts, tests);
match res {
Ok(true) => {}
Ok(false) => {
// We want to report that the tests failed, but we also want to give
// some indication of just what tests we were running. Especially on
// CI, where there can be cross-compiled tests for a lot of
// architectures, without this critical information it can be quite
// easy to miss which tests failed, and as such fail to reproduce
// the failure locally.
println!(
"Some tests failed in compiletest suite={}{} mode={} host={} target={}",
config.suite,
config
.compare_mode
.as_ref()
.map(|c| format!(" compare_mode={:?}", c))
.unwrap_or_default(),
config.mode,
config.host,
config.target
);
std::process::exit(1);
}
Err(e) => {
// We don't know if tests passed or not, but if there was an error
// during testing we don't want to just succeed (we may not have
// tested something), so fail.
//
// This should realistically "never" happen, so don't try to make
// this a pretty error message.
panic!("I/O failure during tests: {:?}", e);
}
}
}
fn configure_cdb(config: &Config) -> Option<Arc<Config>> {
config.cdb.as_ref()?;
Some(Arc::new(Config { debugger: Some(Debugger::Cdb), ..config.clone() }))
}
fn configure_gdb(config: &Config) -> Option<Arc<Config>> {
config.gdb_version?;
if config.matches_env("msvc") {
return None;
}
if config.remote_test_client.is_some() && !config.target.contains("android") {
println!(
"WARNING: debuginfo tests are not available when \
testing with remote"
);
return None;
}
if config.target.contains("android") {
println!(
"{} debug-info test uses tcp 5039 port.\
please reserve it",
config.target
);
// android debug-info test uses remote debugger so, we test 1 thread
// at once as they're all sharing the same TCP port to communicate
// over.
//
// we should figure out how to lift this restriction! (run them all
// on different ports allocated dynamically).
env::set_var("RUST_TEST_THREADS", "1");
}
Some(Arc::new(Config { debugger: Some(Debugger::Gdb), ..config.clone() }))
}
fn configure_lldb(config: &Config) -> Option<Arc<Config>> {
config.lldb_python_dir.as_ref()?;
if let Some(350) = config.lldb_version {
println!(
"WARNING: The used version of LLDB (350) has a \
known issue that breaks debuginfo tests. See \
issue #32520 for more information. Skipping all \
LLDB-based tests!",
);
return None;
}
Some(Arc::new(Config { debugger: Some(Debugger::Lldb), ..config.clone() }))
}
pub fn test_opts(config: &Config) -> test::TestOpts {
if env::var("RUST_TEST_NOCAPTURE").is_ok() {
eprintln!(
"WARNING: RUST_TEST_NOCAPTURE is no longer used. \
Use the `--nocapture` flag instead."
);
}
test::TestOpts {
exclude_should_panic: false,
filters: config.filters.clone(),
filter_exact: config.filter_exact,
run_ignored: if config.run_ignored { test::RunIgnored::Yes } else { test::RunIgnored::No },
format: config.format,
logfile: config.logfile.clone(),
run_tests: true,
bench_benchmarks: true,
nocapture: config.nocapture,
color: config.color,
shuffle: false,
shuffle_seed: None,
test_threads: None,
skip: config.skip.clone(),
list: false,
options: test::Options::new(),
time_options: None,
force_run_in_process: false,
fail_fast: std::env::var_os("RUSTC_TEST_FAIL_FAST").is_some(),
}
}
pub fn make_tests(
config: Arc<Config>,
tests: &mut Vec<test::TestDescAndFn>,
found_paths: &mut BTreeSet<PathBuf>,
) {
debug!("making tests from {:?}", config.src_base.display());
let inputs = common_inputs_stamp(&config);
let modified_tests = modified_tests(&config, &config.src_base).unwrap_or_else(|err| {
panic!("modified_tests got error from dir: {}, error: {}", config.src_base.display(), err)
});
let cache = HeadersCache::load(&config);
let mut poisoned = false;
collect_tests_from_dir(
config.clone(),
&cache,
&config.src_base,
&PathBuf::new(),
&inputs,
tests,
found_paths,
&modified_tests,
&mut poisoned,
)
.unwrap_or_else(|_| panic!("Could not read tests from {}", config.src_base.display()));
if poisoned {
eprintln!();
panic!("there are errors in tests");
}
}
/// Returns a stamp constructed from input files common to all test cases.
fn common_inputs_stamp(config: &Config) -> Stamp {
let rust_src_dir = config.find_rust_src_root().expect("Could not find Rust source root");
let mut stamp = Stamp::from_path(&config.rustc_path);
// Relevant pretty printer files
let pretty_printer_files = [
"src/etc/rust_types.py",
"src/etc/gdb_load_rust_pretty_printers.py",
"src/etc/gdb_lookup.py",
"src/etc/gdb_providers.py",
"src/etc/lldb_batchmode.py",
"src/etc/lldb_lookup.py",
"src/etc/lldb_providers.py",
];
for file in &pretty_printer_files {
let path = rust_src_dir.join(file);
stamp.add_path(&path);
}
stamp.add_dir(&rust_src_dir.join("src/etc/natvis"));
stamp.add_dir(&config.run_lib_path);
if let Some(ref rustdoc_path) = config.rustdoc_path {
stamp.add_path(&rustdoc_path);
stamp.add_path(&rust_src_dir.join("src/etc/htmldocck.py"));
}
// Compiletest itself.
stamp.add_dir(&rust_src_dir.join("src/tools/compiletest/"));
stamp
}
fn modified_tests(config: &Config, dir: &Path) -> Result<Vec<PathBuf>, String> {
if !config.only_modified {
return Ok(vec![]);
}
let files =
get_git_modified_files(&config.git_config(), Some(dir), &vec!["rs", "stderr", "fixed"])?
.unwrap_or(vec![]);
// Add new test cases to the list, it will be convenient in daily development.
let untracked_files = get_git_untracked_files(&config.git_config(), None)?.unwrap_or(vec![]);
let all_paths = [&files[..], &untracked_files[..]].concat();
let full_paths = {
let mut full_paths: Vec<PathBuf> = all_paths
.into_iter()
.map(|f| PathBuf::from(f).with_extension("").with_extension("rs"))
.filter_map(|f| if Path::new(&f).exists() { f.canonicalize().ok() } else { None })
.collect();
full_paths.dedup();
full_paths.sort_unstable();
full_paths
};
Ok(full_paths)
}
fn collect_tests_from_dir(
config: Arc<Config>,
cache: &HeadersCache,
dir: &Path,
relative_dir_path: &Path,
inputs: &Stamp,
tests: &mut Vec<test::TestDescAndFn>,
found_paths: &mut BTreeSet<PathBuf>,
modified_tests: &Vec<PathBuf>,
poisoned: &mut bool,
) -> io::Result<()> {
// Ignore directories that contain a file named `compiletest-ignore-dir`.
if dir.join("compiletest-ignore-dir").exists() {
return Ok(());
}
if config.mode == Mode::RunMake && dir.join("Makefile").exists() {
let paths = TestPaths {
file: dir.to_path_buf(),
relative_dir: relative_dir_path.parent().unwrap().to_path_buf(),
};
tests.extend(make_test(config, cache, &paths, inputs, poisoned));
return Ok(());
}
// If we find a test foo/bar.rs, we have to build the
// output directory `$build/foo` so we can write
// `$build/foo/bar` into it. We do this *now* in this
// sequential loop because otherwise, if we do it in the
// tests themselves, they race for the privilege of
// creating the directories and sometimes fail randomly.
let build_dir = output_relative_path(&config, relative_dir_path);
fs::create_dir_all(&build_dir).unwrap();
// Add each `.rs` file as a test, and recurse further on any
// subdirectories we find, except for `aux` directories.
for file in fs::read_dir(dir)? {
let file = file?;
let file_path = file.path();
let file_name = file.file_name();
if is_test(&file_name) && (!config.only_modified || modified_tests.contains(&file_path)) {
debug!("found test file: {:?}", file_path.display());
let rel_test_path = relative_dir_path.join(file_path.file_stem().unwrap());
found_paths.insert(rel_test_path);
let paths =
TestPaths { file: file_path, relative_dir: relative_dir_path.to_path_buf() };
tests.extend(make_test(config.clone(), cache, &paths, inputs, poisoned))
} else if file_path.is_dir() {
let relative_file_path = relative_dir_path.join(file.file_name());
if &file_name != "auxiliary" {
debug!("found directory: {:?}", file_path.display());
collect_tests_from_dir(
config.clone(),
cache,
&file_path,
&relative_file_path,
inputs,
tests,
found_paths,
modified_tests,
poisoned,
)?;
}
} else {
debug!("found other file/directory: {:?}", file_path.display());
}
}
Ok(())
}
/// Returns true if `file_name` looks like a proper test file name.
pub fn is_test(file_name: &OsString) -> bool {
let file_name = file_name.to_str().unwrap();
if !file_name.ends_with(".rs") {
return false;
}
// `.`, `#`, and `~` are common temp-file prefixes.
let invalid_prefixes = &[".", "#", "~"];
!invalid_prefixes.iter().any(|p| file_name.starts_with(p))
}
fn make_test(
config: Arc<Config>,
cache: &HeadersCache,
testpaths: &TestPaths,
inputs: &Stamp,
poisoned: &mut bool,
) -> Vec<test::TestDescAndFn> {
let test_path = if config.mode == Mode::RunMake {
// Parse directives in the Makefile
testpaths.file.join("Makefile")
} else {
PathBuf::from(&testpaths.file)
};
let early_props = EarlyProps::from_file(&config, &test_path);
// Incremental tests are special, they inherently cannot be run in parallel.
// `runtest::run` will be responsible for iterating over revisions.
let revisions = if early_props.revisions.is_empty() || config.mode == Mode::Incremental {
vec![None]
} else {
early_props.revisions.iter().map(Some).collect()
};
revisions
.into_iter()
.map(|revision| {
let src_file =
std::fs::File::open(&test_path).expect("open test file to parse ignores");
let cfg = revision.map(|v| &**v);
let test_name = crate::make_test_name(&config, testpaths, revision);
let mut desc = make_test_description(
&config, cache, test_name, &test_path, src_file, cfg, poisoned,
);
// Ignore tests that already run and are up to date with respect to inputs.
if !config.force_rerun {
desc.ignore |= is_up_to_date(
&config,
testpaths,
&early_props,
revision.map(|s| s.as_str()),
inputs,
);
}
test::TestDescAndFn {
desc,
testfn: make_test_closure(config.clone(), testpaths, revision),
}
})
.collect()
}
fn stamp(config: &Config, testpaths: &TestPaths, revision: Option<&str>) -> PathBuf {
output_base_dir(config, testpaths, revision).join("stamp")
}
fn files_related_to_test(
config: &Config,
testpaths: &TestPaths,
props: &EarlyProps,
revision: Option<&str>,
) -> Vec<PathBuf> {
let mut related = vec![];
if testpaths.file.is_dir() {
// run-make tests use their individual directory
for entry in WalkDir::new(&testpaths.file) {
let path = entry.unwrap().into_path();
if path.is_file() {
related.push(path);
}
}
} else {
related.push(testpaths.file.clone());
}
for aux in &props.aux {
let path = testpaths.file.parent().unwrap().join("auxiliary").join(aux);
related.push(path);
}
// UI test files.
for extension in UI_EXTENSIONS {
let path = expected_output_path(testpaths, revision, &config.compare_mode, extension);
related.push(path);
}
related
}
fn is_up_to_date(
config: &Config,
testpaths: &TestPaths,
props: &EarlyProps,
revision: Option<&str>,
inputs: &Stamp,
) -> bool {
let stamp_name = stamp(config, testpaths, revision);
// Check hash.
let contents = match fs::read_to_string(&stamp_name) {
Ok(f) => f,
Err(ref e) if e.kind() == ErrorKind::InvalidData => panic!("Can't read stamp contents"),
Err(_) => return false,
};
let expected_hash = runtest::compute_stamp_hash(config);
if contents != expected_hash {
return false;
}
// Check timestamps.
let mut inputs = inputs.clone();
for path in files_related_to_test(config, testpaths, props, revision) {
inputs.add_path(&path);
}
inputs < Stamp::from_path(&stamp_name)
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Stamp {
time: SystemTime,
}
impl Stamp {
fn from_path(path: &Path) -> Self {
let mut stamp = Stamp { time: SystemTime::UNIX_EPOCH };
stamp.add_path(path);
stamp
}
fn add_path(&mut self, path: &Path) {
let modified = fs::metadata(path)
.and_then(|metadata| metadata.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
self.time = self.time.max(modified);
}
fn add_dir(&mut self, path: &Path) {
for entry in WalkDir::new(path) {
let entry = entry.unwrap();
if entry.file_type().is_file() {
let modified = entry
.metadata()
.ok()
.and_then(|metadata| metadata.modified().ok())
.unwrap_or(SystemTime::UNIX_EPOCH);
self.time = self.time.max(modified);
}
}
}
}
fn make_test_name(
config: &Config,
testpaths: &TestPaths,
revision: Option<&String>,
) -> test::TestName {
// Print the name of the file, relative to the repository root.
// `src_base` looks like `/path/to/rust/tests/ui`
let root_directory = config.src_base.parent().unwrap().parent().unwrap();
let path = testpaths.file.strip_prefix(root_directory).unwrap();
let debugger = match config.debugger {
Some(d) => format!("-{}", d),
None => String::new(),
};
let mode_suffix = match config.compare_mode {
Some(ref mode) => format!(" ({})", mode.to_str()),
None => String::new(),
};
test::DynTestName(format!(
"[{}{}{}] {}{}",
config.mode,
debugger,
mode_suffix,
path.display(),
revision.map_or("".to_string(), |rev| format!("#{}", rev))
))
}
fn make_test_closure(
config: Arc<Config>,
testpaths: &TestPaths,
revision: Option<&String>,
) -> test::TestFn {
let config = config.clone();
let testpaths = testpaths.clone();
let revision = revision.cloned();
test::DynTestFn(Box::new(move || {
runtest::run(config, &testpaths, revision.as_deref());
Ok(())
}))
}
/// Returns `true` if the given target is an Android target for the
/// purposes of GDB testing.
fn is_android_gdb_target(target: &str) -> bool {
matches!(
&target[..],
"arm-linux-androideabi" | "armv7-linux-androideabi" | "aarch64-linux-android"
)
}
/// Returns `true` if the given target is a MSVC target for the purpouses of CDB testing.
fn is_pc_windows_msvc_target(target: &str) -> bool {
target.ends_with("-pc-windows-msvc")
}
fn find_cdb(target: &str) -> Option<OsString> {
if !(cfg!(windows) && is_pc_windows_msvc_target(target)) {
return None;
}
let pf86 = env::var_os("ProgramFiles(x86)").or_else(|| env::var_os("ProgramFiles"))?;
let cdb_arch = if cfg!(target_arch = "x86") {
"x86"
} else if cfg!(target_arch = "x86_64") {
"x64"
} else if cfg!(target_arch = "aarch64") {
"arm64"
} else if cfg!(target_arch = "arm") {
"arm"
} else {
return None; // No compatible CDB.exe in the Windows 10 SDK
};
let mut path = PathBuf::new();
path.push(pf86);
path.push(r"Windows Kits\10\Debuggers"); // We could check 8.1 etc. too?
path.push(cdb_arch);
path.push(r"cdb.exe");
if !path.exists() {
return None;
}
Some(path.into_os_string())
}
/// Returns Path to CDB
fn analyze_cdb(cdb: Option<String>, target: &str) -> (Option<OsString>, Option<[u16; 4]>) {
let cdb = cdb.map(OsString::from).or_else(|| find_cdb(target));
let mut version = None;
if let Some(cdb) = cdb.as_ref() {
if let Ok(output) = Command::new(cdb).arg("/version").output() {
if let Some(first_line) = String::from_utf8_lossy(&output.stdout).lines().next() {
version = extract_cdb_version(&first_line);
}
}
}
(cdb, version)
}
fn extract_cdb_version(full_version_line: &str) -> Option<[u16; 4]> {
// Example full_version_line: "cdb version 10.0.18362.1"
let version = full_version_line.rsplit(' ').next()?;
let mut components = version.split('.');
let major: u16 = components.next().unwrap().parse().unwrap();
let minor: u16 = components.next().unwrap().parse().unwrap();
let patch: u16 = components.next().unwrap_or("0").parse().unwrap();
let build: u16 = components.next().unwrap_or("0").parse().unwrap();
Some([major, minor, patch, build])
}
/// Returns (Path to GDB, GDB Version, GDB has Rust Support)
fn analyze_gdb(
gdb: Option<String>,
target: &str,
android_cross_path: &PathBuf,
) -> (Option<String>, Option<u32>, bool) {
#[cfg(not(windows))]
const GDB_FALLBACK: &str = "gdb";
#[cfg(windows)]
const GDB_FALLBACK: &str = "gdb.exe";
const MIN_GDB_WITH_RUST: u32 = 7011010;
let fallback_gdb = || {
if is_android_gdb_target(target) {
let mut gdb_path = match android_cross_path.to_str() {
Some(x) => x.to_owned(),
None => panic!("cannot find android cross path"),
};
gdb_path.push_str("/bin/gdb");
gdb_path
} else {
GDB_FALLBACK.to_owned()
}
};
let gdb = match gdb {
None => fallback_gdb(),
Some(ref s) if s.is_empty() => fallback_gdb(), // may be empty if configure found no gdb
Some(ref s) => s.to_owned(),
};
let mut version_line = None;
if let Ok(output) = Command::new(&gdb).arg("--version").output() {
if let Some(first_line) = String::from_utf8_lossy(&output.stdout).lines().next() {
version_line = Some(first_line.to_string());
}
}
let version = match version_line {
Some(line) => extract_gdb_version(&line),
None => return (None, None, false),
};
let gdb_native_rust = version.map_or(false, |v| v >= MIN_GDB_WITH_RUST);
(Some(gdb), version, gdb_native_rust)
}
fn extract_gdb_version(full_version_line: &str) -> Option<u32> {
let full_version_line = full_version_line.trim();
// GDB versions look like this: "major.minor.patch?.yyyymmdd?", with both
// of the ? sections being optional
// We will parse up to 3 digits for each component, ignoring the date
// We skip text in parentheses. This avoids accidentally parsing
// the openSUSE version, which looks like:
// GNU gdb (GDB; openSUSE Leap 15.0) 8.1
// This particular form is documented in the GNU coding standards:
// https://www.gnu.org/prep/standards/html_node/_002d_002dversion.html#g_t_002d_002dversion
let unbracketed_part = full_version_line.split('[').next().unwrap();
let mut splits = unbracketed_part.trim_end().rsplit(' ');
let version_string = splits.next().unwrap();
let mut splits = version_string.split('.');
let major = splits.next().unwrap();
let minor = splits.next().unwrap();
let patch = splits.next();
let major: u32 = major.parse().unwrap();
let (minor, patch): (u32, u32) = match minor.find(not_a_digit) {
None => {
let minor = minor.parse().unwrap();
let patch: u32 = match patch {
Some(patch) => match patch.find(not_a_digit) {
None => patch.parse().unwrap(),
Some(idx) if idx > 3 => 0,
Some(idx) => patch[..idx].parse().unwrap(),
},
None => 0,
};
(minor, patch)
}
// There is no patch version after minor-date (e.g. "4-2012").
Some(idx) => {
let minor = minor[..idx].parse().unwrap();
(minor, 0)
}
};
Some(((major * 1000) + minor) * 1000 + patch)
}
/// Returns (LLDB version, LLDB is rust-enabled)
fn extract_lldb_version(full_version_line: &str) -> Option<(u32, bool)> {
// Extract the major LLDB version from the given version string.
// LLDB version strings are different for Apple and non-Apple platforms.
// The Apple variant looks like this:
//
// LLDB-179.5 (older versions)
// lldb-300.2.51 (new versions)
//
// We are only interested in the major version number, so this function
// will return `Some(179)` and `Some(300)` respectively.
//
// Upstream versions look like:
// lldb version 6.0.1
//
// There doesn't seem to be a way to correlate the Apple version
// with the upstream version, and since the tests were originally
// written against Apple versions, we make a fake Apple version by
// multiplying the first number by 100. This is a hack, but
// normally fine because the only non-Apple version we test is
// rust-enabled.
let full_version_line = full_version_line.trim();
if let Some(apple_ver) =
full_version_line.strip_prefix("LLDB-").or_else(|| full_version_line.strip_prefix("lldb-"))
{
if let Some(idx) = apple_ver.find(not_a_digit) {
let version: u32 = apple_ver[..idx].parse().unwrap();
return Some((version, full_version_line.contains("rust-enabled")));
}
} else if let Some(lldb_ver) = full_version_line.strip_prefix("lldb version ") {
if let Some(idx) = lldb_ver.find(not_a_digit) {
let version: u32 = lldb_ver[..idx].parse().ok()?;
return Some((version * 100, full_version_line.contains("rust-enabled")));
}
}
None
}
fn not_a_digit(c: char) -> bool {
!c.is_digit(10)
}
fn check_overlapping_tests(found_paths: &BTreeSet<PathBuf>) {
let mut collisions = Vec::new();
for path in found_paths {
for ancestor in path.ancestors().skip(1) {
if found_paths.contains(ancestor) {
collisions.push((path, ancestor));
}
}
}
if !collisions.is_empty() {
let collisions: String = collisions
.into_iter()
.map(|(path, check_parent)| format!("test {path:?} clashes with {check_parent:?}\n"))
.collect();
panic!(
"{collisions}\n\
Tests cannot have overlapping names. Make sure they use unique prefixes."
);
}
}