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    condition! {
170        name: "apple",
171        condition: config.target.contains("apple"),
172        message: "when the target vendor is Apple"
173    }
174
175    condition! {
176        name: "elf",
177        condition: !config.target.contains("windows")
178            && !config.target.contains("wasm")
179            && !config.target.contains("apple")
180            && !config.target.contains("aix")
181            && !config.target.contains("uefi"),
182        message: "when the target binary format is ELF"
183    }
184
185    condition! {
186        name: "enzyme",
187        condition: config.has_enzyme,
188        message: "when rustc is built with LLVM Enzyme"
189    }
190
191    // Technically the locally built compiler uses the "dev" channel rather than the "nightly"
192    // channel, even though most people don't know or won't care about it. To avoid confusion, we
193    // treat the "dev" channel as the "nightly" channel when processing the directive.
194    condition! {
195        name: if config.channel == "dev" { "nightly" } else { &config.channel },
196        allowed_names: &["stable", "beta", "nightly"],
197        message: "when the release channel is {name}",
198    }
199
200    condition! {
201        name: "cross-compile",
202        condition: config.target != config.host,
203        message: "when cross-compiling"
204    }
205    condition! {
206        name: "endian-big",
207        condition: config.is_big_endian(),
208        message: "on big-endian targets",
209    }
210    condition! {
211        name: format!("stage{}", config.stage).as_str(),
212        allowed_names: &["stage0", "stage1", "stage2"],
213        message: "when the bootstrapping stage is {name}",
214    }
215    condition! {
216        name: "remote",
217        condition: config.remote_test_client.is_some(),
218        message: "when running tests remotely",
219    }
220    condition! {
221        name: "rustc-debug-assertions",
222        condition: config.with_rustc_debug_assertions,
223        message: "when rustc is built with debug assertions",
224    }
225    condition! {
226        name: "std-debug-assertions",
227        condition: config.with_std_debug_assertions,
228        message: "when std is built with debug assertions",
229    }
230    condition! {
231        name: config.debugger.as_ref().map(|d| d.to_str()),
232        allowed_names: &Debugger::STR_VARIANTS,
233        message: "when the debugger is {name}",
234    }
235    condition! {
236        name: config.compare_mode
237            .as_ref()
238            .map(|d| format!("compare-mode-{}", d.to_str())),
239        allowed_names: ContainsPrefixed {
240            prefix: "compare-mode-",
241            inner: CompareMode::STR_VARIANTS,
242        },
243        message: "when comparing with {name}",
244    }
245    // Coverage tests run the same test file in multiple modes.
246    // If a particular test should not be run in one of the modes, ignore it
247    // with "ignore-coverage-map" or "ignore-coverage-run".
248    condition! {
249        name: config.mode.to_str(),
250        allowed_names: ["coverage-map", "coverage-run"],
251        message: "when the test mode is {name}",
252    }
253    condition! {
254        name: target_cfg.rustc_abi.as_ref().map(|abi| format!("rustc_abi-{abi}")).unwrap_or_default(),
255        allowed_names: ContainsPrefixed {
256            prefix: "rustc_abi-",
257            inner: target_cfgs.all_rustc_abis.clone(),
258        },
259        message: "when the target `rustc_abi` is {name}",
260    }
261
262    condition! {
263        name: "dist",
264        condition: std::env::var("COMPILETEST_ENABLE_DIST_TESTS") == Ok("1".to_string()),
265        message: "when performing tests on dist toolchain"
266    }
267
268    if prefix == "ignore-" && outcome == MatchOutcome::Invalid {
269        // Don't error out for ignore-tidy-* diretives, as those are not handled by compiletest.
270        if name.starts_with("tidy-") {
271            outcome = MatchOutcome::External;
272        }
273
274        // Don't error out for ignore-pass, as that is handled elsewhere.
275        if name == "pass" {
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 == "llvm-version" {
282            outcome = MatchOutcome::External;
283        }
284
285        // Don't error out for ignore-llvm-version, that has a custom syntax and is handled
286        // elsewhere.
287        if name == "gdb-version" {
288            outcome = MatchOutcome::External;
289        }
290
291        // Don't error out for ignore-backends,as it is handled elsewhere.
292        if name == "backends" {
293            outcome = MatchOutcome::External;
294        }
295    }
296
297    ParsedNameDirective {
298        name: Some(name),
299        comment: comment.map(|c| c.trim().trim_start_matches('-').trim()),
300        outcome,
301        pretty_reason: message,
302    }
303}
304
305/// The result of parse_cfg_name_directive.
306#[derive(Clone, PartialEq, Debug)]
307pub(super) struct ParsedNameDirective<'a> {
308    pub(super) name: Option<&'a str>,
309    pub(super) pretty_reason: Option<String>,
310    pub(super) comment: Option<&'a str>,
311    pub(super) outcome: MatchOutcome,
312}
313
314impl ParsedNameDirective<'_> {
315    fn not_a_directive() -> Self {
316        Self {
317            name: None,
318            pretty_reason: None,
319            comment: None,
320            outcome: MatchOutcome::NotADirective,
321        }
322    }
323}
324
325#[derive(Clone, Copy, PartialEq, Debug)]
326pub(super) enum MatchOutcome {
327    /// No match.
328    NoMatch,
329    /// Match.
330    Match,
331    /// The directive was invalid.
332    Invalid,
333    /// The directive is handled by other parts of our tooling.
334    External,
335    /// The line is not actually a directive.
336    NotADirective,
337}
338
339trait CustomContains {
340    fn custom_contains(&self, item: &str) -> bool;
341}
342
343impl CustomContains for HashSet<String> {
344    fn custom_contains(&self, item: &str) -> bool {
345        self.contains(item)
346    }
347}
348
349impl CustomContains for &[&str] {
350    fn custom_contains(&self, item: &str) -> bool {
351        self.contains(&item)
352    }
353}
354
355impl<const N: usize> CustomContains for [&str; N] {
356    fn custom_contains(&self, item: &str) -> bool {
357        self.contains(&item)
358    }
359}
360
361struct ContainsPrefixed<T: CustomContains> {
362    prefix: &'static str,
363    inner: T,
364}
365
366impl<T: CustomContains> CustomContains for ContainsPrefixed<T> {
367    fn custom_contains(&self, item: &str) -> bool {
368        match item.strip_prefix(self.prefix) {
369            Some(stripped) => self.inner.custom_contains(stripped),
370            None => false,
371        }
372    }
373}
374
375struct ContainsEither<'a, A: CustomContains, B: CustomContains> {
376    a: &'a A,
377    b: &'a B,
378}
379
380impl<A: CustomContains, B: CustomContains> CustomContains for ContainsEither<'_, A, B> {
381    fn custom_contains(&self, item: &str) -> bool {
382        self.a.custom_contains(item) || self.b.custom_contains(item)
383    }
384}
385
386trait CustomMatches {
387    fn custom_matches(&self, name: &str) -> bool;
388}
389
390impl CustomMatches for &str {
391    fn custom_matches(&self, name: &str) -> bool {
392        name == *self
393    }
394}
395
396impl CustomMatches for String {
397    fn custom_matches(&self, name: &str) -> bool {
398        name == self
399    }
400}
401
402impl<T: CustomMatches> CustomMatches for &[T] {
403    fn custom_matches(&self, name: &str) -> bool {
404        self.iter().any(|m| m.custom_matches(name))
405    }
406}
407
408impl<const N: usize, T: CustomMatches> CustomMatches for [T; N] {
409    fn custom_matches(&self, name: &str) -> bool {
410        self.iter().any(|m| m.custom_matches(name))
411    }
412}
413
414impl<T: CustomMatches> CustomMatches for Option<T> {
415    fn custom_matches(&self, name: &str) -> bool {
416        match self {
417            Some(inner) => inner.custom_matches(name),
418            None => false,
419        }
420    }
421}