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 = 1634;
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 dir in Walk::new(&tests_path.join("ui")) {
61        if let Ok(entry) = dir {
62            let parent = entry.path().parent().unwrap().to_path_buf();
63            *directories.entry(parent).or_default() += 1;
64        }
65    }
66
67    let (mut max, mut max_issues) = (0, 0);
68    for (dir_path, count) in directories {
69        let is_issues_dir = tests_path.join("ui/issues") == dir_path;
70        let (limit, maxcnt) = if is_issues_dir {
71            (ISSUES_ENTRY_LIMIT, &mut max_issues)
72        } else {
73            (ENTRY_LIMIT, &mut max)
74        };
75        *maxcnt = (*maxcnt).max(count);
76        if count > limit {
77            tidy_error!(
78                bad,
79                "following path contains more than {} entries, \
80                    you should move the test to some relevant subdirectory (current: {}): {}",
81                limit,
82                count,
83                dir_path.display()
84            );
85        }
86    }
87    if ISSUES_ENTRY_LIMIT > max_issues {
88        tidy_error!(
89            bad,
90            "`ISSUES_ENTRY_LIMIT` is too high (is {ISSUES_ENTRY_LIMIT}, should be {max_issues})"
91        );
92    }
93}
94
95pub fn check(root_path: &Path, bless: bool, bad: &mut bool) {
96    let issues_txt_header = r#"============================================================
97    ⚠️⚠️⚠️NOTHING SHOULD EVER BE ADDED TO THIS LIST⚠️⚠️⚠️
98============================================================
99"#;
100
101    let path = &root_path.join("tests");
102    check_entries(&path, bad);
103
104    // the list of files in ui tests that are allowed to start with `issue-XXXX`
105    // BTreeSet because we would like a stable ordering so --bless works
106    let mut prev_line = "";
107    let mut is_sorted = true;
108    let allowed_issue_names: BTreeSet<_> = include_str!("issues.txt")
109        .strip_prefix(issues_txt_header)
110        .unwrap()
111        .lines()
112        .map(|line| {
113            if prev_line > line {
114                is_sorted = false;
115            }
116
117            prev_line = line;
118            line
119        })
120        .collect();
121
122    if !is_sorted && !bless {
123        tidy_error!(
124            bad,
125            "`src/tools/tidy/src/issues.txt` is not in order, mostly because you modified it manually,
126            please only update it with command `x test tidy --bless`"
127        );
128    }
129
130    let mut remaining_issue_names: BTreeSet<&str> = allowed_issue_names.clone();
131
132    let (ui, ui_fulldeps) = (path.join("ui"), path.join("ui-fulldeps"));
133    let paths = [ui.as_path(), ui_fulldeps.as_path()];
134    crate::walk::walk_no_read(&paths, |_, _| false, &mut |entry| {
135        let file_path = entry.path();
136        if let Some(ext) = file_path.extension().and_then(OsStr::to_str) {
137            // files that are neither an expected extension or an exception should not exist
138            // they're probably typos or not meant to exist
139            if !(EXPECTED_TEST_FILE_EXTENSIONS.contains(&ext)
140                || EXTENSION_EXCEPTION_PATHS.iter().any(|path| file_path.ends_with(path)))
141            {
142                tidy_error!(bad, "file {} has unexpected extension {}", file_path.display(), ext);
143            }
144
145            // NB: We do not use file_stem() as some file names have multiple `.`s and we
146            // must strip all of them.
147            let testname =
148                file_path.file_name().unwrap().to_str().unwrap().split_once('.').unwrap().0;
149            if ext == "stderr" || ext == "stdout" || ext == "fixed" {
150                // Test output filenames have one of the formats:
151                // ```
152                // $testname.stderr
153                // $testname.$mode.stderr
154                // $testname.$revision.stderr
155                // $testname.$revision.$mode.stderr
156                // ```
157                //
158                // For now, just make sure that there is a corresponding
159                // `$testname.rs` file.
160
161                if !file_path.with_file_name(testname).with_extension("rs").exists()
162                    && !testname.contains("ignore-tidy")
163                {
164                    tidy_error!(bad, "Stray file with UI testing output: {:?}", file_path);
165                }
166
167                if let Ok(metadata) = fs::metadata(file_path) {
168                    if metadata.len() == 0 {
169                        tidy_error!(bad, "Empty file with UI testing output: {:?}", file_path);
170                    }
171                }
172            }
173
174            if ext == "rs" {
175                if let Some(test_name) = static_regex!(r"^issues?[-_]?(\d{3,})").captures(testname)
176                {
177                    // these paths are always relative to the passed `path` and always UTF8
178                    let stripped_path = file_path
179                        .strip_prefix(path)
180                        .unwrap()
181                        .to_str()
182                        .unwrap()
183                        .replace(std::path::MAIN_SEPARATOR_STR, "/");
184
185                    if !remaining_issue_names.remove(stripped_path.as_str()) {
186                        tidy_error!(
187                            bad,
188                            "file `tests/{stripped_path}` must begin with a descriptive name, consider `{{reason}}-issue-{issue_n}.rs`",
189                            issue_n = &test_name[1],
190                        );
191                    }
192                }
193            }
194        }
195    });
196
197    // if there are any file names remaining, they were moved on the fs.
198    // our data must remain up to date, so it must be removed from issues.txt
199    // do this automatically on bless, otherwise issue a tidy error
200    if bless && (!remaining_issue_names.is_empty() || !is_sorted) {
201        let tidy_src = root_path.join("src/tools/tidy/src");
202        // instead of overwriting the file, recreate it and use an "atomic rename"
203        // so we don't bork things on panic or a contributor using Ctrl+C
204        let blessed_issues_path = tidy_src.join("issues_blessed.txt");
205        let mut blessed_issues_txt = fs::File::create(&blessed_issues_path).unwrap();
206        blessed_issues_txt.write(issues_txt_header.as_bytes()).unwrap();
207        // If we changed paths to use the OS separator, reassert Unix chauvinism for blessing.
208        for filename in allowed_issue_names.difference(&remaining_issue_names) {
209            writeln!(blessed_issues_txt, "{filename}").unwrap();
210        }
211        let old_issues_path = tidy_src.join("issues.txt");
212        fs::rename(blessed_issues_path, old_issues_path).unwrap();
213    } else {
214        for file_name in remaining_issue_names {
215            let mut p = PathBuf::from(path);
216            p.push(file_name);
217            tidy_error!(
218                bad,
219                "file `{}` no longer exists and should be removed from the exclusions in `src/tools/tidy/src/issues.txt`",
220                p.display()
221            );
222        }
223    }
224}