Skip to main content

compiletest/directives/
cfg.rs

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