compiletest/directives/
cfg.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::{Arc, LazyLock};
3
4use crate::common::{CompareMode, Config, Debugger};
5use crate::directives::{DirectiveLine, IgnoreDecision};
6
7const EXTRA_ARCHS: &[&str] = &["spirv"];
8
9const EXTERNAL_IGNORES_LIST: &[&str] = &[
10    // tidy-alphabetical-start
11    "ignore-backends",
12    "ignore-gdb-version",
13    "ignore-llvm-version",
14    "ignore-pass",
15    // tidy-alphabetical-end
16];
17
18/// Directive names that begin with `ignore-`, but are disregarded by this
19/// module because they are handled elsewhere.
20pub(crate) static EXTERNAL_IGNORES_SET: LazyLock<HashSet<&str>> =
21    LazyLock::new(|| EXTERNAL_IGNORES_LIST.iter().copied().collect());
22
23pub(super) fn handle_ignore(
24    conditions: &PreparedConditions,
25    line: &DirectiveLine<'_>,
26) -> IgnoreDecision {
27    let parsed = parse_cfg_name_directive(conditions, line, "ignore-");
28    let line = line.display();
29
30    match parsed.outcome {
31        MatchOutcome::NoMatch => IgnoreDecision::Continue,
32        MatchOutcome::Match => IgnoreDecision::Ignore {
33            reason: match parsed.comment {
34                Some(comment) => format!("ignored {} ({comment})", parsed.pretty_reason.unwrap()),
35                None => format!("ignored {}", parsed.pretty_reason.unwrap()),
36            },
37        },
38        MatchOutcome::Invalid => IgnoreDecision::Error { message: format!("invalid line: {line}") },
39        MatchOutcome::NotHandledHere => IgnoreDecision::Continue,
40    }
41}
42
43pub(super) fn handle_only(
44    conditions: &PreparedConditions,
45    line: &DirectiveLine<'_>,
46) -> IgnoreDecision {
47    let parsed = parse_cfg_name_directive(conditions, line, "only-");
48    let line = line.display();
49
50    match parsed.outcome {
51        MatchOutcome::Match => IgnoreDecision::Continue,
52        MatchOutcome::NoMatch => IgnoreDecision::Ignore {
53            reason: match parsed.comment {
54                Some(comment) => {
55                    format!("only executed {} ({comment})", parsed.pretty_reason.unwrap())
56                }
57                None => format!("only executed {}", parsed.pretty_reason.unwrap()),
58            },
59        },
60        MatchOutcome::Invalid => IgnoreDecision::Error { message: format!("invalid line: {line}") },
61        MatchOutcome::NotHandledHere => IgnoreDecision::Continue,
62    }
63}
64
65/// Parses a name-value directive which contains config-specific information, e.g., `ignore-x86`
66/// or `only-windows`.
67fn parse_cfg_name_directive<'a>(
68    conditions: &PreparedConditions,
69    line: &'a DirectiveLine<'a>,
70    prefix: &str,
71) -> ParsedNameDirective<'a> {
72    let Some(name) = line.name.strip_prefix(prefix) else {
73        return ParsedNameDirective::not_handled_here();
74    };
75
76    if prefix == "ignore-" && EXTERNAL_IGNORES_SET.contains(line.name) {
77        return ParsedNameDirective::not_handled_here();
78    }
79
80    // FIXME(Zalathar): This currently allows either a space or a colon, and
81    // treats any "value" after a colon as though it were a remark.
82    // We should instead forbid the colon syntax for these directives.
83    let comment = line
84        .remark_after_space()
85        .or_else(|| line.value_after_colon())
86        .map(|c| c.trim().trim_start_matches('-').trim());
87
88    if let Some(cond) = conditions.conds.get(name) {
89        ParsedNameDirective {
90            pretty_reason: Some(Arc::clone(&cond.message_when_ignored)),
91            comment,
92            outcome: if cond.value { MatchOutcome::Match } else { MatchOutcome::NoMatch },
93        }
94    } else {
95        ParsedNameDirective { pretty_reason: None, comment, outcome: MatchOutcome::Invalid }
96    }
97}
98
99/// Uses information about the current target (and all targets) to pre-compute
100/// a value (true or false) for a number of "conditions". Those conditions can
101/// then be used by `ignore-*` and `only-*` directives.
102pub(crate) fn prepare_conditions(config: &Config) -> PreparedConditions {
103    let cfgs = config.target_cfgs();
104    let current = &cfgs.current;
105
106    let mut builder = ConditionsBuilder::new();
107
108    // Some condition names overlap (e.g. "macabi" is both an env and an ABI),
109    // so the order in which conditions are added is significant.
110    // Whichever condition registers that name _first_ will take precedence.
111    // (See `ConditionsBuilder::build`.)
112
113    builder.cond("test", true, "always");
114    builder.cond("auxiliary", true, "used by another main test file");
115
116    for target in &cfgs.all_targets {
117        builder.cond(target, *target == config.target, &format!("when the target is {target}"));
118    }
119    for os in &cfgs.all_oses {
120        builder.cond(os, *os == current.os, &format!("when the operating system is {os}"));
121    }
122    for env in &cfgs.all_envs {
123        builder.cond(env, *env == current.env, &format!("when the target environment is {env}"));
124    }
125    for os_and_env in &cfgs.all_oses_and_envs {
126        builder.cond(
127            os_and_env,
128            *os_and_env == current.os_and_env(),
129            &format!("when the operating system and target environment are {os_and_env}"),
130        );
131    }
132    for abi in &cfgs.all_abis {
133        builder.cond(abi, *abi == current.abi, &format!("when the ABI is {abi}"));
134    }
135    for arch in cfgs.all_archs.iter().map(String::as_str).chain(EXTRA_ARCHS.iter().copied()) {
136        builder.cond(arch, *arch == current.arch, &format!("when the architecture is {arch}"));
137    }
138    for n_bit in &cfgs.all_pointer_widths {
139        builder.cond(
140            n_bit,
141            *n_bit == format!("{}bit", current.pointer_width),
142            &format!("when the pointer width is {n_bit}"),
143        );
144    }
145    for family in &cfgs.all_families {
146        builder.cond(
147            family,
148            current.families.contains(family),
149            &format!("when the target family is {family}"),
150        )
151    }
152
153    builder.cond(
154        "thumb",
155        config.target.starts_with("thumb"),
156        "when the architecture is part of the Thumb family",
157    );
158
159    // The "arch" of `i586-` targets is "x86", so for more specific matching
160    // we have to resort to a string-prefix check.
161    builder.cond("i586", config.matches_arch("i586"), "when the subarchitecture is i586");
162    // FIXME(Zalathar): Use proper target vendor information instead?
163    builder.cond("apple", config.target.contains("apple"), "when the target vendor is Apple");
164    // FIXME(Zalathar): Support all known binary formats, not just ELF?
165    builder.cond("elf", current.binary_format == "elf", "when the target binary format is ELF");
166    builder.cond("enzyme", config.has_enzyme, "when rustc is built with LLVM Enzyme");
167    builder.cond("offload", config.has_offload, "when rustc is built with LLVM Offload");
168
169    // Technically the locally built compiler uses the "dev" channel rather than the "nightly"
170    // channel, even though most people don't know or won't care about it. To avoid confusion, we
171    // treat the "dev" channel as the "nightly" channel when processing the directive.
172    for channel in ["stable", "beta", "nightly"] {
173        let curr_channel = match config.channel.as_str() {
174            "dev" => "nightly",
175            ch => ch,
176        };
177        builder.cond(
178            channel,
179            channel == curr_channel,
180            &format!("when the release channel is {channel}"),
181        );
182    }
183
184    builder.cond("cross-compile", config.target != config.host, "when cross-compiling");
185    builder.cond("endian-big", config.is_big_endian(), "on big-endian targets");
186
187    for stage in ["stage0", "stage1", "stage2"] {
188        builder.cond(
189            stage,
190            stage == format!("stage{}", config.stage),
191            &format!("when the bootstrapping stage is {stage}"),
192        );
193    }
194
195    builder.cond("remote", config.remote_test_client.is_some(), "when running tests remotely");
196    builder.cond(
197        "rustc-debug-assertions",
198        config.with_rustc_debug_assertions,
199        "when rustc is built with debug assertions",
200    );
201    builder.cond(
202        "std-debug-assertions",
203        config.with_std_debug_assertions,
204        "when std is built with debug assertions",
205    );
206    builder.cond(
207        "std-remap-debuginfo",
208        config.with_std_remap_debuginfo,
209        "when std is built with remapping of debuginfo",
210    );
211
212    for &debugger in Debugger::STR_VARIANTS {
213        builder.cond(
214            debugger,
215            Some(debugger) == config.debugger.as_ref().map(Debugger::to_str),
216            &format!("when the debugger is {debugger}"),
217        );
218    }
219
220    for &compare_mode in CompareMode::STR_VARIANTS {
221        builder.cond(
222            &format!("compare-mode-{compare_mode}"),
223            Some(compare_mode) == config.compare_mode.as_ref().map(CompareMode::to_str),
224            &format!("when comparing with compare-mode-{compare_mode}"),
225        );
226    }
227
228    // Coverage tests run the same test file in multiple modes.
229    // If a particular test should not be run in one of the modes, ignore it
230    // with "ignore-coverage-map" or "ignore-coverage-run".
231    for test_mode in ["coverage-map", "coverage-run"] {
232        builder.cond(
233            test_mode,
234            test_mode == config.mode.to_str(),
235            &format!("when the test mode is {test_mode}"),
236        );
237    }
238
239    for rustc_abi in &cfgs.all_rustc_abis {
240        builder.cond(
241            &format!("rustc_abi-{rustc_abi}"),
242            Some(rustc_abi) == current.rustc_abi.as_ref(),
243            &format!("when the target `rustc_abi` is rustc_abi-{rustc_abi}"),
244        );
245    }
246
247    // FIXME(Zalathar): Ideally this should be configured by a command-line
248    // flag, not an environment variable.
249    builder.cond(
250        "dist",
251        std::env::var("COMPILETEST_ENABLE_DIST_TESTS").as_deref() == Ok("1"),
252        "when performing tests on dist toolchain",
253    );
254
255    builder.build()
256}
257
258/// The result of parse_cfg_name_directive.
259#[derive(Clone, PartialEq, Debug)]
260pub(super) struct ParsedNameDirective<'a> {
261    pub(super) pretty_reason: Option<Arc<str>>,
262    pub(super) comment: Option<&'a str>,
263    pub(super) outcome: MatchOutcome,
264}
265
266impl ParsedNameDirective<'_> {
267    fn not_handled_here() -> Self {
268        Self { pretty_reason: None, comment: None, outcome: MatchOutcome::NotHandledHere }
269    }
270}
271
272#[derive(Clone, Copy, PartialEq, Debug)]
273pub(super) enum MatchOutcome {
274    /// No match.
275    NoMatch,
276    /// Match.
277    Match,
278    /// The directive was invalid.
279    Invalid,
280    /// The directive should be ignored by this module, because it is handled elsewhere.
281    NotHandledHere,
282}
283
284#[derive(Debug)]
285pub(crate) struct PreparedConditions {
286    /// Maps the "bare" name of each condition to a structure indicating
287    /// whether the condition is true or false for the target being tested.
288    conds: HashMap<Arc<str>, Cond>,
289}
290
291#[derive(Debug)]
292struct Cond {
293    /// Bare condition name without an ignore/only prefix, e.g. `aarch64` or `windows`.
294    bare_name: Arc<str>,
295
296    /// Is this condition true or false for the target being tested, based on
297    /// the config that was used to prepare these conditions?
298    ///
299    /// For example, the condition `windows` is true on Windows targets.
300    value: bool,
301
302    /// Message fragment to show when a test is ignored based on this condition
303    /// being true or false, e.g. "when the architecture is aarch64".
304    message_when_ignored: Arc<str>,
305}
306
307struct ConditionsBuilder {
308    conds: Vec<Cond>,
309}
310
311impl ConditionsBuilder {
312    fn new() -> Self {
313        Self { conds: vec![] }
314    }
315
316    fn cond(&mut self, bare_name: &str, value: bool, message_when_ignored: &str) {
317        self.conds.push(Cond {
318            bare_name: Arc::<str>::from(bare_name),
319            value,
320            message_when_ignored: Arc::<str>::from(message_when_ignored),
321        });
322    }
323
324    fn build(self) -> PreparedConditions {
325        let conds = self
326            .conds
327            .into_iter()
328            // Build the map in reverse order, so that conditions declared
329            // earlier have priority over ones declared later.
330            .rev()
331            .map(|cond| (Arc::clone(&cond.bare_name), cond))
332            .collect::<HashMap<_, _>>();
333        PreparedConditions { conds }
334    }
335}