Skip to main content

tidy/
target_specific_tests.rs

1//! Tidy check to ensure that all target specific tests (those that require a `--target` flag)
2//! also require the pre-requisite LLVM components to run.
3
4use std::collections::BTreeMap;
5use std::path::Path;
6
7use crate::diagnostics::{CheckId, TidyCtx};
8use crate::iter_header::{HeaderLine, iter_header};
9use crate::walk::filter_not_rust;
10
11const LLVM_COMPONENTS_HEADER: &str = "needs-llvm-components:";
12const COMPILE_FLAGS_HEADER: &str = "compile-flags:";
13
14#[derive(Default, Debug)]
15struct RevisionInfo<'a> {
16    /// Target architecture specified with `--target=`
17    target_arch: Option<Option<&'a str>>,
18    /// LLVM components configured with `//@ needs-llvm-components: ...`
19    llvm_components: Option<Vec<&'a str>>,
20}
21
22pub fn check(tests_path: &Path, tidy_ctx: TidyCtx) {
23    let mut check = tidy_ctx.start_check(CheckId::new("target-specific-tests").path(tests_path));
24
25    crate::walk::walk(tests_path, |path, _is_dir| filter_not_rust(path), &mut |entry, content| {
26        if content.contains("// ignore-tidy-target-specific-tests") {
27            return;
28        }
29
30        let file = entry.path().display();
31        let mut header_map = BTreeMap::new();
32        iter_header(content, &mut |HeaderLine { revision, directive, .. }| {
33            if let Some(value) = directive.strip_prefix(LLVM_COMPONENTS_HEADER) {
34                let info = header_map.entry(revision).or_insert(RevisionInfo::default());
35                let comp_vec = info.llvm_components.get_or_insert(Vec::new());
36                for component in value.split(' ') {
37                    let component = component.trim();
38                    if !component.is_empty() {
39                        comp_vec.push(component);
40                    }
41                }
42            } else if let Some(compile_flags) = directive.strip_prefix(COMPILE_FLAGS_HEADER)
43                && let Some((_, v)) = compile_flags.split_once("--target")
44            {
45                let v = v.trim_start_matches([' ', '=']);
46                let info = header_map.entry(revision).or_insert(RevisionInfo::default());
47                if v.starts_with("{{") {
48                    info.target_arch.replace(None);
49                } else if let Some((arch, _)) = v.split_once("-") {
50                    info.target_arch.replace(Some(arch));
51                } else {
52                    check.error(format!("{file}: seems to have a malformed --target value"));
53                }
54            }
55        });
56
57        // Skip run-make/run-make-cargo tests as revisions are not supported.
58        if entry
59            .path()
60            .strip_prefix(tests_path)
61            .is_ok_and(|rest| rest.starts_with("run-make") || rest.starts_with("run-make-cargo"))
62        {
63            return;
64        }
65
66        // Directives without an explicit revision. These are inherited by all revisions.
67        let global = header_map.remove(&None).unwrap_or_default();
68
69        if header_map.is_empty() {
70            // `//@ revisions` is not used.
71            header_map.insert(None, global);
72        } else {
73            // Make all revisions inherit global directives.
74            for info in header_map.values_mut() {
75                info.target_arch = info.target_arch.or(global.target_arch);
76                if let Some(components) = &global.llvm_components {
77                    info.llvm_components.get_or_insert_with(Vec::new).extend(components);
78                }
79            }
80        }
81
82        for (rev, RevisionInfo { target_arch, llvm_components }) in &header_map {
83            let rev = rev.unwrap_or("[unspecified]");
84            match (target_arch, llvm_components) {
85                (None, None) => {}
86                (Some(target_arch), None) => {
87                    let llvm_component =
88                        target_arch.map_or_else(|| "<arch>".to_string(), arch_to_llvm_component);
89                    check.error(format!(
90                        "{file}: revision {rev} should specify `{LLVM_COMPONENTS_HEADER} {llvm_component}` as it has `--target` set"
91                    ));
92                }
93                (None, Some(_)) => {
94                    check.error(format!(
95                        "{file}: revision {rev} should not specify `{LLVM_COMPONENTS_HEADER}` as it doesn't need `--target`"
96                    ));
97                }
98                (Some(target_arch), Some(llvm_components)) => {
99                    if let Some(target_arch) = target_arch {
100                        let llvm_component = arch_to_llvm_component(target_arch);
101                        if !llvm_components.contains(&llvm_component.as_str()) {
102                            check.error(format!(
103                                "{file}: revision {rev} should specify `{LLVM_COMPONENTS_HEADER} {llvm_component}` as it has `--target` set"
104                            ));
105                        }
106                    }
107                }
108            }
109        }
110    });
111}
112
113fn arch_to_llvm_component(arch: &str) -> String {
114    // NOTE: This is an *approximate* mapping of Rust's `--target` architecture to LLVM component
115    // names. It is not intended to be an authoritative source, but rather a best-effort that's good
116    // enough for the purpose of this tidy check.
117    match arch {
118        "amdgcn" => "amdgpu".into(),
119        "aarch64v8r" | "aarch64_be" | "arm64_32" | "arm64e" | "arm64ec" => "aarch64".into(),
120        "i386" | "i586" | "i686" | "x86" | "x86_64" | "x86_64h" => "x86".into(),
121        "loongarch32" | "loongarch64" => "loongarch".into(),
122        "nvptx64" => "nvptx".into(),
123        "s390x" => "systemz".into(),
124        "sparc64" | "sparcv9" => "sparc".into(),
125        "wasm32" | "wasm32v1" | "wasm64" => "webassembly".into(),
126        _ if arch.starts_with("armeb")
127            || arch.starts_with("armv")
128            || arch.starts_with("thumbv") =>
129        {
130            "arm".into()
131        }
132        _ if arch.starts_with("bpfe") => "bpf".into(),
133        _ if arch.starts_with("mips") => "mips".into(),
134        _ if arch.starts_with("powerpc") => "powerpc".into(),
135        _ if arch.starts_with("riscv") => "riscv".into(),
136        _ => arch.to_ascii_lowercase(),
137    }
138}