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 EXPECTED_TEST_FILE_EXTENSIONS: &[&str] = &[
11    "rs",     // test source files
12    "stderr", // expected stderr file, corresponds to a rs file
13    "svg",    // expected svg file, corresponds to a rs file, equivalent to stderr
14    "stdout", // expected stdout file, corresponds to a rs file
15    "fixed",  // expected source file after applying fixes
16    "md",     // test directory descriptions
17    "ftl",    // translation tests
18];
19
20const EXTENSION_EXCEPTION_PATHS: &[&str] = &[
21    "tests/ui/asm/named-asm-labels.s", // loading an external asm file to test named labels lint
22    "tests/ui/codegen/mismatched-data-layout.json", // testing mismatched data layout w/ custom targets
23    "tests/ui/check-cfg/my-awesome-platform.json",  // testing custom targets with cfgs
24    "tests/ui/argfile/commandline-argfile-badutf8.args", // passing args via a file
25    "tests/ui/argfile/commandline-argfile.args",    // passing args via a file
26    "tests/ui/crate-loading/auxiliary/libfoo.rlib", // testing loading a manually created rlib
27    "tests/ui/include-macros/data.bin", // testing including data with the include macros
28    "tests/ui/include-macros/file.txt", // testing including data with the include macros
29    "tests/ui/macros/macro-expanded-include/file.txt", // testing including data with the include macros
30    "tests/ui/macros/not-utf8.bin", // testing including data with the include macros
31    "tests/ui/macros/syntax-extension-source-utils-files/includeme.fragment", // more include
32    "tests/ui/proc-macro/auxiliary/included-file.txt", // more include
33    "tests/ui/unpretty/auxiliary/data.txt", // more include
34    "tests/ui/invalid/foo.natvis.xml", // sample debugger visualizer
35    "tests/ui/sanitizer/dataflow-abilist.txt", // dataflow sanitizer ABI list file
36    "tests/ui/shell-argfiles/shell-argfiles.args", // passing args via a file
37    "tests/ui/shell-argfiles/shell-argfiles-badquotes.args", // passing args via a file
38    "tests/ui/shell-argfiles/shell-argfiles-via-argfile-shell.args", // passing args via a file
39    "tests/ui/shell-argfiles/shell-argfiles-via-argfile.args", // passing args via a file
40    "tests/ui/std/windows-bat-args1.bat", // tests escaping arguments through batch files
41    "tests/ui/std/windows-bat-args2.bat", // tests escaping arguments through batch files
42    "tests/ui/std/windows-bat-args3.bat", // tests escaping arguments through batch files
43];
44
45pub fn check(root_path: &Path, bless: bool, bad: &mut bool) {
46    let issues_txt_header = r#"============================================================
47    ⚠️⚠️⚠️NOTHING SHOULD EVER BE ADDED TO THIS LIST⚠️⚠️⚠️
48============================================================
49"#;
50
51    let path = &root_path.join("tests");
52
53    // the list of files in ui tests that are allowed to start with `issue-XXXX`
54    // BTreeSet because we would like a stable ordering so --bless works
55    let mut prev_line = "";
56    let mut is_sorted = true;
57    let allowed_issue_names: BTreeSet<_> = include_str!("issues.txt")
58        .strip_prefix(issues_txt_header)
59        .unwrap()
60        .lines()
61        .inspect(|&line| {
62            if prev_line > line {
63                is_sorted = false;
64            }
65
66            prev_line = line;
67        })
68        .collect();
69
70    if !is_sorted && !bless {
71        tidy_error!(
72            bad,
73            "`src/tools/tidy/src/issues.txt` is not in order, mostly because you modified it manually,
74            please only update it with command `x test tidy --bless`"
75        );
76    }
77
78    let mut remaining_issue_names: BTreeSet<&str> = allowed_issue_names.clone();
79
80    let (ui, ui_fulldeps) = (path.join("ui"), path.join("ui-fulldeps"));
81    let paths = [ui.as_path(), ui_fulldeps.as_path()];
82    crate::walk::walk_no_read(&paths, |_, _| false, &mut |entry| {
83        let file_path = entry.path();
84        if let Some(ext) = file_path.extension().and_then(OsStr::to_str) {
85            // files that are neither an expected extension or an exception should not exist
86            // they're probably typos or not meant to exist
87            if !(EXPECTED_TEST_FILE_EXTENSIONS.contains(&ext)
88                || EXTENSION_EXCEPTION_PATHS.iter().any(|path| file_path.ends_with(path)))
89            {
90                tidy_error!(bad, "file {} has unexpected extension {}", file_path.display(), ext);
91            }
92
93            // NB: We do not use file_stem() as some file names have multiple `.`s and we
94            // must strip all of them.
95            let testname =
96                file_path.file_name().unwrap().to_str().unwrap().split_once('.').unwrap().0;
97            if ext == "stderr" || ext == "stdout" || ext == "fixed" {
98                // Test output filenames have one of the formats:
99                // ```
100                // $testname.stderr
101                // $testname.$mode.stderr
102                // $testname.$revision.stderr
103                // $testname.$revision.$mode.stderr
104                // ```
105                //
106                // For now, just make sure that there is a corresponding
107                // `$testname.rs` file.
108
109                if !file_path.with_file_name(testname).with_extension("rs").exists()
110                    && !testname.contains("ignore-tidy")
111                {
112                    tidy_error!(bad, "Stray file with UI testing output: {:?}", file_path);
113                }
114
115                if let Ok(metadata) = fs::metadata(file_path)
116                    && metadata.len() == 0
117                {
118                    tidy_error!(bad, "Empty file with UI testing output: {:?}", file_path);
119                }
120            }
121
122            if ext == "rs"
123                && let Some(test_name) = static_regex!(r"^issues?[-_]?(\d{3,})").captures(testname)
124            {
125                // these paths are always relative to the passed `path` and always UTF8
126                let stripped_path = file_path
127                    .strip_prefix(path)
128                    .unwrap()
129                    .to_str()
130                    .unwrap()
131                    .replace(std::path::MAIN_SEPARATOR_STR, "/");
132
133                if !remaining_issue_names.remove(stripped_path.as_str())
134                    && !stripped_path.starts_with("ui/issues/")
135                {
136                    tidy_error!(
137                        bad,
138                        "file `tests/{stripped_path}` must begin with a descriptive name, consider `{{reason}}-issue-{issue_n}.rs`",
139                        issue_n = &test_name[1],
140                    );
141                }
142            }
143        }
144    });
145
146    // if there are any file names remaining, they were moved on the fs.
147    // our data must remain up to date, so it must be removed from issues.txt
148    // do this automatically on bless, otherwise issue a tidy error
149    if bless && (!remaining_issue_names.is_empty() || !is_sorted) {
150        let tidy_src = root_path.join("src/tools/tidy/src");
151        // instead of overwriting the file, recreate it and use an "atomic rename"
152        // so we don't bork things on panic or a contributor using Ctrl+C
153        let blessed_issues_path = tidy_src.join("issues_blessed.txt");
154        let mut blessed_issues_txt = fs::File::create(&blessed_issues_path).unwrap();
155        blessed_issues_txt.write_all(issues_txt_header.as_bytes()).unwrap();
156        // If we changed paths to use the OS separator, reassert Unix chauvinism for blessing.
157        for filename in allowed_issue_names.difference(&remaining_issue_names) {
158            writeln!(blessed_issues_txt, "{filename}").unwrap();
159        }
160        let old_issues_path = tidy_src.join("issues.txt");
161        fs::rename(blessed_issues_path, old_issues_path).unwrap();
162    } else {
163        for file_name in remaining_issue_names {
164            let mut p = PathBuf::from(path);
165            p.push(file_name);
166            tidy_error!(
167                bad,
168                "file `{}` no longer exists and should be removed from the exclusions in `src/tools/tidy/src/issues.txt`",
169                p.display()
170            );
171        }
172    }
173}