about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/tools/compiletest/src/header.rs311
-rw-r--r--src/tools/compiletest/src/header/cfg.rs52
-rw-r--r--src/tools/compiletest/src/header/needs.rs236
-rw-r--r--src/tools/compiletest/src/header/tests.rs20
-rw-r--r--src/tools/compiletest/src/main.rs26
-rw-r--r--tests/debuginfo/associated-types.rs2
-rw-r--r--tests/debuginfo/borrowed-enum.rs2
-rw-r--r--tests/debuginfo/generic-method-on-generic-struct.rs2
-rw-r--r--tests/debuginfo/generic-struct.rs2
-rw-r--r--tests/debuginfo/generic-tuple-style-enum.rs2
-rw-r--r--tests/debuginfo/method-on-generic-struct.rs2
-rw-r--r--tests/debuginfo/struct-style-enum.rs2
-rw-r--r--tests/debuginfo/tuple-style-enum.rs2
-rw-r--r--tests/debuginfo/unique-enum.rs2
14 files changed, 467 insertions, 196 deletions
diff --git a/src/tools/compiletest/src/header.rs b/src/tools/compiletest/src/header.rs
index a7efe16150e..aaa70bf19b2 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()
             });
@@ -288,7 +299,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;
                 }
@@ -582,7 +593,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;
     }
@@ -591,8 +602,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;
@@ -605,7 +618,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);
         }
     }
 }
@@ -665,21 +678,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".
@@ -867,155 +865,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");
     });
 
@@ -1049,22 +950,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)
@@ -1077,7 +990,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(|| {
@@ -1088,32 +1005,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")
@@ -1126,7 +1058,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 {
@@ -1134,12 +1068,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) =
@@ -1150,11 +1092,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 3b9333dfe7a..a9694d4d52c 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;
@@ -218,8 +253,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,
+        }
     }
 }
 
@@ -233,6 +273,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 00000000000..35d6179abaa
--- /dev/null
+++ b/src/tools/compiletest/src/header/needs.rs
@@ -0,0 +1,236 @@
+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",
+        },
+    ];
+
+    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,
+}
+
+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<_>>();
+
+        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(),
+
+            // On Windows, dlltool.exe is used for all architectures.
+            #[cfg(windows)]
+            i686_dlltool: path.iter().any(|dir| dir.join("dlltool.exe").is_file()),
+            #[cfg(windows)]
+            x86_64_dlltool: path.iter().any(|dir| dir.join("dlltool.exe").is_file()),
+
+            // For non-Windows, there are architecture specific dlltool binaries.
+            #[cfg(not(windows))]
+            i686_dlltool: path.iter().any(|dir| dir.join("i686-w64-mingw32-dlltool").is_file()),
+            #[cfg(not(windows))]
+            x86_64_dlltool: path.iter().any(|dir| dir.join("x86_64-w64-mingw32-dlltool").is_file()),
+        }
+    }
+}
diff --git a/src/tools/compiletest/src/header/tests.rs b/src/tools/compiletest/src/header/tests.rs
index acd588d7fee..9af7bd5e201 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() {
diff --git a/src/tools/compiletest/src/main.rs b/src/tools/compiletest/src/main.rs
index 6a91d25a824..4a2b9de8aee 100644
--- a/src/tools/compiletest/src/main.rs
+++ b/src/tools/compiletest/src/main.rs
@@ -25,6 +25,7 @@ use tracing::*;
 use walkdir::WalkDir;
 
 use self::header::{make_test_description, EarlyProps};
+use crate::header::HeadersCache;
 use std::sync::Arc;
 
 #[cfg(test)]
@@ -558,16 +559,26 @@ pub fn make_tests(
     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.
@@ -631,12 +642,14 @@ fn modified_tests(config: &Config, dir: &Path) -> Result<Vec<PathBuf>, String> {
 
 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() {
@@ -648,7 +661,7 @@ fn collect_tests_from_dir(
             file: dir.to_path_buf(),
             relative_dir: relative_dir_path.parent().unwrap().to_path_buf(),
         };
-        tests.extend(make_test(config, &paths, inputs));
+        tests.extend(make_test(config, cache, &paths, inputs, poisoned));
         return Ok(());
     }
 
@@ -674,19 +687,21 @@ fn collect_tests_from_dir(
             let paths =
                 TestPaths { file: file_path, relative_dir: relative_dir_path.to_path_buf() };
 
-            tests.extend(make_test(config.clone(), &paths, inputs))
+            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 {
@@ -711,8 +726,10 @@ pub fn is_test(file_name: &OsString) -> bool {
 
 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
@@ -729,6 +746,7 @@ fn make_test(
     } else {
         early_props.revisions.iter().map(Some).collect()
     };
+
     revisions
         .into_iter()
         .map(|revision| {
@@ -736,7 +754,9 @@ fn make_test(
                 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);
+            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(
diff --git a/tests/debuginfo/associated-types.rs b/tests/debuginfo/associated-types.rs
index 0a0ce3c671f..a1735520b11 100644
--- a/tests/debuginfo/associated-types.rs
+++ b/tests/debuginfo/associated-types.rs
@@ -1,6 +1,6 @@
 // Some versions of the non-rust-enabled LLDB print the wrong generic
 // parameter type names in this test.
-// rust-lldb
+// needs-rust-lldb
 
 // compile-flags:-g
 
diff --git a/tests/debuginfo/borrowed-enum.rs b/tests/debuginfo/borrowed-enum.rs
index f3e465dc652..37d458cb494 100644
--- a/tests/debuginfo/borrowed-enum.rs
+++ b/tests/debuginfo/borrowed-enum.rs
@@ -1,6 +1,6 @@
 // Require a gdb or lldb that can read DW_TAG_variant_part.
 // min-gdb-version: 8.2
-// rust-lldb
+// needs-rust-lldb
 
 // compile-flags:-g
 
diff --git a/tests/debuginfo/generic-method-on-generic-struct.rs b/tests/debuginfo/generic-method-on-generic-struct.rs
index 97609ef5d93..2d54c2b07df 100644
--- a/tests/debuginfo/generic-method-on-generic-struct.rs
+++ b/tests/debuginfo/generic-method-on-generic-struct.rs
@@ -2,7 +2,7 @@
 
 // Some versions of the non-rust-enabled LLDB print the wrong generic
 // parameter type names in this test.
-// rust-lldb
+// needs-rust-lldb
 
 // === GDB TESTS ===================================================================================
 
diff --git a/tests/debuginfo/generic-struct.rs b/tests/debuginfo/generic-struct.rs
index 5fa5ce80099..5213eebc18b 100644
--- a/tests/debuginfo/generic-struct.rs
+++ b/tests/debuginfo/generic-struct.rs
@@ -1,6 +1,6 @@
 // Some versions of the non-rust-enabled LLDB print the wrong generic
 // parameter type names in this test.
-// rust-lldb
+// needs-rust-lldb
 
 // compile-flags:-g
 
diff --git a/tests/debuginfo/generic-tuple-style-enum.rs b/tests/debuginfo/generic-tuple-style-enum.rs
index 60362e54e7d..a55402691dc 100644
--- a/tests/debuginfo/generic-tuple-style-enum.rs
+++ b/tests/debuginfo/generic-tuple-style-enum.rs
@@ -1,6 +1,6 @@
 // Require a gdb or lldb that can read DW_TAG_variant_part.
 // min-gdb-version: 8.2
-// rust-lldb
+// needs-rust-lldb
 
 // compile-flags:-g
 
diff --git a/tests/debuginfo/method-on-generic-struct.rs b/tests/debuginfo/method-on-generic-struct.rs
index bf047449164..138d8391d40 100644
--- a/tests/debuginfo/method-on-generic-struct.rs
+++ b/tests/debuginfo/method-on-generic-struct.rs
@@ -1,6 +1,6 @@
 // Some versions of the non-rust-enabled LLDB print the wrong generic
 // parameter type names in this test.
-// rust-lldb
+// needs-rust-lldb
 
 // compile-flags:-g
 
diff --git a/tests/debuginfo/struct-style-enum.rs b/tests/debuginfo/struct-style-enum.rs
index 3d819e36898..0152dd9ea9b 100644
--- a/tests/debuginfo/struct-style-enum.rs
+++ b/tests/debuginfo/struct-style-enum.rs
@@ -1,6 +1,6 @@
 // Require a gdb or lldb that can read DW_TAG_variant_part.
 // min-gdb-version: 8.2
-// rust-lldb
+// needs-rust-lldb
 
 // compile-flags:-g
 
diff --git a/tests/debuginfo/tuple-style-enum.rs b/tests/debuginfo/tuple-style-enum.rs
index 39ead172e65..60f3ecbd21e 100644
--- a/tests/debuginfo/tuple-style-enum.rs
+++ b/tests/debuginfo/tuple-style-enum.rs
@@ -1,6 +1,6 @@
 // Require a gdb or lldb that can read DW_TAG_variant_part.
 // min-gdb-version: 8.2
-// rust-lldb
+// needs-rust-lldb
 
 // compile-flags:-g
 
diff --git a/tests/debuginfo/unique-enum.rs b/tests/debuginfo/unique-enum.rs
index d7dfaeefe2b..1ff6f5d9cbe 100644
--- a/tests/debuginfo/unique-enum.rs
+++ b/tests/debuginfo/unique-enum.rs
@@ -1,6 +1,6 @@
 // Require a gdb or lldb that can read DW_TAG_variant_part.
 // min-gdb-version: 8.2
-// rust-lldb
+// needs-rust-lldb
 
 // compile-flags:-g