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: "auxiliary",
105        message: "used by another main test file"
106    }
107    condition! {
108        name: &config.target,
109        allowed_names: &target_cfgs.all_targets,
110        message: "when the target is {name}"
111    }
112    condition! {
113        name: &[
114            Some(&*target_cfg.os),
115            // If something is ignored for emscripten, it likely also needs to be
116            // ignored for wasm32-unknown-unknown.
117            (config.target == "wasm32-unknown-unknown").then_some("emscripten"),
118        ],
119        allowed_names: &target_cfgs.all_oses,
120        message: "when the operating system is {name}"
121    }
122    condition! {
123        name: &target_cfg.env,
124        allowed_names: &target_cfgs.all_envs,
125        message: "when the target environment is {name}"
126    }
127    condition! {
128        name: &target_cfg.os_and_env(),
129        allowed_names: &target_cfgs.all_oses_and_envs,
130        message: "when the operating system and target environment are {name}"
131    }
132    condition! {
133        name: &target_cfg.abi,
134        allowed_names: &target_cfgs.all_abis,
135        message: "when the ABI is {name}"
136    }
137    condition! {
138        name: &target_cfg.arch,
139        allowed_names: ContainsEither { a: &target_cfgs.all_archs, b: &EXTRA_ARCHS },
140        message: "when the architecture is {name}"
141    }
142    condition! {
143        name: format!("{}bit", target_cfg.pointer_width),
144        allowed_names: &target_cfgs.all_pointer_widths,
145        message: "when the pointer width is {name}"
146    }
147    condition! {
148        name: &*target_cfg.families,
149        allowed_names: &target_cfgs.all_families,
150        message: "when the target family is {name}"
151    }
152
153    // `wasm32-bare` is an alias to refer to just wasm32-unknown-unknown
154    // (in contrast to `wasm32` which also matches non-bare targets)
155    condition! {
156        name: "wasm32-bare",
157        condition: config.target == "wasm32-unknown-unknown",
158        message: "when the target is WASM"
159    }
160
161    condition! {
162        name: "thumb",
163        condition: config.target.starts_with("thumb"),
164        message: "when the architecture is part of the Thumb family"
165    }
166
167    condition! {
168        name: "apple",
169        condition: config.target.contains("apple"),
170        message: "when the target vendor is Apple"
171    }
172
173    condition! {
174        name: "elf",
175        condition: !config.target.contains("windows")
176            && !config.target.contains("wasm")
177            && !config.target.contains("apple")
178            && !config.target.contains("aix")
179            && !config.target.contains("uefi"),
180        message: "when the target binary format is ELF"
181    }
182
183    condition! {
184        name: "enzyme",
185        condition: config.has_enzyme,
186        message: "when rustc is built with LLVM Enzyme"
187    }
188
189    // Technically the locally built compiler uses the "dev" channel rather than the "nightly"
190    // channel, even though most people don't know or won't care about it. To avoid confusion, we
191    // treat the "dev" channel as the "nightly" channel when processing the directive.
192    condition! {
193        name: if config.channel == "dev" { "nightly" } else { &config.channel },
194        allowed_names: &["stable", "beta", "nightly"],
195        message: "when the release channel is {name}",
196    }
197
198    condition! {
199        name: "cross-compile",
200        condition: config.target != config.host,
201        message: "when cross-compiling"
202    }
203    condition! {
204        name: "endian-big",
205        condition: config.is_big_endian(),
206        message: "on big-endian targets",
207    }
208    condition! {
209        name: format!("stage{}", config.stage).as_str(),
210        allowed_names: &["stage0", "stage1", "stage2"],
211        message: "when the bootstrapping stage is {name}",
212    }
213    condition! {
214        name: "remote",
215        condition: config.remote_test_client.is_some(),
216        message: "when running tests remotely",
217    }
218    condition! {
219        name: "rustc-debug-assertions",
220        condition: config.with_rustc_debug_assertions,
221        message: "when rustc is built with debug assertions",
222    }
223    condition! {
224        name: "std-debug-assertions",
225        condition: config.with_std_debug_assertions,
226        message: "when std is built with debug assertions",
227    }
228    condition! {
229        name: config.debugger.as_ref().map(|d| d.to_str()),
230        allowed_names: &Debugger::STR_VARIANTS,
231        message: "when the debugger is {name}",
232    }
233    condition! {
234        name: config.compare_mode
235            .as_ref()
236            .map(|d| format!("compare-mode-{}", d.to_str())),
237        allowed_names: ContainsPrefixed {
238            prefix: "compare-mode-",
239            inner: CompareMode::STR_VARIANTS,
240        },
241        message: "when comparing with {name}",
242    }
243    // Coverage tests run the same test file in multiple modes.
244    // If a particular test should not be run in one of the modes, ignore it
245    // with "ignore-coverage-map" or "ignore-coverage-run".
246    condition! {
247        name: config.mode.to_str(),
248        allowed_names: ["coverage-map", "coverage-run"],
249        message: "when the test mode is {name}",
250    }
251    condition! {
252        name: target_cfg.rustc_abi.as_ref().map(|abi| format!("rustc_abi-{abi}")).unwrap_or_default(),
253        allowed_names: ContainsPrefixed {
254            prefix: "rustc_abi-",
255            inner: target_cfgs.all_rustc_abis.clone(),
256        },
257        message: "when the target `rustc_abi` is {name}",
258    }
259
260    condition! {
261        name: "dist",
262        condition: std::env::var("COMPILETEST_ENABLE_DIST_TESTS") == Ok("1".to_string()),
263        message: "when performing tests on dist toolchain"
264    }
265
266    if prefix == "ignore" && outcome == MatchOutcome::Invalid {
267        // Don't error out for ignore-tidy-* diretives, as those are not handled by compiletest.
268        if name.starts_with("tidy-") {
269            outcome = MatchOutcome::External;
270        }
271
272        // Don't error out for ignore-pass, as that is handled elsewhere.
273        if name == "pass" {
274            outcome = MatchOutcome::External;
275        }
276
277        // Don't error out for ignore-llvm-version, that has a custom syntax and is handled
278        // elsewhere.
279        if name == "llvm-version" {
280            outcome = MatchOutcome::External;
281        }
282
283        // Don't error out for ignore-llvm-version, that has a custom syntax and is handled
284        // elsewhere.
285        if name == "gdb-version" {
286            outcome = MatchOutcome::External;
287        }
288    }
289
290    ParsedNameDirective {
291        name: Some(name),
292        comment: comment.map(|c| c.trim().trim_start_matches('-').trim()),
293        outcome,
294        pretty_reason: message,
295    }
296}
297
298/// The result of parse_cfg_name_directive.
299#[derive(Clone, PartialEq, Debug)]
300pub(super) struct ParsedNameDirective<'a> {
301    pub(super) name: Option<&'a str>,
302    pub(super) pretty_reason: Option<String>,
303    pub(super) comment: Option<&'a str>,
304    pub(super) outcome: MatchOutcome,
305}
306
307impl ParsedNameDirective<'_> {
308    fn not_a_directive() -> Self {
309        Self {
310            name: None,
311            pretty_reason: None,
312            comment: None,
313            outcome: MatchOutcome::NotADirective,
314        }
315    }
316}
317
318#[derive(Clone, Copy, PartialEq, Debug)]
319pub(super) enum MatchOutcome {
320    /// No match.
321    NoMatch,
322    /// Match.
323    Match,
324    /// The directive was invalid.
325    Invalid,
326    /// The directive is handled by other parts of our tooling.
327    External,
328    /// The line is not actually a directive.
329    NotADirective,
330}
331
332trait CustomContains {
333    fn custom_contains(&self, item: &str) -> bool;
334}
335
336impl CustomContains for HashSet<String> {
337    fn custom_contains(&self, item: &str) -> bool {
338        self.contains(item)
339    }
340}
341
342impl CustomContains for &[&str] {
343    fn custom_contains(&self, item: &str) -> bool {
344        self.contains(&item)
345    }
346}
347
348impl<const N: usize> CustomContains for [&str; N] {
349    fn custom_contains(&self, item: &str) -> bool {
350        self.contains(&item)
351    }
352}
353
354struct ContainsPrefixed<T: CustomContains> {
355    prefix: &'static str,
356    inner: T,
357}
358
359impl<T: CustomContains> CustomContains for ContainsPrefixed<T> {
360    fn custom_contains(&self, item: &str) -> bool {
361        match item.strip_prefix(self.prefix) {
362            Some(stripped) => self.inner.custom_contains(stripped),
363            None => false,
364        }
365    }
366}
367
368struct ContainsEither<'a, A: CustomContains, B: CustomContains> {
369    a: &'a A,
370    b: &'a B,
371}
372
373impl<A: CustomContains, B: CustomContains> CustomContains for ContainsEither<'_, A, B> {
374    fn custom_contains(&self, item: &str) -> bool {
375        self.a.custom_contains(item) || self.b.custom_contains(item)
376    }
377}
378
379trait CustomMatches {
380    fn custom_matches(&self, name: &str) -> bool;
381}
382
383impl CustomMatches for &str {
384    fn custom_matches(&self, name: &str) -> bool {
385        name == *self
386    }
387}
388
389impl CustomMatches for String {
390    fn custom_matches(&self, name: &str) -> bool {
391        name == self
392    }
393}
394
395impl<T: CustomMatches> CustomMatches for &[T] {
396    fn custom_matches(&self, name: &str) -> bool {
397        self.iter().any(|m| m.custom_matches(name))
398    }
399}
400
401impl<const N: usize, T: CustomMatches> CustomMatches for [T; N] {
402    fn custom_matches(&self, name: &str) -> bool {
403        self.iter().any(|m| m.custom_matches(name))
404    }
405}
406
407impl<T: CustomMatches> CustomMatches for Option<T> {
408    fn custom_matches(&self, name: &str) -> bool {
409        match self {
410            Some(inner) => inner.custom_matches(name),
411            None => false,
412        }
413    }
414}