compiletest/directives/
cfg.rs

1use std::collections::HashSet;
2
3use crate::common::{CompareMode, Config, Debugger};
4use crate::directives::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        // Don't error out for ignore-backends,as it is handled elsewhere.
290        if name == "backends" {
291            outcome = MatchOutcome::External;
292        }
293    }
294
295    ParsedNameDirective {
296        name: Some(name),
297        comment: comment.map(|c| c.trim().trim_start_matches('-').trim()),
298        outcome,
299        pretty_reason: message,
300    }
301}
302
303/// The result of parse_cfg_name_directive.
304#[derive(Clone, PartialEq, Debug)]
305pub(super) struct ParsedNameDirective<'a> {
306    pub(super) name: Option<&'a str>,
307    pub(super) pretty_reason: Option<String>,
308    pub(super) comment: Option<&'a str>,
309    pub(super) outcome: MatchOutcome,
310}
311
312impl ParsedNameDirective<'_> {
313    fn not_a_directive() -> Self {
314        Self {
315            name: None,
316            pretty_reason: None,
317            comment: None,
318            outcome: MatchOutcome::NotADirective,
319        }
320    }
321}
322
323#[derive(Clone, Copy, PartialEq, Debug)]
324pub(super) enum MatchOutcome {
325    /// No match.
326    NoMatch,
327    /// Match.
328    Match,
329    /// The directive was invalid.
330    Invalid,
331    /// The directive is handled by other parts of our tooling.
332    External,
333    /// The line is not actually a directive.
334    NotADirective,
335}
336
337trait CustomContains {
338    fn custom_contains(&self, item: &str) -> bool;
339}
340
341impl CustomContains for HashSet<String> {
342    fn custom_contains(&self, item: &str) -> bool {
343        self.contains(item)
344    }
345}
346
347impl CustomContains for &[&str] {
348    fn custom_contains(&self, item: &str) -> bool {
349        self.contains(&item)
350    }
351}
352
353impl<const N: usize> CustomContains for [&str; N] {
354    fn custom_contains(&self, item: &str) -> bool {
355        self.contains(&item)
356    }
357}
358
359struct ContainsPrefixed<T: CustomContains> {
360    prefix: &'static str,
361    inner: T,
362}
363
364impl<T: CustomContains> CustomContains for ContainsPrefixed<T> {
365    fn custom_contains(&self, item: &str) -> bool {
366        match item.strip_prefix(self.prefix) {
367            Some(stripped) => self.inner.custom_contains(stripped),
368            None => false,
369        }
370    }
371}
372
373struct ContainsEither<'a, A: CustomContains, B: CustomContains> {
374    a: &'a A,
375    b: &'a B,
376}
377
378impl<A: CustomContains, B: CustomContains> CustomContains for ContainsEither<'_, A, B> {
379    fn custom_contains(&self, item: &str) -> bool {
380        self.a.custom_contains(item) || self.b.custom_contains(item)
381    }
382}
383
384trait CustomMatches {
385    fn custom_matches(&self, name: &str) -> bool;
386}
387
388impl CustomMatches for &str {
389    fn custom_matches(&self, name: &str) -> bool {
390        name == *self
391    }
392}
393
394impl CustomMatches for String {
395    fn custom_matches(&self, name: &str) -> bool {
396        name == self
397    }
398}
399
400impl<T: CustomMatches> CustomMatches for &[T] {
401    fn custom_matches(&self, name: &str) -> bool {
402        self.iter().any(|m| m.custom_matches(name))
403    }
404}
405
406impl<const N: usize, T: CustomMatches> CustomMatches for [T; N] {
407    fn custom_matches(&self, name: &str) -> bool {
408        self.iter().any(|m| m.custom_matches(name))
409    }
410}
411
412impl<T: CustomMatches> CustomMatches for Option<T> {
413    fn custom_matches(&self, name: &str) -> bool {
414        match self {
415            Some(inner) => inner.custom_matches(name),
416            None => false,
417        }
418    }
419}