tidy/
ui_tests.rs

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