compiletest/directives/
cfg.rs

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