tidy/
ui_tests.rs

1//! Tidy check to ensure below in UI test directories:
2//! - the number of entries in each directory must be less than `ENTRY_LIMIT`
3//! - there are no stray `.stderr` files
4
5use std::collections::{BTreeSet, HashMap};
6use std::ffi::OsStr;
7use std::fs;
8use std::io::Write;
9use std::path::{Path, PathBuf};
10
11use ignore::Walk;
12
13// FIXME: GitHub's UI truncates file lists that exceed 1000 entries, so these
14// should all be 1000 or lower. Limits significantly smaller than 1000 are also
15// desirable, because large numbers of files are unwieldy in general. See issue
16// #73494.
17const ENTRY_LIMIT: u32 = 901;
18// FIXME: The following limits should be reduced eventually.
19
20const ISSUES_ENTRY_LIMIT: u32 = 1616;
21
22const EXPECTED_TEST_FILE_EXTENSIONS: &[&str] = &[
23    "rs",     // test source files
24    "stderr", // expected stderr file, corresponds to a rs file
25    "svg",    // expected svg file, corresponds to a rs file, equivalent to stderr
26    "stdout", // expected stdout file, corresponds to a rs file
27    "fixed",  // expected source file after applying fixes
28    "md",     // test directory descriptions
29    "ftl",    // translation tests
30];
31
32const EXTENSION_EXCEPTION_PATHS: &[&str] = &[
33    "tests/ui/asm/named-asm-labels.s", // loading an external asm file to test named labels lint
34    "tests/ui/codegen/mismatched-data-layout.json", // testing mismatched data layout w/ custom targets
35    "tests/ui/check-cfg/my-awesome-platform.json",  // testing custom targets with cfgs
36    "tests/ui/argfile/commandline-argfile-badutf8.args", // passing args via a file
37    "tests/ui/argfile/commandline-argfile.args",    // passing args via a file
38    "tests/ui/crate-loading/auxiliary/libfoo.rlib", // testing loading a manually created rlib
39    "tests/ui/include-macros/data.bin", // testing including data with the include macros
40    "tests/ui/include-macros/file.txt", // testing including data with the include macros
41    "tests/ui/macros/macro-expanded-include/file.txt", // testing including data with the include macros
42    "tests/ui/macros/not-utf8.bin", // testing including data with the include macros
43    "tests/ui/macros/syntax-extension-source-utils-files/includeme.fragment", // more include
44    "tests/ui/proc-macro/auxiliary/included-file.txt", // more include
45    "tests/ui/unpretty/auxiliary/data.txt", // more include
46    "tests/ui/invalid/foo.natvis.xml", // sample debugger visualizer
47    "tests/ui/sanitizer/dataflow-abilist.txt", // dataflow sanitizer ABI list file
48    "tests/ui/shell-argfiles/shell-argfiles.args", // passing args via a file
49    "tests/ui/shell-argfiles/shell-argfiles-badquotes.args", // passing args via a file
50    "tests/ui/shell-argfiles/shell-argfiles-via-argfile-shell.args", // passing args via a file
51    "tests/ui/shell-argfiles/shell-argfiles-via-argfile.args", // passing args via a file
52    "tests/ui/std/windows-bat-args1.bat", // tests escaping arguments through batch files
53    "tests/ui/std/windows-bat-args2.bat", // tests escaping arguments through batch files
54    "tests/ui/std/windows-bat-args3.bat", // tests escaping arguments through batch files
55];
56
57fn check_entries(tests_path: &Path, bad: &mut bool) {
58    let mut directories: HashMap<PathBuf, u32> = HashMap::new();
59
60    for entry in Walk::new(tests_path.join("ui")).flatten() {
61        let parent = entry.path().parent().unwrap().to_path_buf();
62        *directories.entry(parent).or_default() += 1;
63    }
64
65    let (mut max, mut max_issues) = (0, 0);
66    for (dir_path, count) in directories {
67        let is_issues_dir = tests_path.join("ui/issues") == dir_path;
68        let (limit, maxcnt) = if is_issues_dir {
69            (ISSUES_ENTRY_LIMIT, &mut max_issues)
70        } else {
71            (ENTRY_LIMIT, &mut max)
72        };
73        *maxcnt = (*maxcnt).max(count);
74        if count > limit {
75            tidy_error!(
76                bad,
77                "following path contains more than {} entries, \
78                    you should move the test to some relevant subdirectory (current: {}): {}",
79                limit,
80                count,
81                dir_path.display()
82            );
83        }
84    }
85    if ISSUES_ENTRY_LIMIT > max_issues {
86        tidy_error!(
87            bad,
88            "`ISSUES_ENTRY_LIMIT` is too high (is {ISSUES_ENTRY_LIMIT}, should be {max_issues})"
89        );
90    }
91}
92
93pub fn check(root_path: &Path, bless: bool, bad: &mut bool) {
94    let issues_txt_header = r#"============================================================
95    ⚠️⚠️⚠️NOTHING SHOULD EVER BE ADDED TO THIS LIST⚠️⚠️⚠️
96============================================================
97"#;
98
99    let path = &root_path.join("tests");
100    check_entries(path, bad);
101
102    // the list of files in ui tests that are allowed to start with `issue-XXXX`
103    // BTreeSet because we would like a stable ordering so --bless works
104    let mut prev_line = "";
105    let mut is_sorted = true;
106    let allowed_issue_names: BTreeSet<_> = include_str!("issues.txt")
107        .strip_prefix(issues_txt_header)
108        .unwrap()
109        .lines()
110        .inspect(|&line| {
111            if prev_line > line {
112                is_sorted = false;
113            }
114
115            prev_line = line;
116        })
117        .collect();
118
119    if !is_sorted && !bless {
120        tidy_error!(
121            bad,
122            "`src/tools/tidy/src/issues.txt` is not in order, mostly because you modified it manually,
123            please only update it with command `x test tidy --bless`"
124        );
125    }
126
127    let mut remaining_issue_names: BTreeSet<&str> = allowed_issue_names.clone();
128
129    let (ui, ui_fulldeps) = (path.join("ui"), path.join("ui-fulldeps"));
130    let paths = [ui.as_path(), ui_fulldeps.as_path()];
131    crate::walk::walk_no_read(&paths, |_, _| false, &mut |entry| {
132        let file_path = entry.path();
133        if let Some(ext) = file_path.extension().and_then(OsStr::to_str) {
134            // files that are neither an expected extension or an exception should not exist
135            // they're probably typos or not meant to exist
136            if !(EXPECTED_TEST_FILE_EXTENSIONS.contains(&ext)
137                || EXTENSION_EXCEPTION_PATHS.iter().any(|path| file_path.ends_with(path)))
138            {
139                tidy_error!(bad, "file {} has unexpected extension {}", file_path.display(), ext);
140            }
141
142            // NB: We do not use file_stem() as some file names have multiple `.`s and we
143            // must strip all of them.
144            let testname =
145                file_path.file_name().unwrap().to_str().unwrap().split_once('.').unwrap().0;
146            if ext == "stderr" || ext == "stdout" || ext == "fixed" {
147                // Test output filenames have one of the formats:
148                // ```
149                // $testname.stderr
150                // $testname.$mode.stderr
151                // $testname.$revision.stderr
152                // $testname.$revision.$mode.stderr
153                // ```
154                //
155                // For now, just make sure that there is a corresponding
156                // `$testname.rs` file.
157
158                if !file_path.with_file_name(testname).with_extension("rs").exists()
159                    && !testname.contains("ignore-tidy")
160                {
161                    tidy_error!(bad, "Stray file with UI testing output: {:?}", file_path);
162                }
163
164                if let Ok(metadata) = fs::metadata(file_path) {
165                    if metadata.len() == 0 {
166                        tidy_error!(bad, "Empty file with UI testing output: {:?}", file_path);
167                    }
168                }
169            }
170
171            if ext == "rs" {
172                if let Some(test_name) = static_regex!(r"^issues?[-_]?(\d{3,})").captures(testname)
173                {
174                    // these paths are always relative to the passed `path` and always UTF8
175                    let stripped_path = file_path
176                        .strip_prefix(path)
177                        .unwrap()
178                        .to_str()
179                        .unwrap()
180                        .replace(std::path::MAIN_SEPARATOR_STR, "/");
181
182                    if !remaining_issue_names.remove(stripped_path.as_str()) {
183                        tidy_error!(
184                            bad,
185                            "file `tests/{stripped_path}` must begin with a descriptive name, consider `{{reason}}-issue-{issue_n}.rs`",
186                            issue_n = &test_name[1],
187                        );
188                    }
189                }
190            }
191        }
192    });
193
194    // if there are any file names remaining, they were moved on the fs.
195    // our data must remain up to date, so it must be removed from issues.txt
196    // do this automatically on bless, otherwise issue a tidy error
197    if bless && (!remaining_issue_names.is_empty() || !is_sorted) {
198        let tidy_src = root_path.join("src/tools/tidy/src");
199        // instead of overwriting the file, recreate it and use an "atomic rename"
200        // so we don't bork things on panic or a contributor using Ctrl+C
201        let blessed_issues_path = tidy_src.join("issues_blessed.txt");
202        let mut blessed_issues_txt = fs::File::create(&blessed_issues_path).unwrap();
203        blessed_issues_txt.write_all(issues_txt_header.as_bytes()).unwrap();
204        // If we changed paths to use the OS separator, reassert Unix chauvinism for blessing.
205        for filename in allowed_issue_names.difference(&remaining_issue_names) {
206            writeln!(blessed_issues_txt, "{filename}").unwrap();
207        }
208        let old_issues_path = tidy_src.join("issues.txt");
209        fs::rename(blessed_issues_path, old_issues_path).unwrap();
210    } else {
211        for file_name in remaining_issue_names {
212            let mut p = PathBuf::from(path);
213            p.push(file_name);
214            tidy_error!(
215                bad,
216                "file `{}` no longer exists and should be removed from the exclusions in `src/tools/tidy/src/issues.txt`",
217                p.display()
218            );
219        }
220    }
221}