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, RunningCheck, TidyCtx};
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, tidy_ctx: TidyCtx) {
18 let path = &root_path.join("tests");
19 let mut check = tidy_ctx.start_check(CheckId::new("ui_tests").path(path));
20 let bless = tidy_ctx.is_bless_enabled();
21
22 let mut prev_line = "";
25 let mut is_sorted = true;
26 let allowed_issue_names: BTreeSet<_> = include_str!("issues.txt")
27 .strip_prefix(ISSUES_TXT_HEADER)
28 .unwrap()
29 .lines()
30 .inspect(|&line| {
31 if prev_line > line {
32 is_sorted = false;
33 }
34
35 prev_line = line;
36 })
37 .collect();
38
39 if !is_sorted && !bless {
40 check.error(
41 "`src/tools/tidy/src/issues.txt` is not in order, mostly because you modified it manually,
42 please only update it with command `x test tidy --bless`"
43 );
44 }
45
46 deny_new_top_level_ui_tests(&mut check, &path.join("ui"));
47
48 let remaining_issue_names = recursively_check_ui_tests(&mut check, path, &allowed_issue_names);
49
50 if bless && (!remaining_issue_names.is_empty() || !is_sorted) {
54 let tidy_src = root_path.join("src/tools/tidy/src");
55 let blessed_issues_path = tidy_src.join("issues_blessed.txt");
58 let mut blessed_issues_txt = fs::File::create(&blessed_issues_path).unwrap();
59 blessed_issues_txt.write_all(ISSUES_TXT_HEADER.as_bytes()).unwrap();
60 for filename in allowed_issue_names.difference(&remaining_issue_names) {
62 writeln!(blessed_issues_txt, "{filename}").unwrap();
63 }
64 let old_issues_path = tidy_src.join("issues.txt");
65 fs::rename(blessed_issues_path, old_issues_path).unwrap();
66 } else {
67 for file_name in remaining_issue_names {
68 let mut p = PathBuf::from(path);
69 p.push(file_name);
70 check.error(format!(
71 "file `{}` no longer exists and should be removed from the exclusions in `src/tools/tidy/src/issues.txt`",
72 p.display()
73 ));
74 }
75 }
76
77 let mut prev_line = String::new();
82 let mut is_sorted = true;
83 let documented_subdirs: BTreeSet<_> = include_str!("../../../../tests/ui/README.md")
84 .lines()
85 .filter_map(|line| {
86 static_regex!(r"^##.*?`(?<dir>[^`]+)`").captures(line).map(|cap| {
87 let dir = &cap["dir"];
88 if dir.ends_with('/') {
90 dir.strip_suffix('/').unwrap().to_string()
91 } else {
92 dir.to_string()
93 }
94 })
95 })
96 .inspect(|line| {
97 if prev_line.as_str() > line.as_str() {
98 is_sorted = false;
99 }
100
101 prev_line = line.clone();
102 })
103 .collect();
104 let filesystem_subdirs = collect_ui_tests_subdirs(&path);
105 let is_modified = !filesystem_subdirs.eq(&documented_subdirs);
106
107 if !is_sorted {
108 check.error("`tests/ui/README.md` is not in order");
109 }
110 if is_modified {
111 for directory in documented_subdirs.symmetric_difference(&filesystem_subdirs) {
112 if documented_subdirs.contains(directory) {
113 check.error(format!(
114 "ui subdirectory `{directory}` is listed in `tests/ui/README.md` but does not exist in the filesystem"
115 ));
116 } else {
117 check.error(format!(
118 "ui subdirectory `{directory}` exists in the filesystem but is not documented in `tests/ui/README.md`"
119 ));
120 }
121 }
122 check.error(
123 "`tests/ui/README.md` subdirectory listing is out of sync with the filesystem. \
124 Please add or remove subdirectory entries (## headers with backtick-wrapped names) to match the actual directories in `tests/ui/`"
125 );
126 }
127}
128
129fn deny_new_top_level_ui_tests(check: &mut RunningCheck, tests_path: &Path) {
130 let top_level_ui_tests = ignore::WalkBuilder::new(tests_path)
137 .max_depth(Some(1))
138 .follow_links(false)
139 .build()
140 .flatten()
141 .filter(|e| {
142 let file_name = e.file_name();
143 file_name != ".gitattributes" && file_name != "README.md"
144 })
145 .filter(|e| !e.file_type().is_some_and(|f| f.is_dir()));
146
147 for entry in top_level_ui_tests {
148 check.error(format!(
149 "ui tests should be added under meaningful subdirectories: `{}`, see https://github.com/rust-lang/compiler-team/issues/902",
150 entry.path().display()
151 ));
152 }
153}
154
155fn recursively_check_ui_tests<'issues>(
156 check: &mut RunningCheck,
157 path: &Path,
158 allowed_issue_names: &'issues BTreeSet<&'issues str>,
159) -> BTreeSet<&'issues str> {
160 let mut remaining_issue_names: BTreeSet<&str> = allowed_issue_names.clone();
161
162 let (ui, ui_fulldeps) = (path.join("ui"), path.join("ui-fulldeps"));
163 let paths = [ui.as_path(), ui_fulldeps.as_path()];
164 crate::walk::walk_no_read(&paths, |_, _| false, &mut |entry| {
165 let file_path = entry.path();
166 if let Some(ext) = file_path.extension().and_then(OsStr::to_str) {
167 check_unexpected_extension(check, file_path, ext);
168
169 let testname =
172 file_path.file_name().unwrap().to_str().unwrap().split_once('.').unwrap().0;
173 if ext == "stderr" || ext == "stdout" || ext == "fixed" {
174 check_stray_output_snapshot(check, file_path, testname);
175 check_empty_output_snapshot(check, file_path);
176 }
177
178 deny_new_nondescriptive_test_names(
179 check,
180 path,
181 &mut remaining_issue_names,
182 file_path,
183 testname,
184 ext,
185 );
186 }
187 });
188 remaining_issue_names
189}
190
191fn collect_ui_tests_subdirs(path: &Path) -> BTreeSet<String> {
192 let ui = path.join("ui");
193 let entries = std::fs::read_dir(ui.as_path()).unwrap();
194
195 entries
196 .filter_map(|entry| entry.ok())
197 .map(|entry| entry.path())
198 .filter(|path| path.is_dir())
199 .map(|dir_path| {
200 let dir_path = dir_path.strip_prefix(path).unwrap();
201 format!(
202 "tests/{}",
203 dir_path.to_string_lossy().replace(std::path::MAIN_SEPARATOR_STR, "/")
204 )
205 })
206 .collect()
207}
208
209fn check_unexpected_extension(check: &mut RunningCheck, file_path: &Path, ext: &str) {
210 const EXPECTED_TEST_FILE_EXTENSIONS: &[&str] = &[
211 "rs", "stderr", "svg", "stdout", "fixed", "md", "ftl", ];
219
220 const EXTENSION_EXCEPTION_PATHS: &[&str] = &[
221 "tests/ui/asm/named-asm-labels.s", "tests/ui/asm/normalize-offsets-for-crlf.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/include-macros/invalid-utf8-binary-file.bin", "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", ];
246
247 if !(EXPECTED_TEST_FILE_EXTENSIONS.contains(&ext)
250 || EXTENSION_EXCEPTION_PATHS.iter().any(|path| file_path.ends_with(path)))
251 {
252 check.error(format!("file {} has unexpected extension {}", file_path.display(), ext));
253 }
254}
255
256fn check_stray_output_snapshot(check: &mut RunningCheck, file_path: &Path, testname: &str) {
257 if !file_path.with_file_name(testname).with_extension("rs").exists()
269 && !testname.contains("ignore-tidy")
270 {
271 check.error(format!("Stray file with UI testing output: {:?}", file_path));
272 }
273}
274
275fn check_empty_output_snapshot(check: &mut RunningCheck, file_path: &Path) {
276 if let Ok(metadata) = fs::metadata(file_path)
277 && metadata.len() == 0
278 {
279 check.error(format!("Empty file with UI testing output: {:?}", file_path));
280 }
281}
282
283fn deny_new_nondescriptive_test_names(
284 check: &mut RunningCheck,
285 path: &Path,
286 remaining_issue_names: &mut BTreeSet<&str>,
287 file_path: &Path,
288 testname: &str,
289 ext: &str,
290) {
291 if ext == "rs"
292 && let Some(test_name) = static_regex!(r"^issues?[-_]?(\d{3,})").captures(testname)
293 {
294 let stripped_path = file_path
296 .strip_prefix(path)
297 .unwrap()
298 .to_str()
299 .unwrap()
300 .replace(std::path::MAIN_SEPARATOR_STR, "/");
301
302 if !remaining_issue_names.remove(stripped_path.as_str())
303 && !stripped_path.starts_with("ui/issues/")
304 {
305 check.error(format!(
306 "issue-number-only test names are not descriptive, consider renaming file `tests/{stripped_path}` to `{{reason}}-issue-{issue_n}.rs`",
307 issue_n = &test_name[1],
308 ));
309 }
310 }
311}