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