tidy/
tests_revision_unpaired_stdout_stderr.rs

1//! Checks that there are no unpaired `.stderr` or `.stdout` for a test with and without revisions.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::ffi::OsStr;
5use std::path::Path;
6
7use crate::iter_header::*;
8use crate::walk::*;
9
10// Should be kept in sync with `CompareMode` in `src/tools/compiletest/src/common.rs`,
11// as well as `run`.
12const IGNORES: &[&str] = &[
13    "polonius",
14    "chalk",
15    "split-dwarf",
16    "split-dwarf-single",
17    "next-solver-coherence",
18    "next-solver",
19    "run",
20];
21const EXTENSIONS: &[&str] = &["stdout", "stderr"];
22const SPECIAL_TEST: &str = "tests/ui/command/need-crate-arg-ignore-tidy.x.rs";
23
24pub fn check(tests_path: impl AsRef<Path>, bad: &mut bool) {
25    // Recurse over subdirectories under `tests/`
26    walk_dir(tests_path.as_ref(), filter, &mut |entry| {
27        // We are inspecting a folder. Collect the paths to interesting files `.rs`, `.stderr`,
28        // `.stdout` under the current folder (shallow).
29        let mut files_under_inspection = BTreeSet::new();
30        for sibling in std::fs::read_dir(entry.path()).unwrap() {
31            let Ok(sibling) = sibling else {
32                continue;
33            };
34
35            if sibling.path().is_dir() {
36                continue;
37            }
38
39            let sibling_path = sibling.path();
40
41            let Some(ext) = sibling_path.extension().map(OsStr::to_str).flatten() else {
42                continue;
43            };
44
45            if ext == "rs" || EXTENSIONS.contains(&ext) {
46                files_under_inspection.insert(sibling_path);
47            }
48        }
49
50        let mut test_info = BTreeMap::new();
51
52        for test in
53            files_under_inspection.iter().filter(|f| f.extension().is_some_and(|ext| ext == "rs"))
54        {
55            if test.ends_with(SPECIAL_TEST) {
56                continue;
57            }
58
59            let mut expected_revisions = BTreeSet::new();
60
61            let Ok(contents) = std::fs::read_to_string(test) else { continue };
62
63            // Collect directives.
64            iter_header(&contents, &mut |HeaderLine { revision, directive, .. }| {
65                // We're trying to *find* `//@ revision: xxx` directives themselves, not revisioned
66                // directives.
67                if revision.is_some() {
68                    return;
69                }
70
71                let directive = directive.trim();
72
73                if directive.starts_with("revisions") {
74                    let Some((name, value)) = directive.split_once([':', ' ']) else {
75                        return;
76                    };
77
78                    if name == "revisions" {
79                        let revs = value.split(' ');
80                        for rev in revs {
81                            expected_revisions.insert(rev.to_owned());
82                        }
83                    }
84                }
85            });
86
87            let Some(test_name) = test.file_stem().map(OsStr::to_str).flatten() else {
88                continue;
89            };
90
91            assert!(
92                !test_name.contains('.'),
93                "test name cannot contain dots '.': `{}`",
94                test.display()
95            );
96
97            test_info.insert(test_name.to_string(), (test, expected_revisions));
98        }
99
100        // Our test file `foo.rs` has specified no revisions. There should not be any
101        // `foo.rev{.stderr,.stdout}` files. rustc-dev-guide says test output files can have names
102        // of the form: `test-name.revision.compare_mode.extension`, but our only concern is
103        // `test-name.revision` and `extension`.
104        for sibling in files_under_inspection.iter().filter(|f| {
105            f.extension().map(OsStr::to_str).flatten().is_some_and(|ext| EXTENSIONS.contains(&ext))
106        }) {
107            let Some(filename) = sibling.file_name().map(OsStr::to_str).flatten() else {
108                continue;
109            };
110
111            let filename_components = filename.split('.').collect::<Vec<_>>();
112            let [file_prefix, ..] = &filename_components[..] else {
113                continue;
114            };
115
116            let Some((test_path, expected_revisions)) = test_info.get(*file_prefix) else {
117                continue;
118            };
119
120            match &filename_components[..] {
121                // Cannot have a revision component, skip.
122                [] | [_] => return,
123                [_, _] if !expected_revisions.is_empty() => {
124                    // Found unrevisioned output files for a revisioned test.
125                    tidy_error!(
126                        bad,
127                        "found unrevisioned output file `{}` for a revisioned test `{}`",
128                        sibling.display(),
129                        test_path.display(),
130                    );
131                }
132                [_, _] => return,
133                [_, found_revision, .., extension] => {
134                    if !IGNORES.contains(&found_revision)
135                        && !expected_revisions.contains(*found_revision)
136                        // This is from `//@ stderr-per-bitwidth`
137                        && !(*extension == "stderr" && ["32bit", "64bit"].contains(&found_revision))
138                    {
139                        // Found some unexpected revision-esque component that is not a known
140                        // compare-mode or expected revision.
141                        tidy_error!(
142                            bad,
143                            "found output file `{}` for unexpected revision `{}` of test `{}`",
144                            sibling.display(),
145                            found_revision,
146                            test_path.display()
147                        );
148                    }
149                }
150            }
151        }
152    });
153}
154
155fn filter(path: &Path) -> bool {
156    filter_dirs(path) // ignore certain dirs
157        || (path.file_name().is_some_and(|name| name == "auxiliary")) // ignore auxiliary folder
158}