1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//! Checks that there are no unpaired `.stderr` or `.stdout` for a test with and without revisions.

use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsStr;
use std::path::Path;

use crate::iter_header::*;
use crate::walk::*;

// Should be kept in sync with `CompareMode` in `src/tools/compiletest/src/common.rs`,
// as well as `run`.
const IGNORES: &[&str] = &[
    "polonius",
    "chalk",
    "split-dwarf",
    "split-dwarf-single",
    "next-solver-coherence",
    "next-solver",
    "run",
];
const EXTENSIONS: &[&str] = &["stdout", "stderr"];
const SPECIAL_TEST: &str = "tests/ui/command/need-crate-arg-ignore-tidy.x.rs";

pub fn check(tests_path: impl AsRef<Path>, bad: &mut bool) {
    // Recurse over subdirectories under `tests/`
    walk_dir(tests_path.as_ref(), filter, &mut |entry| {
        // We are inspecting a folder. Collect the paths to interesting files `.rs`, `.stderr`,
        // `.stdout` under the current folder (shallow).
        let mut files_under_inspection = BTreeSet::new();
        for sibling in std::fs::read_dir(entry.path()).unwrap() {
            let Ok(sibling) = sibling else {
                continue;
            };

            if sibling.path().is_dir() {
                continue;
            }

            let sibling_path = sibling.path();

            let Some(ext) = sibling_path.extension().map(OsStr::to_str).flatten() else {
                continue;
            };

            if ext == "rs" || EXTENSIONS.contains(&ext) {
                files_under_inspection.insert(sibling_path);
            }
        }

        let mut test_info = BTreeMap::new();

        for test in
            files_under_inspection.iter().filter(|f| f.extension().is_some_and(|ext| ext == "rs"))
        {
            if test.ends_with(SPECIAL_TEST) {
                continue;
            }

            let mut expected_revisions = BTreeSet::new();

            let contents = std::fs::read_to_string(test).unwrap();

            // Collect directives.
            iter_header(&contents, &mut |HeaderLine { revision, directive, .. }| {
                // We're trying to *find* `//@ revision: xxx` directives themselves, not revisioned
                // directives.
                if revision.is_some() {
                    return;
                }

                let directive = directive.trim();

                if directive.starts_with("revisions") {
                    let Some((name, value)) = directive.split_once([':', ' ']) else {
                        return;
                    };

                    if name == "revisions" {
                        let revs = value.split(' ');
                        for rev in revs {
                            expected_revisions.insert(rev.to_owned());
                        }
                    }
                }
            });

            let Some(test_name) = test.file_stem().map(OsStr::to_str).flatten() else {
                continue;
            };

            assert!(
                !test_name.contains('.'),
                "test name cannot contain dots '.': `{}`",
                test.display()
            );

            test_info.insert(test_name.to_string(), (test, expected_revisions));
        }

        // Our test file `foo.rs` has specified no revisions. There should not be any
        // `foo.rev{.stderr,.stdout}` files. rustc-dev-guide says test output files can have names
        // of the form: `test-name.revision.compare_mode.extension`, but our only concern is
        // `test-name.revision` and `extension`.
        for sibling in files_under_inspection.iter().filter(|f| {
            f.extension().map(OsStr::to_str).flatten().is_some_and(|ext| EXTENSIONS.contains(&ext))
        }) {
            let Some(filename) = sibling.file_name().map(OsStr::to_str).flatten() else {
                continue;
            };

            let filename_components = filename.split('.').collect::<Vec<_>>();
            let [file_prefix, ..] = &filename_components[..] else {
                continue;
            };

            let Some((test_path, expected_revisions)) = test_info.get(*file_prefix) else {
                continue;
            };

            match &filename_components[..] {
                // Cannot have a revision component, skip.
                [] | [_] => return,
                [_, _] if !expected_revisions.is_empty() => {
                    // Found unrevisioned output files for a revisioned test.
                    tidy_error!(
                        bad,
                        "found unrevisioned output file `{}` for a revisioned test `{}`",
                        sibling.display(),
                        test_path.display(),
                    );
                }
                [_, _] => return,
                [_, found_revision, .., extension] => {
                    if !IGNORES.contains(&found_revision)
                        && !expected_revisions.contains(*found_revision)
                        // This is from `//@ stderr-per-bitwidth`
                        && !(*extension == "stderr" && ["32bit", "64bit"].contains(&found_revision))
                    {
                        // Found some unexpected revision-esque component that is not a known
                        // compare-mode or expected revision.
                        tidy_error!(
                            bad,
                            "found output file `{}` for unexpected revision `{}` of test `{}`",
                            sibling.display(),
                            found_revision,
                            test_path.display()
                        );
                    }
                }
            }
        }
    });
}

fn filter(path: &Path) -> bool {
    filter_dirs(path) // ignore certain dirs
        || (path.file_name().is_some_and(|name| name == "auxiliary")) // ignore auxiliary folder
}