1use std::collections::BTreeSet;
5use std::ffi::OsStr;
6use std::fs;
7use std::io::Write;
8use std::path::{Path, PathBuf};
9
10use crate::diagnostics::{CheckId, DiagCtx, RunningCheck};
11
12const ISSUES_TXT_HEADER: &str = r#"============================================================
13 ⚠️⚠️⚠️NOTHING SHOULD EVER BE ADDED TO THIS LIST⚠️⚠️⚠️
14============================================================
15"#;
16
17pub fn check(root_path: &Path, bless: bool, diag_ctx: DiagCtx) {
18 let path = &root_path.join("tests");
19 let mut check = diag_ctx.start_check(CheckId::new("ui_tests").path(path));
20
21 let mut prev_line = "";
24 let mut is_sorted = true;
25 let allowed_issue_names: BTreeSet<_> = include_str!("issues.txt")
26 .strip_prefix(ISSUES_TXT_HEADER)
27 .unwrap()
28 .lines()
29 .inspect(|&line| {
30 if prev_line > line {
31 is_sorted = false;
32 }
33
34 prev_line = line;
35 })
36 .collect();
37
38 if !is_sorted && !bless {
39 check.error(
40 "`src/tools/tidy/src/issues.txt` is not in order, mostly because you modified it manually,
41 please only update it with command `x test tidy --bless`"
42 );
43 }
44
45 deny_new_top_level_ui_tests(&mut check, &path.join("ui"));
46
47 let remaining_issue_names = recursively_check_ui_tests(&mut check, path, &allowed_issue_names);
48
49 if bless && (!remaining_issue_names.is_empty() || !is_sorted) {
53 let tidy_src = root_path.join("src/tools/tidy/src");
54 let blessed_issues_path = tidy_src.join("issues_blessed.txt");
57 let mut blessed_issues_txt = fs::File::create(&blessed_issues_path).unwrap();
58 blessed_issues_txt.write_all(ISSUES_TXT_HEADER.as_bytes()).unwrap();
59 for filename in allowed_issue_names.difference(&remaining_issue_names) {
61 writeln!(blessed_issues_txt, "{filename}").unwrap();
62 }
63 let old_issues_path = tidy_src.join("issues.txt");
64 fs::rename(blessed_issues_path, old_issues_path).unwrap();
65 } else {
66 for file_name in remaining_issue_names {
67 let mut p = PathBuf::from(path);
68 p.push(file_name);
69 check.error(format!(
70 "file `{}` no longer exists and should be removed from the exclusions in `src/tools/tidy/src/issues.txt`",
71 p.display()
72 ));
73 }
74 }
75}
76
77fn deny_new_top_level_ui_tests(check: &mut RunningCheck, tests_path: &Path) {
78 let top_level_ui_tests = ignore::WalkBuilder::new(tests_path)
85 .max_depth(Some(1))
86 .follow_links(false)
87 .build()
88 .flatten()
89 .filter(|e| {
90 let file_name = e.file_name();
91 file_name != ".gitattributes" && file_name != "README.md"
92 })
93 .filter(|e| !e.file_type().is_some_and(|f| f.is_dir()));
94
95 for entry in top_level_ui_tests {
96 check.error(format!(
97 "ui tests should be added under meaningful subdirectories: `{}`, see https://github.com/rust-lang/compiler-team/issues/902",
98 entry.path().display()
99 ));
100 }
101}
102
103fn recursively_check_ui_tests<'issues>(
104 check: &mut RunningCheck,
105 path: &Path,
106 allowed_issue_names: &'issues BTreeSet<&'issues str>,
107) -> BTreeSet<&'issues str> {
108 let mut remaining_issue_names: BTreeSet<&str> = allowed_issue_names.clone();
109
110 let (ui, ui_fulldeps) = (path.join("ui"), path.join("ui-fulldeps"));
111 let paths = [ui.as_path(), ui_fulldeps.as_path()];
112 crate::walk::walk_no_read(&paths, |_, _| false, &mut |entry| {
113 let file_path = entry.path();
114 if let Some(ext) = file_path.extension().and_then(OsStr::to_str) {
115 check_unexpected_extension(check, file_path, ext);
116
117 let testname =
120 file_path.file_name().unwrap().to_str().unwrap().split_once('.').unwrap().0;
121 if ext == "stderr" || ext == "stdout" || ext == "fixed" {
122 check_stray_output_snapshot(check, file_path, testname);
123 check_empty_output_snapshot(check, file_path);
124 }
125
126 deny_new_nondescriptive_test_names(
127 check,
128 path,
129 &mut remaining_issue_names,
130 file_path,
131 testname,
132 ext,
133 );
134 }
135 });
136 remaining_issue_names
137}
138
139fn check_unexpected_extension(check: &mut RunningCheck, file_path: &Path, ext: &str) {
140 const EXPECTED_TEST_FILE_EXTENSIONS: &[&str] = &[
141 "rs", "stderr", "svg", "stdout", "fixed", "md", "ftl", ];
149
150 const EXTENSION_EXCEPTION_PATHS: &[&str] = &[
151 "tests/ui/asm/named-asm-labels.s", "tests/ui/codegen/mismatched-data-layout.json", "tests/ui/check-cfg/my-awesome-platform.json", "tests/ui/argfile/commandline-argfile-badutf8.args", "tests/ui/argfile/commandline-argfile.args", "tests/ui/crate-loading/auxiliary/libfoo.rlib", "tests/ui/include-macros/data.bin", "tests/ui/include-macros/file.txt", "tests/ui/macros/macro-expanded-include/file.txt", "tests/ui/macros/not-utf8.bin", "tests/ui/macros/syntax-extension-source-utils-files/includeme.fragment", "tests/ui/proc-macro/auxiliary/included-file.txt", "tests/ui/unpretty/auxiliary/data.txt", "tests/ui/invalid/foo.natvis.xml", "tests/ui/sanitizer/dataflow-abilist.txt", "tests/ui/shell-argfiles/shell-argfiles.args", "tests/ui/shell-argfiles/shell-argfiles-badquotes.args", "tests/ui/shell-argfiles/shell-argfiles-via-argfile-shell.args", "tests/ui/shell-argfiles/shell-argfiles-via-argfile.args", "tests/ui/std/windows-bat-args1.bat", "tests/ui/std/windows-bat-args2.bat", "tests/ui/std/windows-bat-args3.bat", ];
174
175 if !(EXPECTED_TEST_FILE_EXTENSIONS.contains(&ext)
178 || EXTENSION_EXCEPTION_PATHS.iter().any(|path| file_path.ends_with(path)))
179 {
180 check.error(format!("file {} has unexpected extension {}", file_path.display(), ext));
181 }
182}
183
184fn check_stray_output_snapshot(check: &mut RunningCheck, file_path: &Path, testname: &str) {
185 if !file_path.with_file_name(testname).with_extension("rs").exists()
197 && !testname.contains("ignore-tidy")
198 {
199 check.error(format!("Stray file with UI testing output: {:?}", file_path));
200 }
201}
202
203fn check_empty_output_snapshot(check: &mut RunningCheck, file_path: &Path) {
204 if let Ok(metadata) = fs::metadata(file_path)
205 && metadata.len() == 0
206 {
207 check.error(format!("Empty file with UI testing output: {:?}", file_path));
208 }
209}
210
211fn deny_new_nondescriptive_test_names(
212 check: &mut RunningCheck,
213 path: &Path,
214 remaining_issue_names: &mut BTreeSet<&str>,
215 file_path: &Path,
216 testname: &str,
217 ext: &str,
218) {
219 if ext == "rs"
220 && let Some(test_name) = static_regex!(r"^issues?[-_]?(\d{3,})").captures(testname)
221 {
222 let stripped_path = file_path
224 .strip_prefix(path)
225 .unwrap()
226 .to_str()
227 .unwrap()
228 .replace(std::path::MAIN_SEPARATOR_STR, "/");
229
230 if !remaining_issue_names.remove(stripped_path.as_str())
231 && !stripped_path.starts_with("ui/issues/")
232 {
233 check.error(format!(
234 "file `tests/{stripped_path}` must begin with a descriptive name, consider `{{reason}}-issue-{issue_n}.rs`",
235 issue_n = &test_name[1],
236 ));
237 }
238 }
239}