1use 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
13const ENTRY_LIMIT: u32 = 901;
18const ISSUES_ENTRY_LIMIT: u32 = 1634;
21
22const EXPECTED_TEST_FILE_EXTENSIONS: &[&str] = &[
23 "rs", "stderr", "svg", "stdout", "fixed", "md", "ftl", ];
31
32const EXTENSION_EXCEPTION_PATHS: &[&str] = &[
33 "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", ];
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 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 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 let testname =
148 file_path.file_name().unwrap().to_str().unwrap().split_once('.').unwrap().0;
149 if ext == "stderr" || ext == "stdout" || ext == "fixed" {
150 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 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 bless && (!remaining_issue_names.is_empty() || !is_sorted) {
201 let tidy_src = root_path.join("src/tools/tidy/src");
202 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 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}