diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-18 02:49:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-18 02:49:42 +0000 |
commit | 837b550238aa671a591ccf282dddeab29cadb206 (patch) | |
tree | 914b6b8862bace72bd3245ca184d374b08d8a672 /src/tools/compiletest | |
parent | Adding debian version 1.70.0+dfsg2-1. (diff) | |
download | rustc-837b550238aa671a591ccf282dddeab29cadb206.tar.xz rustc-837b550238aa671a591ccf282dddeab29cadb206.zip |
Merging upstream version 1.71.1+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/tools/compiletest')
-rw-r--r-- | src/tools/compiletest/Cargo.toml | 6 | ||||
-rw-r--r-- | src/tools/compiletest/src/common.rs | 127 | ||||
-rw-r--r-- | src/tools/compiletest/src/header.rs | 326 | ||||
-rw-r--r-- | src/tools/compiletest/src/header/cfg.rs | 52 | ||||
-rw-r--r-- | src/tools/compiletest/src/header/needs.rs | 267 | ||||
-rw-r--r-- | src/tools/compiletest/src/header/tests.rs | 30 | ||||
-rw-r--r-- | src/tools/compiletest/src/lib.rs | 1136 | ||||
-rw-r--r-- | src/tools/compiletest/src/main.rs | 1115 | ||||
-rw-r--r-- | src/tools/compiletest/src/runtest.rs | 169 |
9 files changed, 1871 insertions, 1357 deletions
diff --git a/src/tools/compiletest/Cargo.toml b/src/tools/compiletest/Cargo.toml index 85fd6523c..d2f258320 100644 --- a/src/tools/compiletest/Cargo.toml +++ b/src/tools/compiletest/Cargo.toml @@ -3,6 +3,9 @@ name = "compiletest" version = "0.0.0" edition = "2021" +[lib] +doctest = false + [dependencies] colored = "2" diff = "0.1.10" @@ -20,6 +23,7 @@ once_cell = "1.16.0" walkdir = "2" glob = "0.3.0" lazycell = "1.3.0" +anyhow = "1" [target.'cfg(unix)'.dependencies] libc = "0.2" @@ -28,7 +32,7 @@ libc = "0.2" miow = "0.5" [target.'cfg(windows)'.dependencies.windows] -version = "0.46.0" +version = "0.48.0" features = [ "Win32_Foundation", "Win32_System_Diagnostics_Debug", diff --git a/src/tools/compiletest/src/common.rs b/src/tools/compiletest/src/common.rs index d2f494942..f796c8987 100644 --- a/src/tools/compiletest/src/common.rs +++ b/src/tools/compiletest/src/common.rs @@ -69,6 +69,12 @@ string_enum! { } } +impl Default for Mode { + fn default() -> Self { + Mode::Ui + } +} + impl Mode { pub fn disambiguator(self) -> &'static str { // Pretty-printing tests could run concurrently, and if they do, @@ -125,7 +131,7 @@ pub enum PanicStrategy { } /// Configuration for compiletest -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct Config { /// `true` to overwrite stderr/stdout files instead of complaining about changes in output. pub bless: bool, @@ -303,6 +309,9 @@ pub struct Config { /// The current Rust channel pub channel: String, + /// Whether adding git commit information such as the commit hash has been enabled for building + pub git_hash: bool, + /// The default Rust edition pub edition: Option<String>, @@ -419,19 +428,12 @@ pub struct TargetCfgs { impl TargetCfgs { fn new(config: &Config) -> TargetCfgs { - let targets: HashMap<String, TargetCfg> = if config.stage_id.starts_with("stage0-") { - // #[cfg(bootstrap)] - // Needed only for one cycle, remove during the bootstrap bump. - Self::collect_all_slow(config) - } else { - serde_json::from_str(&rustc_output( - config, - &["--print=all-target-specs-json", "-Zunstable-options"], - )) - .unwrap() - }; - - let mut current = None; + let targets: HashMap<String, TargetCfg> = serde_json::from_str(&rustc_output( + config, + &["--print=all-target-specs-json", "-Zunstable-options"], + )) + .unwrap(); + let mut all_targets = HashSet::new(); let mut all_archs = HashSet::new(); let mut all_oses = HashSet::new(); @@ -452,14 +454,11 @@ impl TargetCfgs { } all_pointer_widths.insert(format!("{}bit", cfg.pointer_width)); - if target == config.target { - current = Some(cfg); - } all_targets.insert(target.into()); } Self { - current: current.expect("current target not found"), + current: Self::get_current_target_config(config), all_targets, all_archs, all_oses, @@ -471,23 +470,87 @@ impl TargetCfgs { } } - // #[cfg(bootstrap)] - // Needed only for one cycle, remove during the bootstrap bump. - fn collect_all_slow(config: &Config) -> HashMap<String, TargetCfg> { - let mut result = HashMap::new(); - for target in rustc_output(config, &["--print=target-list"]).trim().lines() { - let json = rustc_output( - config, - &["--print=target-spec-json", "-Zunstable-options", "--target", target], - ); - match serde_json::from_str(&json) { - Ok(res) => { - result.insert(target.into(), res); + fn get_current_target_config(config: &Config) -> TargetCfg { + let mut arch = None; + let mut os = None; + let mut env = None; + let mut abi = None; + let mut families = Vec::new(); + let mut pointer_width = None; + let mut endian = None; + let mut panic = None; + + for config in + rustc_output(config, &["--print=cfg", "--target", &config.target]).trim().lines() + { + let (name, value) = config + .split_once("=\"") + .map(|(name, value)| { + ( + name, + Some( + value + .strip_suffix("\"") + .expect("key-value pair should be properly quoted"), + ), + ) + }) + .unwrap_or_else(|| (config, None)); + + match name { + "target_arch" => { + arch = Some(value.expect("target_arch should be a key-value pair").to_string()); + } + "target_os" => { + os = Some(value.expect("target_os sould be a key-value pair").to_string()); } - Err(err) => panic!("failed to parse target spec for {target}: {err}"), + "target_env" => { + env = Some(value.expect("target_env should be a key-value pair").to_string()); + } + "target_abi" => { + abi = Some(value.expect("target_abi should be a key-value pair").to_string()); + } + "target_family" => { + families + .push(value.expect("target_family should be a key-value pair").to_string()); + } + "target_pointer_width" => { + pointer_width = Some( + value + .expect("target_pointer_width should be a key-value pair") + .parse::<u32>() + .expect("target_pointer_width should be a valid u32"), + ); + } + "target_endian" => { + endian = Some(match value.expect("target_endian should be a key-value pair") { + "big" => Endian::Big, + "little" => Endian::Little, + _ => panic!("target_endian should be either 'big' or 'little'"), + }); + } + "panic" => { + panic = Some(match value.expect("panic should be a key-value pair") { + "abort" => PanicStrategy::Abort, + "unwind" => PanicStrategy::Unwind, + _ => panic!("panic should be either 'abort' or 'unwind'"), + }); + } + _ => (), } } - result + + TargetCfg { + arch: arch.expect("target configuration should specify target_arch"), + os: os.expect("target configuration should specify target_os"), + env: env.expect("target configuration should specify target_env"), + abi: abi.expect("target configuration should specify target_abi"), + families, + pointer_width: pointer_width + .expect("target configuration should specify target_pointer_width"), + endian: endian.expect("target configuration should specify target_endian"), + panic: panic.expect("target configuration should specify panic"), + } } } diff --git a/src/tools/compiletest/src/header.rs b/src/tools/compiletest/src/header.rs index bc65ec932..8cc935e54 100644 --- a/src/tools/compiletest/src/header.rs +++ b/src/tools/compiletest/src/header.rs @@ -12,13 +12,24 @@ use tracing::*; use crate::common::{Config, Debugger, FailMode, Mode, PassMode}; use crate::header::cfg::parse_cfg_name_directive; use crate::header::cfg::MatchOutcome; -use crate::util; +use crate::header::needs::CachedNeedsConditions; use crate::{extract_cdb_version, extract_gdb_version}; mod cfg; +mod needs; #[cfg(test)] mod tests; +pub struct HeadersCache { + needs: CachedNeedsConditions, +} + +impl HeadersCache { + pub fn load(config: &Config) -> Self { + Self { needs: CachedNeedsConditions::load(config) } + } +} + /// Properties which must be known very early, before actually running /// the test. #[derive(Default)] @@ -36,7 +47,7 @@ impl EarlyProps { pub fn from_reader<R: Read>(config: &Config, testfile: &Path, rdr: R) -> Self { let mut props = EarlyProps::default(); - iter_header(testfile, rdr, &mut |_, ln| { + iter_header(testfile, rdr, &mut |_, ln, _| { config.push_name_value_directive(ln, directives::AUX_BUILD, &mut props.aux, |r| { r.trim().to_string() }); @@ -94,6 +105,9 @@ pub struct TestProps { pub dont_check_compiler_stdout: bool, // For UI tests, allows compiler to generate arbitrary output to stderr pub dont_check_compiler_stderr: bool, + // When checking the output of stdout or stderr check + // that the lines of expected output are a subset of the actual output. + pub compare_output_lines_by_subset: bool, // Don't force a --crate-type=dylib flag on the command line // // Set this for example if you have an auxiliary test file that contains @@ -202,6 +216,7 @@ mod directives { pub const KNOWN_BUG: &'static str = "known-bug"; pub const MIR_UNIT_TEST: &'static str = "unit-test"; pub const REMAP_SRC_BASE: &'static str = "remap-src-base"; + pub const COMPARE_OUTPUT_LINES_BY_SUBSET: &'static str = "compare-output-lines-by-subset"; // This isn't a real directive, just one that is probably mistyped often pub const INCORRECT_COMPILER_FLAGS: &'static str = "compiler-flags"; } @@ -227,6 +242,7 @@ impl TestProps { check_run_results: false, dont_check_compiler_stdout: false, dont_check_compiler_stderr: false, + compare_output_lines_by_subset: false, no_prefer_dynamic: false, pretty_expanded: false, pretty_mode: "normal".to_string(), @@ -293,7 +309,7 @@ impl TestProps { if !testfile.is_dir() { let file = File::open(testfile).unwrap(); - iter_header(testfile, file, &mut |revision, ln| { + iter_header(testfile, file, &mut |revision, ln, _| { if revision.is_some() && revision != cfg { return; } @@ -467,6 +483,11 @@ impl TestProps { s.trim().to_string() }); config.set_name_directive(ln, REMAP_SRC_BASE, &mut self.remap_src_base); + config.set_name_directive( + ln, + COMPARE_OUTPUT_LINES_BY_SUBSET, + &mut self.compare_output_lines_by_subset, + ); }); } @@ -593,7 +614,7 @@ pub fn line_directive<'line>( } } -fn iter_header<R: Read>(testfile: &Path, rdr: R, it: &mut dyn FnMut(Option<&str>, &str)) { +fn iter_header<R: Read>(testfile: &Path, rdr: R, it: &mut dyn FnMut(Option<&str>, &str, usize)) { if testfile.is_dir() { return; } @@ -602,8 +623,10 @@ fn iter_header<R: Read>(testfile: &Path, rdr: R, it: &mut dyn FnMut(Option<&str> let mut rdr = BufReader::new(rdr); let mut ln = String::new(); + let mut line_number = 0; loop { + line_number += 1; ln.clear(); if rdr.read_line(&mut ln).unwrap() == 0 { break; @@ -616,7 +639,7 @@ fn iter_header<R: Read>(testfile: &Path, rdr: R, it: &mut dyn FnMut(Option<&str> if ln.starts_with("fn") || ln.starts_with("mod") { return; } else if let Some((lncfg, ln)) = line_directive(comment, ln) { - it(lncfg, ln); + it(lncfg, ln, line_number); } } } @@ -676,21 +699,6 @@ impl Config { } } - fn parse_needs_matching_clang(&self, line: &str) -> bool { - self.parse_name_directive(line, "needs-matching-clang") - } - - fn parse_needs_profiler_support(&self, line: &str) -> bool { - self.parse_name_directive(line, "needs-profiler-support") - } - - fn has_cfg_prefix(&self, line: &str, prefix: &str) -> bool { - // returns whether this line contains this prefix or not. For prefix - // "ignore", returns true if line says "ignore-x86_64", "ignore-arch", - // "ignore-android" etc. - line.starts_with(prefix) && line.as_bytes().get(prefix.len()) == Some(&b'-') - } - fn parse_name_directive(&self, line: &str, directive: &str) -> bool { // Ensure the directive is a whole word. Do not match "ignore-x86" when // the line says "ignore-x86_64". @@ -878,155 +886,58 @@ where pub fn make_test_description<R: Read>( config: &Config, + cache: &HeadersCache, name: test::TestName, path: &Path, src: R, cfg: Option<&str>, + poisoned: &mut bool, ) -> test::TestDesc { let mut ignore = false; let mut ignore_message = None; let mut should_fail = false; - let rustc_has_profiler_support = env::var_os("RUSTC_PROFILER_SUPPORT").is_some(); - let rustc_has_sanitizer_support = env::var_os("RUSTC_SANITIZER_SUPPORT").is_some(); - let has_asm_support = config.has_asm_support(); - let has_asan = util::ASAN_SUPPORTED_TARGETS.contains(&&*config.target); - let has_cfi = util::CFI_SUPPORTED_TARGETS.contains(&&*config.target); - let has_kcfi = util::KCFI_SUPPORTED_TARGETS.contains(&&*config.target); - let has_kasan = util::KASAN_SUPPORTED_TARGETS.contains(&&*config.target); - let has_lsan = util::LSAN_SUPPORTED_TARGETS.contains(&&*config.target); - let has_msan = util::MSAN_SUPPORTED_TARGETS.contains(&&*config.target); - let has_tsan = util::TSAN_SUPPORTED_TARGETS.contains(&&*config.target); - let has_hwasan = util::HWASAN_SUPPORTED_TARGETS.contains(&&*config.target); - let has_memtag = util::MEMTAG_SUPPORTED_TARGETS.contains(&&*config.target); - let has_shadow_call_stack = util::SHADOWCALLSTACK_SUPPORTED_TARGETS.contains(&&*config.target); - let has_xray = util::XRAY_SUPPORTED_TARGETS.contains(&&*config.target); - - // For tests using the `needs-rust-lld` directive (e.g. for `-Zgcc-ld=lld`), we need to find - // whether `rust-lld` is present in the compiler under test. - // - // The --compile-lib-path is the path to host shared libraries, but depends on the OS. For - // example: - // - on linux, it can be <sysroot>/lib - // - on windows, it can be <sysroot>/bin - // - // However, `rust-lld` is only located under the lib path, so we look for it there. - let has_rust_lld = config - .compile_lib_path - .parent() - .expect("couldn't traverse to the parent of the specified --compile-lib-path") - .join("lib") - .join("rustlib") - .join(&config.target) - .join("bin") - .join(if config.host.contains("windows") { "rust-lld.exe" } else { "rust-lld" }) - .exists(); - - fn is_on_path(file: &'static str) -> impl Fn() -> bool { - move || env::split_paths(&env::var_os("PATH").unwrap()).any(|dir| dir.join(file).is_file()) - } - - // On Windows, dlltool.exe is used for all architectures. - #[cfg(windows)] - let (has_i686_dlltool, has_x86_64_dlltool) = - (is_on_path("dlltool.exe"), is_on_path("dlltool.exe")); - // For non-Windows, there are architecture specific dlltool binaries. - #[cfg(not(windows))] - let (has_i686_dlltool, has_x86_64_dlltool) = - (is_on_path("i686-w64-mingw32-dlltool"), is_on_path("x86_64-w64-mingw32-dlltool")); - - iter_header(path, src, &mut |revision, ln| { + iter_header(path, src, &mut |revision, ln, line_number| { if revision.is_some() && revision != cfg { return; } - macro_rules! reason { + + macro_rules! decision { ($e:expr) => { - ignore |= match $e { - true => { - ignore_message = Some(stringify!($e)); - true + match $e { + IgnoreDecision::Ignore { reason } => { + ignore = true; + // The ignore reason must be a &'static str, so we have to leak memory to + // create it. This is fine, as the header is parsed only at the start of + // compiletest so it won't grow indefinitely. + ignore_message = Some(&*Box::leak(Box::<str>::from(reason))); } - false => ignore, - } - }; - } - - { - let parsed = parse_cfg_name_directive(config, ln, "ignore"); - ignore = match parsed.outcome { - MatchOutcome::Match => { - let reason = parsed.pretty_reason.unwrap(); - // The ignore reason must be a &'static str, so we have to leak memory to - // create it. This is fine, as the header is parsed only at the start of - // compiletest so it won't grow indefinitely. - ignore_message = Some(Box::leak(Box::<str>::from(match parsed.comment { - Some(comment) => format!("ignored {reason} ({comment})"), - None => format!("ignored {reason}"), - })) as &str); - true + IgnoreDecision::Error { message } => { + eprintln!("error: {}:{line_number}: {message}", path.display()); + *poisoned = true; + return; + } + IgnoreDecision::Continue => {} } - MatchOutcome::NoMatch => ignore, - MatchOutcome::External => ignore, - MatchOutcome::Invalid => panic!("invalid line in {}: {ln}", path.display()), }; } - if config.has_cfg_prefix(ln, "only") { - let parsed = parse_cfg_name_directive(config, ln, "only"); - ignore = match parsed.outcome { - MatchOutcome::Match => ignore, - MatchOutcome::NoMatch => { - let reason = parsed.pretty_reason.unwrap(); - // The ignore reason must be a &'static str, so we have to leak memory to - // create it. This is fine, as the header is parsed only at the start of - // compiletest so it won't grow indefinitely. - ignore_message = Some(Box::leak(Box::<str>::from(match parsed.comment { - Some(comment) => format!("only executed {reason} ({comment})"), - None => format!("only executed {reason}"), - })) as &str); - true - } - MatchOutcome::External => ignore, - MatchOutcome::Invalid => panic!("invalid line in {}: {ln}", path.display()), - }; + decision!(cfg::handle_ignore(config, ln)); + decision!(cfg::handle_only(config, ln)); + decision!(needs::handle_needs(&cache.needs, config, ln)); + decision!(ignore_llvm(config, ln)); + decision!(ignore_cdb(config, ln)); + decision!(ignore_gdb(config, ln)); + decision!(ignore_lldb(config, ln)); + + if config.target == "wasm32-unknown-unknown" { + if config.parse_name_directive(ln, directives::CHECK_RUN_RESULTS) { + decision!(IgnoreDecision::Ignore { + reason: "ignored when checking the run results on WASM".into(), + }); + } } - reason!(ignore_llvm(config, ln)); - reason!( - config.run_clang_based_tests_with.is_none() && config.parse_needs_matching_clang(ln) - ); - reason!(!has_asm_support && config.parse_name_directive(ln, "needs-asm-support")); - reason!(!rustc_has_profiler_support && config.parse_needs_profiler_support(ln)); - reason!(!config.run_enabled() && config.parse_name_directive(ln, "needs-run-enabled")); - reason!( - !rustc_has_sanitizer_support - && config.parse_name_directive(ln, "needs-sanitizer-support") - ); - reason!(!has_asan && config.parse_name_directive(ln, "needs-sanitizer-address")); - reason!(!has_cfi && config.parse_name_directive(ln, "needs-sanitizer-cfi")); - reason!(!has_kcfi && config.parse_name_directive(ln, "needs-sanitizer-kcfi")); - reason!(!has_kasan && config.parse_name_directive(ln, "needs-sanitizer-kasan")); - reason!(!has_lsan && config.parse_name_directive(ln, "needs-sanitizer-leak")); - reason!(!has_msan && config.parse_name_directive(ln, "needs-sanitizer-memory")); - reason!(!has_tsan && config.parse_name_directive(ln, "needs-sanitizer-thread")); - reason!(!has_hwasan && config.parse_name_directive(ln, "needs-sanitizer-hwaddress")); - reason!(!has_memtag && config.parse_name_directive(ln, "needs-sanitizer-memtag")); - reason!( - !has_shadow_call_stack - && config.parse_name_directive(ln, "needs-sanitizer-shadow-call-stack") - ); - reason!(!config.can_unwind() && config.parse_name_directive(ln, "needs-unwind")); - reason!(!has_xray && config.parse_name_directive(ln, "needs-xray")); - reason!( - config.target == "wasm32-unknown-unknown" - && config.parse_name_directive(ln, directives::CHECK_RUN_RESULTS) - ); - reason!(config.debugger == Some(Debugger::Cdb) && ignore_cdb(config, ln)); - reason!(config.debugger == Some(Debugger::Gdb) && ignore_gdb(config, ln)); - reason!(config.debugger == Some(Debugger::Lldb) && ignore_lldb(config, ln)); - reason!(!has_rust_lld && config.parse_name_directive(ln, "needs-rust-lld")); - reason!(config.parse_name_directive(ln, "needs-i686-dlltool") && !has_i686_dlltool()); - reason!(config.parse_name_directive(ln, "needs-x86_64-dlltool") && !has_x86_64_dlltool()); should_fail |= config.parse_name_directive(ln, "should-fail"); }); @@ -1043,15 +954,10 @@ pub fn make_test_description<R: Read>( name, ignore, ignore_message, - #[cfg(not(bootstrap))] source_file: "", - #[cfg(not(bootstrap))] start_line: 0, - #[cfg(not(bootstrap))] start_col: 0, - #[cfg(not(bootstrap))] end_line: 0, - #[cfg(not(bootstrap))] end_col: 0, should_panic, compile_fail: false, @@ -1060,22 +966,34 @@ pub fn make_test_description<R: Read>( } } -fn ignore_cdb(config: &Config, line: &str) -> bool { +fn ignore_cdb(config: &Config, line: &str) -> IgnoreDecision { + if config.debugger != Some(Debugger::Cdb) { + return IgnoreDecision::Continue; + } + if let Some(actual_version) = config.cdb_version { - if let Some(min_version) = line.strip_prefix("min-cdb-version:").map(str::trim) { - let min_version = extract_cdb_version(min_version).unwrap_or_else(|| { - panic!("couldn't parse version range: {:?}", min_version); + if let Some(rest) = line.strip_prefix("min-cdb-version:").map(str::trim) { + let min_version = extract_cdb_version(rest).unwrap_or_else(|| { + panic!("couldn't parse version range: {:?}", rest); }); // Ignore if actual version is smaller than the minimum // required version - return actual_version < min_version; + if actual_version < min_version { + return IgnoreDecision::Ignore { + reason: format!("ignored when the CDB version is lower than {rest}"), + }; + } } } - false + IgnoreDecision::Continue } -fn ignore_gdb(config: &Config, line: &str) -> bool { +fn ignore_gdb(config: &Config, line: &str) -> IgnoreDecision { + if config.debugger != Some(Debugger::Gdb) { + return IgnoreDecision::Continue; + } + if let Some(actual_version) = config.gdb_version { if let Some(rest) = line.strip_prefix("min-gdb-version:").map(str::trim) { let (start_ver, end_ver) = extract_version_range(rest, extract_gdb_version) @@ -1088,7 +1006,11 @@ fn ignore_gdb(config: &Config, line: &str) -> bool { } // Ignore if actual version is smaller than the minimum // required version - return actual_version < start_ver; + if actual_version < start_ver { + return IgnoreDecision::Ignore { + reason: format!("ignored when the GDB version is lower than {rest}"), + }; + } } else if let Some(rest) = line.strip_prefix("ignore-gdb-version:").map(str::trim) { let (min_version, max_version) = extract_version_range(rest, extract_gdb_version) .unwrap_or_else(|| { @@ -1099,32 +1021,47 @@ fn ignore_gdb(config: &Config, line: &str) -> bool { panic!("Malformed GDB version range: max < min") } - return actual_version >= min_version && actual_version <= max_version; + if actual_version >= min_version && actual_version <= max_version { + if min_version == max_version { + return IgnoreDecision::Ignore { + reason: format!("ignored when the GDB version is {rest}"), + }; + } else { + return IgnoreDecision::Ignore { + reason: format!("ignored when the GDB version is between {rest}"), + }; + } + } } } - false + IgnoreDecision::Continue } -fn ignore_lldb(config: &Config, line: &str) -> bool { +fn ignore_lldb(config: &Config, line: &str) -> IgnoreDecision { + if config.debugger != Some(Debugger::Lldb) { + return IgnoreDecision::Continue; + } + if let Some(actual_version) = config.lldb_version { - if let Some(min_version) = line.strip_prefix("min-lldb-version:").map(str::trim) { - let min_version = min_version.parse().unwrap_or_else(|e| { - panic!("Unexpected format of LLDB version string: {}\n{:?}", min_version, e); + if let Some(rest) = line.strip_prefix("min-lldb-version:").map(str::trim) { + let min_version = rest.parse().unwrap_or_else(|e| { + panic!("Unexpected format of LLDB version string: {}\n{:?}", rest, e); }); // Ignore if actual version is smaller the minimum required // version - actual_version < min_version - } else { - line.starts_with("rust-lldb") && !config.lldb_native_rust + if actual_version < min_version { + return IgnoreDecision::Ignore { + reason: format!("ignored when the LLDB version is {rest}"), + }; + } } - } else { - false } + IgnoreDecision::Continue } -fn ignore_llvm(config: &Config, line: &str) -> bool { +fn ignore_llvm(config: &Config, line: &str) -> IgnoreDecision { if config.system_llvm && line.starts_with("no-system-llvm") { - return true; + return IgnoreDecision::Ignore { reason: "ignored when the system LLVM is used".into() }; } if let Some(needed_components) = config.parse_name_value_directive(line, "needs-llvm-components") @@ -1137,7 +1074,9 @@ fn ignore_llvm(config: &Config, line: &str) -> bool { if env::var_os("COMPILETEST_NEEDS_ALL_LLVM_COMPONENTS").is_some() { panic!("missing LLVM component: {}", missing_component); } - return true; + return IgnoreDecision::Ignore { + reason: format!("ignored when the {missing_component} LLVM component is missing"), + }; } } if let Some(actual_version) = config.llvm_version { @@ -1145,12 +1084,20 @@ fn ignore_llvm(config: &Config, line: &str) -> bool { let min_version = extract_llvm_version(rest).unwrap(); // Ignore if actual version is smaller the minimum required // version - actual_version < min_version + if actual_version < min_version { + return IgnoreDecision::Ignore { + reason: format!("ignored when the LLVM version is older than {rest}"), + }; + } } else if let Some(rest) = line.strip_prefix("min-system-llvm-version:").map(str::trim) { let min_version = extract_llvm_version(rest).unwrap(); // Ignore if using system LLVM and actual version // is smaller the minimum required version - config.system_llvm && actual_version < min_version + if config.system_llvm && actual_version < min_version { + return IgnoreDecision::Ignore { + reason: format!("ignored when the system LLVM version is older than {rest}"), + }; + } } else if let Some(rest) = line.strip_prefix("ignore-llvm-version:").map(str::trim) { // Syntax is: "ignore-llvm-version: <version1> [- <version2>]" let (v_min, v_max) = @@ -1161,11 +1108,24 @@ fn ignore_llvm(config: &Config, line: &str) -> bool { panic!("Malformed LLVM version range: max < min") } // Ignore if version lies inside of range. - actual_version >= v_min && actual_version <= v_max - } else { - false + if actual_version >= v_min && actual_version <= v_max { + if v_min == v_max { + return IgnoreDecision::Ignore { + reason: format!("ignored when the LLVM version is {rest}"), + }; + } else { + return IgnoreDecision::Ignore { + reason: format!("ignored when the LLVM version is between {rest}"), + }; + } + } } - } else { - false } + IgnoreDecision::Continue +} + +enum IgnoreDecision { + Ignore { reason: String }, + Continue, + Error { message: String }, } diff --git a/src/tools/compiletest/src/header/cfg.rs b/src/tools/compiletest/src/header/cfg.rs index aa36fd708..86a749b93 100644 --- a/src/tools/compiletest/src/header/cfg.rs +++ b/src/tools/compiletest/src/header/cfg.rs @@ -1,8 +1,43 @@ use crate::common::{CompareMode, Config, Debugger}; +use crate::header::IgnoreDecision; use std::collections::HashSet; const EXTRA_ARCHS: &[&str] = &["spirv"]; +pub(super) fn handle_ignore(config: &Config, line: &str) -> IgnoreDecision { + let parsed = parse_cfg_name_directive(config, line, "ignore"); + match parsed.outcome { + MatchOutcome::NoMatch => IgnoreDecision::Continue, + MatchOutcome::Match => IgnoreDecision::Ignore { + reason: match parsed.comment { + Some(comment) => format!("ignored {} ({comment})", parsed.pretty_reason.unwrap()), + None => format!("ignored {}", parsed.pretty_reason.unwrap()), + }, + }, + MatchOutcome::Invalid => IgnoreDecision::Error { message: format!("invalid line: {line}") }, + MatchOutcome::External => IgnoreDecision::Continue, + MatchOutcome::NotADirective => IgnoreDecision::Continue, + } +} + +pub(super) fn handle_only(config: &Config, line: &str) -> IgnoreDecision { + let parsed = parse_cfg_name_directive(config, line, "only"); + match parsed.outcome { + MatchOutcome::Match => IgnoreDecision::Continue, + MatchOutcome::NoMatch => IgnoreDecision::Ignore { + reason: match parsed.comment { + Some(comment) => { + format!("only executed {} ({comment})", parsed.pretty_reason.unwrap()) + } + None => format!("only executed {}", parsed.pretty_reason.unwrap()), + }, + }, + MatchOutcome::Invalid => IgnoreDecision::Error { message: format!("invalid line: {line}") }, + MatchOutcome::External => IgnoreDecision::Continue, + MatchOutcome::NotADirective => IgnoreDecision::Continue, + } +} + /// Parses a name-value directive which contains config-specific information, e.g., `ignore-x86` /// or `normalize-stderr-32bit`. pub(super) fn parse_cfg_name_directive<'a>( @@ -11,10 +46,10 @@ pub(super) fn parse_cfg_name_directive<'a>( prefix: &str, ) -> ParsedNameDirective<'a> { if !line.as_bytes().starts_with(prefix.as_bytes()) { - return ParsedNameDirective::invalid(); + return ParsedNameDirective::not_a_directive(); } if line.as_bytes().get(prefix.len()) != Some(&b'-') { - return ParsedNameDirective::invalid(); + return ParsedNameDirective::not_a_directive(); } let line = &line[prefix.len() + 1..]; @@ -24,7 +59,7 @@ pub(super) fn parse_cfg_name_directive<'a>( // Some of the matchers might be "" depending on what the target information is. To avoid // problems we outright reject empty directives. if name == "" { - return ParsedNameDirective::invalid(); + return ParsedNameDirective::not_a_directive(); } let mut outcome = MatchOutcome::Invalid; @@ -222,8 +257,13 @@ pub(super) struct ParsedNameDirective<'a> { } impl ParsedNameDirective<'_> { - fn invalid() -> Self { - Self { name: None, pretty_reason: None, comment: None, outcome: MatchOutcome::NoMatch } + fn not_a_directive() -> Self { + Self { + name: None, + pretty_reason: None, + comment: None, + outcome: MatchOutcome::NotADirective, + } } } @@ -237,6 +277,8 @@ pub(super) enum MatchOutcome { Invalid, /// The directive is handled by other parts of our tooling. External, + /// The line is not actually a directive. + NotADirective, } trait CustomContains { diff --git a/src/tools/compiletest/src/header/needs.rs b/src/tools/compiletest/src/header/needs.rs new file mode 100644 index 000000000..4a57c6140 --- /dev/null +++ b/src/tools/compiletest/src/header/needs.rs @@ -0,0 +1,267 @@ +use crate::common::{Config, Debugger}; +use crate::header::IgnoreDecision; +use crate::util; + +pub(super) fn handle_needs( + cache: &CachedNeedsConditions, + config: &Config, + ln: &str, +) -> IgnoreDecision { + // Note thet we intentionally still put the needs- prefix here to make the file show up when + // grepping for a directive name, even though we could technically strip that. + let needs = &[ + Need { + name: "needs-asm-support", + condition: config.has_asm_support(), + ignore_reason: "ignored on targets without inline assembly support", + }, + Need { + name: "needs-sanitizer-support", + condition: cache.sanitizer_support, + ignore_reason: "ignored on targets without sanitizers support", + }, + Need { + name: "needs-sanitizer-address", + condition: cache.sanitizer_address, + ignore_reason: "ignored on targets without address sanitizer", + }, + Need { + name: "needs-sanitizer-cfi", + condition: cache.sanitizer_cfi, + ignore_reason: "ignored on targets without CFI sanitizer", + }, + Need { + name: "needs-sanitizer-kcfi", + condition: cache.sanitizer_kcfi, + ignore_reason: "ignored on targets without kernel CFI sanitizer", + }, + Need { + name: "needs-sanitizer-kasan", + condition: cache.sanitizer_kasan, + ignore_reason: "ignored on targets without kernel address sanitizer", + }, + Need { + name: "needs-sanitizer-leak", + condition: cache.sanitizer_leak, + ignore_reason: "ignored on targets without leak sanitizer", + }, + Need { + name: "needs-sanitizer-memory", + condition: cache.sanitizer_memory, + ignore_reason: "ignored on targets without memory sanitizer", + }, + Need { + name: "needs-sanitizer-thread", + condition: cache.sanitizer_thread, + ignore_reason: "ignored on targets without thread sanitizer", + }, + Need { + name: "needs-sanitizer-hwaddress", + condition: cache.sanitizer_hwaddress, + ignore_reason: "ignored on targets without hardware-assisted address sanitizer", + }, + Need { + name: "needs-sanitizer-memtag", + condition: cache.sanitizer_memtag, + ignore_reason: "ignored on targets without memory tagging sanitizer", + }, + Need { + name: "needs-sanitizer-shadow-call-stack", + condition: cache.sanitizer_shadow_call_stack, + ignore_reason: "ignored on targets without shadow call stacks", + }, + Need { + name: "needs-run-enabled", + condition: config.run_enabled(), + ignore_reason: "ignored when running the resulting test binaries is disabled", + }, + Need { + name: "needs-unwind", + condition: config.can_unwind(), + ignore_reason: "ignored on targets without unwinding support", + }, + Need { + name: "needs-profiler-support", + condition: std::env::var_os("RUSTC_PROFILER_SUPPORT").is_some(), + ignore_reason: "ignored when profiler support is disabled", + }, + Need { + name: "needs-matching-clang", + condition: config.run_clang_based_tests_with.is_some(), + ignore_reason: "ignored when the used clang does not match the built LLVM", + }, + Need { + name: "needs-xray", + condition: cache.xray, + ignore_reason: "ignored on targets without xray tracing", + }, + Need { + name: "needs-rust-lld", + condition: cache.rust_lld, + ignore_reason: "ignored on targets without Rust's LLD", + }, + Need { + name: "needs-rust-lldb", + condition: config.debugger != Some(Debugger::Lldb) || config.lldb_native_rust, + ignore_reason: "ignored on targets without Rust's LLDB", + }, + Need { + name: "needs-i686-dlltool", + condition: cache.i686_dlltool, + ignore_reason: "ignored when dlltool for i686 is not present", + }, + Need { + name: "needs-x86_64-dlltool", + condition: cache.x86_64_dlltool, + ignore_reason: "ignored when dlltool for x86_64 is not present", + }, + Need { + name: "needs-dlltool", + condition: cache.dlltool, + ignore_reason: "ignored when dlltool for the current architecture is not present", + }, + Need { + name: "needs-git-hash", + condition: config.git_hash, + ignore_reason: "ignored when git hashes have been omitted for building", + }, + ]; + + let (name, comment) = match ln.split_once([':', ' ']) { + Some((name, comment)) => (name, Some(comment)), + None => (ln, None), + }; + + if !name.starts_with("needs-") { + return IgnoreDecision::Continue; + } + + // Handled elsewhere. + if name == "needs-llvm-components" { + return IgnoreDecision::Continue; + } + + let mut found_valid = false; + for need in needs { + if need.name == name { + if need.condition { + found_valid = true; + break; + } else { + return IgnoreDecision::Ignore { + reason: if let Some(comment) = comment { + format!("{} ({comment})", need.ignore_reason) + } else { + need.ignore_reason.into() + }, + }; + } + } + } + + if found_valid { + IgnoreDecision::Continue + } else { + IgnoreDecision::Error { message: format!("invalid needs directive: {name}") } + } +} + +struct Need { + name: &'static str, + condition: bool, + ignore_reason: &'static str, +} + +pub(super) struct CachedNeedsConditions { + sanitizer_support: bool, + sanitizer_address: bool, + sanitizer_cfi: bool, + sanitizer_kcfi: bool, + sanitizer_kasan: bool, + sanitizer_leak: bool, + sanitizer_memory: bool, + sanitizer_thread: bool, + sanitizer_hwaddress: bool, + sanitizer_memtag: bool, + sanitizer_shadow_call_stack: bool, + xray: bool, + rust_lld: bool, + i686_dlltool: bool, + x86_64_dlltool: bool, + dlltool: bool, +} + +impl CachedNeedsConditions { + pub(super) fn load(config: &Config) -> Self { + let path = std::env::var_os("PATH").expect("missing PATH environment variable"); + let path = std::env::split_paths(&path).collect::<Vec<_>>(); + + // On Windows, dlltool.exe is used for all architectures. + #[cfg(windows)] + let dlltool = path.iter().any(|dir| dir.join("dlltool.exe").is_file()); + + // For non-Windows, there are architecture specific dlltool binaries. + #[cfg(not(windows))] + let i686_dlltool = path.iter().any(|dir| dir.join("i686-w64-mingw32-dlltool").is_file()); + #[cfg(not(windows))] + let x86_64_dlltool = + path.iter().any(|dir| dir.join("x86_64-w64-mingw32-dlltool").is_file()); + + let target = &&*config.target; + Self { + sanitizer_support: std::env::var_os("RUSTC_SANITIZER_SUPPORT").is_some(), + sanitizer_address: util::ASAN_SUPPORTED_TARGETS.contains(target), + sanitizer_cfi: util::CFI_SUPPORTED_TARGETS.contains(target), + sanitizer_kcfi: util::KCFI_SUPPORTED_TARGETS.contains(target), + sanitizer_kasan: util::KASAN_SUPPORTED_TARGETS.contains(target), + sanitizer_leak: util::LSAN_SUPPORTED_TARGETS.contains(target), + sanitizer_memory: util::MSAN_SUPPORTED_TARGETS.contains(target), + sanitizer_thread: util::TSAN_SUPPORTED_TARGETS.contains(target), + sanitizer_hwaddress: util::HWASAN_SUPPORTED_TARGETS.contains(target), + sanitizer_memtag: util::MEMTAG_SUPPORTED_TARGETS.contains(target), + sanitizer_shadow_call_stack: util::SHADOWCALLSTACK_SUPPORTED_TARGETS.contains(target), + xray: util::XRAY_SUPPORTED_TARGETS.contains(target), + + // For tests using the `needs-rust-lld` directive (e.g. for `-Zgcc-ld=lld`), we need to find + // whether `rust-lld` is present in the compiler under test. + // + // The --compile-lib-path is the path to host shared libraries, but depends on the OS. For + // example: + // - on linux, it can be <sysroot>/lib + // - on windows, it can be <sysroot>/bin + // + // However, `rust-lld` is only located under the lib path, so we look for it there. + rust_lld: config + .compile_lib_path + .parent() + .expect("couldn't traverse to the parent of the specified --compile-lib-path") + .join("lib") + .join("rustlib") + .join(target) + .join("bin") + .join(if config.host.contains("windows") { "rust-lld.exe" } else { "rust-lld" }) + .exists(), + + #[cfg(windows)] + i686_dlltool: dlltool, + #[cfg(windows)] + x86_64_dlltool: dlltool, + #[cfg(windows)] + dlltool, + + // For non-Windows, there are architecture specific dlltool binaries. + #[cfg(not(windows))] + i686_dlltool, + #[cfg(not(windows))] + x86_64_dlltool, + #[cfg(not(windows))] + dlltool: if config.matches_arch("x86") { + i686_dlltool + } else if config.matches_arch("x86_64") { + x86_64_dlltool + } else { + false + }, + } + } +} diff --git a/src/tools/compiletest/src/header/tests.rs b/src/tools/compiletest/src/header/tests.rs index acd588d7f..362fba116 100644 --- a/src/tools/compiletest/src/header/tests.rs +++ b/src/tools/compiletest/src/header/tests.rs @@ -1,7 +1,25 @@ +use std::io::Read; use std::path::Path; use crate::common::{Config, Debugger}; -use crate::header::{make_test_description, parse_normalization_string, EarlyProps}; +use crate::header::{parse_normalization_string, EarlyProps, HeadersCache}; + +fn make_test_description<R: Read>( + config: &Config, + name: test::TestName, + path: &Path, + src: R, + cfg: Option<&str>, +) -> test::TestDesc { + let cache = HeadersCache::load(config); + let mut poisoned = false; + let test = + crate::header::make_test_description(config, &cache, name, path, src, cfg, &mut poisoned); + if poisoned { + panic!("poisoned!"); + } + test +} #[test] fn test_parse_normalization_string() { @@ -234,6 +252,16 @@ fn debugger() { } #[test] +fn git_hash() { + let mut config = config(); + config.git_hash = false; + assert!(check_ignore(&config, "// needs-git-hash")); + + config.git_hash = true; + assert!(!check_ignore(&config, "// needs-git-hash")); +} + +#[test] fn sanitizers() { let mut config = config(); diff --git a/src/tools/compiletest/src/lib.rs b/src/tools/compiletest/src/lib.rs new file mode 100644 index 000000000..fc48d0159 --- /dev/null +++ b/src/tools/compiletest/src/lib.rs @@ -0,0 +1,1136 @@ +#![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") + .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"); + + 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), + 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"), + } +} + +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(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(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.clone())); + } + } + } + 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." + ); + } +} diff --git a/src/tools/compiletest/src/main.rs b/src/tools/compiletest/src/main.rs index 6a91d25a8..34d48559c 100644 --- a/src/tools/compiletest/src/main.rs +++ b/src/tools/compiletest/src/main.rs @@ -1,44 +1,6 @@ -#![crate_name = "compiletest"] -// The `test` crate is the only unstable feature -// allowed here, just to share similar code. -#![feature(test)] +use std::{env, sync::Arc}; -extern crate test; - -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 std::sync::Arc; - -#[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 compiletest::{common::Mode, log_config, parse_config, run_tests}; fn main() { tracing_subscriber::fmt::init(); @@ -56,1076 +18,3 @@ fn main() { log_config(&config); run_tests(config); } - -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") - .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") - .optopt("", "edition", "default Rust edition", "EDITION"); - - 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), - 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(), - 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"), - } -} - -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) - }); - collect_tests_from_dir( - config.clone(), - &config.src_base, - &PathBuf::new(), - &inputs, - tests, - found_paths, - &modified_tests, - ) - .unwrap_or_else(|_| panic!("Could not read tests from {}", config.src_base.display())); -} - -/// 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(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(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>, - dir: &Path, - relative_dir_path: &Path, - inputs: &Stamp, - tests: &mut Vec<test::TestDescAndFn>, - found_paths: &mut BTreeSet<PathBuf>, - modified_tests: &Vec<PathBuf>, -) -> 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, &paths, inputs)); - 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(), &paths, inputs)) - } 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(), - &file_path, - &relative_file_path, - inputs, - tests, - found_paths, - modified_tests, - )?; - } - } 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>, - testpaths: &TestPaths, - inputs: &Stamp, -) -> 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, test_name, &test_path, src_file, cfg); - // 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.clone())); - } - } - } - 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." - ); - } -} diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs index 7f0b894f5..5bc4d1642 100644 --- a/src/tools/compiletest/src/runtest.rs +++ b/src/tools/compiletest/src/runtest.rs @@ -32,6 +32,7 @@ use std::process::{Child, Command, ExitStatus, Output, Stdio}; use std::str; use std::sync::Arc; +use anyhow::Context; use glob::glob; use once_cell::sync::Lazy; use tracing::*; @@ -131,7 +132,11 @@ pub fn run(config: Arc<Config>, testpaths: &TestPaths, revision: Option<&str>) { } let cx = TestCx { config: &config, props: &props, testpaths, revision }; - create_dir_all(&cx.output_base_dir()).unwrap(); + create_dir_all(&cx.output_base_dir()) + .with_context(|| { + format!("failed to create output base directory {}", cx.output_base_dir().display()) + }) + .unwrap(); if props.incremental { cx.init_incremental_test(); } @@ -224,6 +229,7 @@ enum Emit { Metadata, LlvmIr, Asm, + LinkArgsAsm, } impl<'test> TestCx<'test> { @@ -1384,7 +1390,9 @@ impl<'test> TestCx<'test> { let actual_errors = json::parse_output(&diagnostic_file_name, &proc_res.stderr, proc_res); let mut unexpected = Vec::new(); let mut found = vec![false; expected_errors.len()]; - for actual_error in &actual_errors { + for mut actual_error in actual_errors { + actual_error.msg = self.normalize_output(&actual_error.msg, &[]); + let opt_index = expected_errors.iter().enumerate().position(|(index, expected_error)| { !found[index] @@ -1403,7 +1411,8 @@ impl<'test> TestCx<'test> { None => { // If the test is a known bug, don't require that the error is annotated - if self.is_unexpected_compiler_message(actual_error, expect_help, expect_note) { + if self.is_unexpected_compiler_message(&actual_error, expect_help, expect_note) + { self.error(&format!( "{}:{}: unexpected {}: '{}'", file_name, @@ -1479,7 +1488,16 @@ impl<'test> TestCx<'test> { } fn compile_test(&self, will_execute: WillExecute, emit: Emit) -> ProcRes { - self.compile_test_general(will_execute, emit, self.props.local_pass_mode()) + self.compile_test_general(will_execute, emit, self.props.local_pass_mode(), Vec::new()) + } + + fn compile_test_with_passes( + &self, + will_execute: WillExecute, + emit: Emit, + passes: Vec<String>, + ) -> ProcRes { + self.compile_test_general(will_execute, emit, self.props.local_pass_mode(), passes) } fn compile_test_general( @@ -1487,6 +1505,7 @@ impl<'test> TestCx<'test> { will_execute: WillExecute, emit: Emit, local_pm: Option<PassMode>, + passes: Vec<String>, ) -> ProcRes { // Only use `make_exe_name` when the test ends up being executed. let output_file = match will_execute { @@ -1523,6 +1542,7 @@ impl<'test> TestCx<'test> { emit, allow_unused, LinkToAux::Yes, + passes, ); self.compose_and_run_compiler(rustc, None) @@ -1773,6 +1793,7 @@ impl<'test> TestCx<'test> { Emit::None, AllowUnused::No, LinkToAux::No, + Vec::new(), ); for key in &aux_props.unset_rustc_env { @@ -1904,6 +1925,7 @@ impl<'test> TestCx<'test> { emit: Emit, allow_unused: AllowUnused, link_to_aux: LinkToAux, + passes: Vec<String>, // Vec of passes under mir-opt test to be dumped ) -> Command { let is_aux = input_file.components().map(|c| c.as_os_str()).any(|c| c == "auxiliary"); let is_rustdoc = self.is_rustdoc() && !is_aux; @@ -1917,6 +1939,12 @@ impl<'test> TestCx<'test> { // Use a single thread for efficiency and a deterministic error message order rustc.arg("-Zthreads=1"); + // Optionally prevent default --sysroot if specified in test compile-flags. + if !self.props.compile_flags.iter().any(|flag| flag.starts_with("--sysroot")) { + // In stage 0, make sure we use `stage0-sysroot` instead of the bootstrap sysroot. + rustc.arg("--sysroot").arg(&self.config.sysroot_base); + } + // Optionally prevent default --target if specified in test compile-flags. let custom_target = self.props.compile_flags.iter().any(|x| x.starts_with("--target")); @@ -1998,9 +2026,18 @@ impl<'test> TestCx<'test> { rustc.arg("-Cstrip=debuginfo"); } MirOpt => { + // We check passes under test to minimize the mir-opt test dump + // if files_for_miropt_test parses the passes, we dump only those passes + // otherwise we conservatively pass -Zdump-mir=all + let zdump_arg = if !passes.is_empty() { + format!("-Zdump-mir={}", passes.join(" | ")) + } else { + "-Zdump-mir=all".to_string() + }; + rustc.args(&[ "-Copt-level=1", - "-Zdump-mir=all", + &zdump_arg, "-Zvalidate-mir", "-Zdump-mir-exclude-pass-number", "-Zmir-pretty-relative-line-numbers=yes", @@ -2046,6 +2083,9 @@ impl<'test> TestCx<'test> { Emit::Asm => { rustc.args(&["--emit", "asm"]); } + Emit::LinkArgsAsm => { + rustc.args(&["-Clink-args=--emit=asm"]); + } } if !is_rustdoc { @@ -2320,6 +2360,7 @@ impl<'test> TestCx<'test> { Emit::LlvmIr, AllowUnused::No, LinkToAux::Yes, + Vec::new(), ); self.compose_and_run_compiler(rustc, None) @@ -2339,16 +2380,26 @@ impl<'test> TestCx<'test> { emit = Emit::Asm; } + Some("bpf-linker") => { + emit = Emit::LinkArgsAsm; + } + Some("ptx-linker") => { // No extra flags needed. } - Some(_) => self.fatal("unknown 'assembly-output' header"), + Some(header) => self.fatal(&format!("unknown 'assembly-output' header: {header}")), None => self.fatal("missing 'assembly-output' header"), } - let rustc = - self.make_compile_args(input_file, output_file, emit, AllowUnused::No, LinkToAux::Yes); + let rustc = self.make_compile_args( + input_file, + output_file, + emit, + AllowUnused::No, + LinkToAux::Yes, + Vec::new(), + ); (self.compose_and_run_compiler(rustc, None), output_path) } @@ -2479,6 +2530,7 @@ impl<'test> TestCx<'test> { Emit::None, AllowUnused::Yes, LinkToAux::Yes, + Vec::new(), ); new_rustdoc.build_all_auxiliary(&mut rustc); @@ -3255,17 +3307,35 @@ impl<'test> TestCx<'test> { match output_kind { TestOutput::Compile => { if !self.props.dont_check_compiler_stdout { - errors += - self.compare_output(stdout_kind, &normalized_stdout, &expected_stdout); + errors += self.compare_output( + stdout_kind, + &normalized_stdout, + &expected_stdout, + self.props.compare_output_lines_by_subset, + ); } if !self.props.dont_check_compiler_stderr { - errors += - self.compare_output(stderr_kind, &normalized_stderr, &expected_stderr); + errors += self.compare_output( + stderr_kind, + &normalized_stderr, + &expected_stderr, + self.props.compare_output_lines_by_subset, + ); } } TestOutput::Run => { - errors += self.compare_output(stdout_kind, &normalized_stdout, &expected_stdout); - errors += self.compare_output(stderr_kind, &normalized_stderr, &expected_stderr); + errors += self.compare_output( + stdout_kind, + &normalized_stdout, + &expected_stdout, + self.props.compare_output_lines_by_subset, + ); + errors += self.compare_output( + stderr_kind, + &normalized_stderr, + &expected_stderr, + self.props.compare_output_lines_by_subset, + ); } } errors @@ -3275,7 +3345,8 @@ impl<'test> TestCx<'test> { if let Some(FailMode::Build) = self.props.fail_mode { // Make sure a build-fail test cannot fail due to failing analysis (e.g. typeck). let pm = Some(PassMode::Check); - let proc_res = self.compile_test_general(WillExecute::No, Emit::Metadata, pm); + let proc_res = + self.compile_test_general(WillExecute::No, Emit::Metadata, pm, Vec::new()); self.check_if_test_should_compile(&proc_res, pm); } @@ -3349,7 +3420,12 @@ impl<'test> TestCx<'test> { ) }); - errors += self.compare_output("fixed", &fixed_code, &expected_fixed); + errors += self.compare_output( + "fixed", + &fixed_code, + &expected_fixed, + self.props.compare_output_lines_by_subset, + ); } else if !expected_fixed.is_empty() { panic!( "the `// run-rustfix` directive wasn't found but a `*.fixed` \ @@ -3439,6 +3515,7 @@ impl<'test> TestCx<'test> { emit_metadata, AllowUnused::No, LinkToAux::Yes, + Vec::new(), ); let res = self.compose_and_run_compiler(rustc, None); if !res.status.success() { @@ -3457,14 +3534,14 @@ impl<'test> TestCx<'test> { let pm = self.pass_mode(); let should_run = self.should_run(pm); let emit_metadata = self.should_emit_metadata(pm); - let proc_res = self.compile_test(should_run, emit_metadata); + let passes = self.get_passes(); + let proc_res = self.compile_test_with_passes(should_run, emit_metadata, passes); + self.check_mir_dump(); if !proc_res.status.success() { self.fatal_proc_rec("compilation failed!", &proc_res); } - self.check_mir_dump(); - if let WillExecute::Yes = should_run { let proc_res = self.exec_compiled_test(); @@ -3474,6 +3551,26 @@ impl<'test> TestCx<'test> { } } + fn get_passes(&self) -> Vec<String> { + let files = miropt_test_tools::files_for_miropt_test( + &self.testpaths.file, + self.config.get_pointer_width(), + ); + + let mut out = Vec::new(); + + for miropt_test_tools::MiroptTestFiles { + from_file: _, + to_file: _, + expected_file: _, + passes, + } in files + { + out.extend(passes); + } + out + } + fn check_mir_dump(&self) { let test_file_contents = fs::read_to_string(&self.testpaths.file).unwrap(); @@ -3503,8 +3600,9 @@ impl<'test> TestCx<'test> { &self.testpaths.file, self.config.get_pointer_width(), ); - - for miropt_test_tools::MiroptTestFiles { from_file, to_file, expected_file } in files { + for miropt_test_tools::MiroptTestFiles { from_file, to_file, expected_file, passes: _ } in + files + { let dumped_string = if let Some(after) = to_file { self.diff_mir_files(from_file.into(), after.into()) } else { @@ -3807,11 +3905,38 @@ impl<'test> TestCx<'test> { } } - fn compare_output(&self, kind: &str, actual: &str, expected: &str) -> usize { + fn compare_output( + &self, + kind: &str, + actual: &str, + expected: &str, + compare_output_by_lines: bool, + ) -> usize { if actual == expected { return 0; } + let tmp; + let (expected, actual): (&str, &str) = if compare_output_by_lines { + let actual_lines: HashSet<_> = actual.lines().collect(); + let expected_lines: Vec<_> = expected.lines().collect(); + let mut used = expected_lines.clone(); + used.retain(|line| actual_lines.contains(line)); + // check if `expected` contains a subset of the lines of `actual` + if used.len() == expected_lines.len() && (expected.is_empty() == actual.is_empty()) { + return 0; + } + if expected_lines.is_empty() { + // if we have no lines to check, force a full overwite + ("", actual) + } else { + tmp = (expected_lines.join("\n"), used.join("\n")); + (&tmp.0, &tmp.1) + } + } else { + (expected, actual) + }; + if !self.config.bless { if expected.is_empty() { println!("normalized {}:\n{}\n", kind, actual); |