compiletest/header/
cfg.rs

1use std::collections::HashSet;
2
3use crate::common::{CompareMode, Config, Debugger};
4use crate::header::IgnoreDecision;
5
6const EXTRA_ARCHS: &[&str] = &["spirv"];
7
8pub(super) fn handle_ignore(config: &Config, line: &str) -> IgnoreDecision {
9    let parsed = parse_cfg_name_directive(config, line, "ignore");
10    match parsed.outcome {
11        MatchOutcome::NoMatch => IgnoreDecision::Continue,
12        MatchOutcome::Match => IgnoreDecision::Ignore {
13            reason: match parsed.comment {
14                Some(comment) => format!("ignored {} ({comment})", parsed.pretty_reason.unwrap()),
15                None => format!("ignored {}", parsed.pretty_reason.unwrap()),
16            },
17        },
18        MatchOutcome::Invalid => IgnoreDecision::Error { message: format!("invalid line: {line}") },
19        MatchOutcome::External => IgnoreDecision::Continue,
20        MatchOutcome::NotADirective => IgnoreDecision::Continue,
21    }
22}
23
24pub(super) fn handle_only(config: &Config, line: &str) -> IgnoreDecision {
25    let parsed = parse_cfg_name_directive(config, line, "only");
26    match parsed.outcome {
27        MatchOutcome::Match => IgnoreDecision::Continue,
28        MatchOutcome::NoMatch => IgnoreDecision::Ignore {
29            reason: match parsed.comment {
30                Some(comment) => {
31                    format!("only executed {} ({comment})", parsed.pretty_reason.unwrap())
32                }
33                None => format!("only executed {}", parsed.pretty_reason.unwrap()),
34            },
35        },
36        MatchOutcome::Invalid => IgnoreDecision::Error { message: format!("invalid line: {line}") },
37        MatchOutcome::External => IgnoreDecision::Continue,
38        MatchOutcome::NotADirective => IgnoreDecision::Continue,
39    }
40}
41
42/// Parses a name-value directive which contains config-specific information, e.g., `ignore-x86`
43/// or `only-windows`.
44fn parse_cfg_name_directive<'a>(
45    config: &Config,
46    line: &'a str,
47    prefix: &str,
48) -> ParsedNameDirective<'a> {
49    if !line.as_bytes().starts_with(prefix.as_bytes()) {
50        return ParsedNameDirective::not_a_directive();
51    }
52    if line.as_bytes().get(prefix.len()) != Some(&b'-') {
53        return ParsedNameDirective::not_a_directive();
54    }
55    let line = &line[prefix.len() + 1..];
56
57    let (name, comment) =
58        line.split_once(&[':', ' ']).map(|(l, c)| (l, Some(c))).unwrap_or((line, None));
59
60    // Some of the matchers might be "" depending on what the target information is. To avoid
61    // problems we outright reject empty directives.
62    if name.is_empty() {
63        return ParsedNameDirective::not_a_directive();
64    }
65
66    let mut outcome = MatchOutcome::Invalid;
67    let mut message = None;
68
69    macro_rules! condition {
70        (
71            name: $name:expr,
72            $(allowed_names: $allowed_names:expr,)?
73            $(condition: $condition:expr,)?
74            message: $($message:tt)*
75        ) => {{
76            // This is not inlined to avoid problems with macro repetitions.
77            let format_message = || format!($($message)*);
78
79            if outcome != MatchOutcome::Invalid {
80                // Ignore all other matches if we already found one
81            } else if $name.custom_matches(name) {
82                message = Some(format_message());
83                if true $(&& $condition)? {
84                    outcome = MatchOutcome::Match;
85                } else {
86                    outcome = MatchOutcome::NoMatch;
87                }
88            }
89            $(else if $allowed_names.custom_contains(name) {
90                message = Some(format_message());
91                outcome = MatchOutcome::NoMatch;
92            })?
93        }};
94    }
95
96    let target_cfgs = config.target_cfgs();
97    let target_cfg = config.target_cfg();
98
99    condition! {
100        name: "test",
101        message: "always"
102    }
103    condition! {
104        name: &config.target,
105        allowed_names: &target_cfgs.all_targets,
106        message: "when the target is {name}"
107    }
108    condition! {
109        name: &[
110            Some(&*target_cfg.os),
111            // If something is ignored for emscripten, it likely also needs to be
112            // ignored for wasm32-unknown-unknown.
113            (config.target == "wasm32-unknown-unknown").then_some("emscripten"),
114        ],
115        allowed_names: &target_cfgs.all_oses,
116        message: "when the operating system is {name}"
117    }
118    condition! {
119        name: &target_cfg.env,
120        allowed_names: &target_cfgs.all_envs,
121        message: "when the target environment is {name}"
122    }
123    condition! {
124        name: &target_cfg.os_and_env(),
125        allowed_names: &target_cfgs.all_oses_and_envs,
126        message: "when the operating system and target environment are {name}"
127    }
128    condition! {
129        name: &target_cfg.abi,
130        allowed_names: &target_cfgs.all_abis,
131        message: "when the ABI is {name}"
132    }
133    condition! {
134        name: &target_cfg.arch,
135        allowed_names: ContainsEither { a: &target_cfgs.all_archs, b: &EXTRA_ARCHS },
136        message: "when the architecture is {name}"
137    }
138    condition! {
139        name: format!("{}bit", target_cfg.pointer_width),
140        allowed_names: &target_cfgs.all_pointer_widths,
141        message: "when the pointer width is {name}"
142    }
143    condition! {
144        name: &*target_cfg.families,
145        allowed_names: &target_cfgs.all_families,
146        message: "when the target family is {name}"
147    }
148
149    // `wasm32-bare` is an alias to refer to just wasm32-unknown-unknown
150    // (in contrast to `wasm32` which also matches non-bare targets)
151    condition! {
152        name: "wasm32-bare",
153        condition: config.target == "wasm32-unknown-unknown",
154        message: "when the target is WASM"
155    }
156
157    condition! {
158        name: "thumb",
159        condition: config.target.starts_with("thumb"),
160        message: "when the architecture is part of the Thumb family"
161    }
162
163    condition! {
164        name: "apple",
165        condition: config.target.contains("apple"),
166        message: "when the target vendor is Apple"
167    }
168
169    condition! {
170        name: "elf",
171        condition: !config.target.contains("windows")
172            && !config.target.contains("wasm")
173            && !config.target.contains("apple")
174            && !config.target.contains("aix")
175            && !config.target.contains("uefi"),
176        message: "when the target binary format is ELF"
177    }
178
179    condition! {
180        name: "enzyme",
181        condition: config.has_enzyme,
182        message: "when rustc is built with LLVM Enzyme"
183    }
184
185    // Technically the locally built compiler uses the "dev" channel rather than the "nightly"
186    // channel, even though most people don't know or won't care about it. To avoid confusion, we
187    // treat the "dev" channel as the "nightly" channel when processing the directive.
188    condition! {
189        name: if config.channel == "dev" { "nightly" } else { &config.channel },
190        allowed_names: &["stable", "beta", "nightly"],
191        message: "when the release channel is {name}",
192    }
193
194    condition! {
195        name: "cross-compile",
196        condition: config.target != config.host,
197        message: "when cross-compiling"
198    }
199    condition! {
200        name: "endian-big",
201        condition: config.is_big_endian(),
202        message: "on big-endian targets",
203    }
204    condition! {
205        name: format!("stage{}", config.stage).as_str(),
206        allowed_names: &["stage0", "stage1", "stage2"],
207        message: "when the bootstrapping stage is {name}",
208    }
209    condition! {
210        name: "remote",
211        condition: config.remote_test_client.is_some(),
212        message: "when running tests remotely",
213    }
214    condition! {
215        name: "rustc-debug-assertions",
216        condition: config.with_rustc_debug_assertions,
217        message: "when rustc is built with debug assertions",
218    }
219    condition! {
220        name: "std-debug-assertions",
221        condition: config.with_std_debug_assertions,
222        message: "when std is built with debug assertions",
223    }
224    condition! {
225        name: config.debugger.as_ref().map(|d| d.to_str()),
226        allowed_names: &Debugger::STR_VARIANTS,
227        message: "when the debugger is {name}",
228    }
229    condition! {
230        name: config.compare_mode
231            .as_ref()
232            .map(|d| format!("compare-mode-{}", d.to_str())),
233        allowed_names: ContainsPrefixed {
234            prefix: "compare-mode-",
235            inner: CompareMode::STR_VARIANTS,
236        },
237        message: "when comparing with {name}",
238    }
239    // Coverage tests run the same test file in multiple modes.
240    // If a particular test should not be run in one of the modes, ignore it
241    // with "ignore-coverage-map" or "ignore-coverage-run".
242    condition! {
243        name: config.mode.to_str(),
244        allowed_names: ["coverage-map", "coverage-run"],
245        message: "when the test mode is {name}",
246    }
247    condition! {
248        name: target_cfg.rustc_abi.as_ref().map(|abi| format!("rustc_abi-{abi}")).unwrap_or_default(),
249        allowed_names: ContainsPrefixed {
250            prefix: "rustc_abi-",
251            inner: target_cfgs.all_rustc_abis.clone(),
252        },
253        message: "when the target `rustc_abi` is {name}",
254    }
255
256    condition! {
257        name: "dist",
258        condition: std::env::var("COMPILETEST_ENABLE_DIST_TESTS") == Ok("1".to_string()),
259        message: "when performing tests on dist toolchain"
260    }
261
262    if prefix == "ignore" && outcome == MatchOutcome::Invalid {
263        // Don't error out for ignore-tidy-* diretives, as those are not handled by compiletest.
264        if name.starts_with("tidy-") {
265            outcome = MatchOutcome::External;
266        }
267
268        // Don't error out for ignore-pass, as that is handled elsewhere.
269        if name == "pass" {
270            outcome = MatchOutcome::External;
271        }
272
273        // Don't error out for ignore-llvm-version, that has a custom syntax and is handled
274        // elsewhere.
275        if name == "llvm-version" {
276            outcome = MatchOutcome::External;
277        }
278
279        // Don't error out for ignore-llvm-version, that has a custom syntax and is handled
280        // elsewhere.
281        if name == "gdb-version" {
282            outcome = MatchOutcome::External;
283        }
284    }
285
286    ParsedNameDirective {
287        name: Some(name),
288        comment: comment.map(|c| c.trim().trim_start_matches('-').trim()),
289        outcome,
290        pretty_reason: message,
291    }
292}
293
294/// The result of parse_cfg_name_directive.
295#[derive(Clone, PartialEq, Debug)]
296pub(super) struct ParsedNameDirective<'a> {
297    pub(super) name: Option<&'a str>,
298    pub(super) pretty_reason: Option<String>,
299    pub(super) comment: Option<&'a str>,
300    pub(super) outcome: MatchOutcome,
301}
302
303impl ParsedNameDirective<'_> {
304    fn not_a_directive() -> Self {
305        Self {
306            name: None,
307            pretty_reason: None,
308            comment: None,
309            outcome: MatchOutcome::NotADirective,
310        }
311    }
312}
313
314#[derive(Clone, Copy, PartialEq, Debug)]
315pub(super) enum MatchOutcome {
316    /// No match.
317    NoMatch,
318    /// Match.
319    Match,
320    /// The directive was invalid.
321    Invalid,
322    /// The directive is handled by other parts of our tooling.
323    External,
324    /// The line is not actually a directive.
325    NotADirective,
326}
327
328trait CustomContains {
329    fn custom_contains(&self, item: &str) -> bool;
330}
331
332impl CustomContains for HashSet<String> {
333    fn custom_contains(&self, item: &str) -> bool {
334        self.contains(item)
335    }
336}
337
338impl CustomContains for &[&str] {
339    fn custom_contains(&self, item: &str) -> bool {
340        self.contains(&item)
341    }
342}
343
344impl<const N: usize> CustomContains for [&str; N] {
345    fn custom_contains(&self, item: &str) -> bool {
346        self.contains(&item)
347    }
348}
349
350struct ContainsPrefixed<T: CustomContains> {
351    prefix: &'static str,
352    inner: T,
353}
354
355impl<T: CustomContains> CustomContains for ContainsPrefixed<T> {
356    fn custom_contains(&self, item: &str) -> bool {
357        match item.strip_prefix(self.prefix) {
358            Some(stripped) => self.inner.custom_contains(stripped),
359            None => false,
360        }
361    }
362}
363
364struct ContainsEither<'a, A: CustomContains, B: CustomContains> {
365    a: &'a A,
366    b: &'a B,
367}
368
369impl<A: CustomContains, B: CustomContains> CustomContains for ContainsEither<'_, A, B> {
370    fn custom_contains(&self, item: &str) -> bool {
371        self.a.custom_contains(item) || self.b.custom_contains(item)
372    }
373}
374
375trait CustomMatches {
376    fn custom_matches(&self, name: &str) -> bool;
377}
378
379impl CustomMatches for &str {
380    fn custom_matches(&self, name: &str) -> bool {
381        name == *self
382    }
383}
384
385impl CustomMatches for String {
386    fn custom_matches(&self, name: &str) -> bool {
387        name == self
388    }
389}
390
391impl<T: CustomMatches> CustomMatches for &[T] {
392    fn custom_matches(&self, name: &str) -> bool {
393        self.iter().any(|m| m.custom_matches(name))
394    }
395}
396
397impl<const N: usize, T: CustomMatches> CustomMatches for [T; N] {
398    fn custom_matches(&self, name: &str) -> bool {
399        self.iter().any(|m| m.custom_matches(name))
400    }
401}
402
403impl<T: CustomMatches> CustomMatches for Option<T> {
404    fn custom_matches(&self, name: &str) -> bool {
405        match self {
406            Some(inner) => inner.custom_matches(name),
407            None => false,
408        }
409    }
410}