Skip to content

Commit dcde08f

Browse files
authored
Rollup merge of #121475 - jieyouxu:tidy-stderr-check, r=the8472,compiler-errors
Add tidy check for .stderr/.stdout files for non-existent test revisions Closes #77498.
2 parents f23c6dd + 19ee457 commit dcde08f

29 files changed

+202
-619
lines changed

Diff for: src/tools/tidy/src/iter_header.rs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const COMMENT: &str = "//@";
2+
3+
/// A header line, like `//@name: value` consists of the prefix `//@` and the directive
4+
/// `name: value`. It is also possibly revisioned, e.g. `//@[revision] name: value`.
5+
pub(crate) struct HeaderLine<'ln> {
6+
pub(crate) revision: Option<&'ln str>,
7+
pub(crate) directive: &'ln str,
8+
}
9+
10+
/// Iterate through compiletest headers in a test contents.
11+
///
12+
/// Adjusted from compiletest/src/header.rs.
13+
pub(crate) fn iter_header<'ln>(contents: &'ln str, it: &mut dyn FnMut(HeaderLine<'ln>)) {
14+
for ln in contents.lines() {
15+
let ln = ln.trim();
16+
17+
// We're left with potentially `[rev]name: value`.
18+
let Some(remainder) = ln.strip_prefix(COMMENT) else {
19+
continue;
20+
};
21+
22+
if let Some(remainder) = remainder.trim_start().strip_prefix('[') {
23+
let Some((revision, remainder)) = remainder.split_once(']') else {
24+
panic!("malformed revision directive: expected `//@[rev]`, found `{ln}`");
25+
};
26+
// We trimmed off the `[rev]` portion, left with `name: value`.
27+
it(HeaderLine { revision: Some(revision), directive: remainder.trim() });
28+
} else {
29+
it(HeaderLine { revision: None, directive: remainder.trim() });
30+
}
31+
}
32+
}

Diff for: src/tools/tidy/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ pub mod ext_tool_checks;
6565
pub mod extdeps;
6666
pub mod features;
6767
pub mod fluent_alphabetical;
68+
pub(crate) mod iter_header;
6869
pub mod mir_opt_tests;
6970
pub mod pal;
7071
pub mod rustdoc_css_themes;
@@ -73,6 +74,7 @@ pub mod style;
7374
pub mod target_policy;
7475
pub mod target_specific_tests;
7576
pub mod tests_placement;
77+
pub mod tests_revision_unpaired_stdout_stderr;
7678
pub mod ui_tests;
7779
pub mod unit_tests;
7880
pub mod unstable_book;

Diff for: src/tools/tidy/src/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ fn main() {
100100

101101
// Checks over tests.
102102
check!(tests_placement, &root_path);
103+
check!(tests_revision_unpaired_stdout_stderr, &tests_path);
103104
check!(debug_artifacts, &tests_path);
104105
check!(ui_tests, &tests_path, bless);
105106
check!(mir_opt_tests, &tests_path, bless);

Diff for: src/tools/tidy/src/target_specific_tests.rs

+4-24
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,12 @@
44
use std::collections::BTreeMap;
55
use std::path::Path;
66

7+
use crate::iter_header::{iter_header, HeaderLine};
78
use crate::walk::filter_not_rust;
89

9-
const COMMENT: &str = "//@";
1010
const LLVM_COMPONENTS_HEADER: &str = "needs-llvm-components:";
1111
const COMPILE_FLAGS_HEADER: &str = "compile-flags:";
1212

13-
/// Iterate through compiletest headers in a test contents.
14-
///
15-
/// Adjusted from compiletest/src/header.rs.
16-
fn iter_header<'a>(contents: &'a str, it: &mut dyn FnMut(Option<&'a str>, &'a str)) {
17-
for ln in contents.lines() {
18-
let ln = ln.trim();
19-
if ln.starts_with(COMMENT) && ln[COMMENT.len()..].trim_start().starts_with('[') {
20-
if let Some(close_brace) = ln.find(']') {
21-
let open_brace = ln.find('[').unwrap();
22-
let lncfg = &ln[open_brace + 1..close_brace];
23-
it(Some(lncfg), ln[(close_brace + 1)..].trim_start());
24-
} else {
25-
panic!("malformed condition directive: expected `//[foo]`, found `{ln}`")
26-
}
27-
} else if ln.starts_with(COMMENT) {
28-
it(None, ln[COMMENT.len()..].trim_start());
29-
}
30-
}
31-
}
32-
3313
#[derive(Default, Debug)]
3414
struct RevisionInfo<'a> {
3515
target_arch: Option<&'a str>,
@@ -40,9 +20,9 @@ pub fn check(path: &Path, bad: &mut bool) {
4020
crate::walk::walk(path, |path, _is_dir| filter_not_rust(path), &mut |entry, content| {
4121
let file = entry.path().display();
4222
let mut header_map = BTreeMap::new();
43-
iter_header(content, &mut |cfg, directive| {
23+
iter_header(content, &mut |HeaderLine { revision, directive }| {
4424
if let Some(value) = directive.strip_prefix(LLVM_COMPONENTS_HEADER) {
45-
let info = header_map.entry(cfg).or_insert(RevisionInfo::default());
25+
let info = header_map.entry(revision).or_insert(RevisionInfo::default());
4626
let comp_vec = info.llvm_components.get_or_insert(Vec::new());
4727
for component in value.split(' ') {
4828
let component = component.trim();
@@ -56,7 +36,7 @@ pub fn check(path: &Path, bad: &mut bool) {
5636
if let Some((arch, _)) =
5737
v.trim_start_matches(|c| c == ' ' || c == '=').split_once("-")
5838
{
59-
let info = header_map.entry(cfg).or_insert(RevisionInfo::default());
39+
let info = header_map.entry(revision).or_insert(RevisionInfo::default());
6040
info.target_arch.replace(arch);
6141
} else {
6242
eprintln!("{file}: seems to have a malformed --target value");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//! Checks that there are no unpaired `.stderr` or `.stdout` for a test with and without revisions.
2+
3+
use std::collections::{BTreeMap, BTreeSet};
4+
use std::ffi::OsStr;
5+
use std::path::Path;
6+
7+
use crate::iter_header::*;
8+
use crate::walk::*;
9+
10+
// Should be kept in sync with `CompareMode` in `src/tools/compiletest/src/common.rs`,
11+
// as well as `run`.
12+
const IGNORES: &[&str] = &[
13+
"polonius",
14+
"chalk",
15+
"split-dwarf",
16+
"split-dwarf-single",
17+
"next-solver-coherence",
18+
"next-solver",
19+
"run",
20+
];
21+
const EXTENSIONS: &[&str] = &["stdout", "stderr"];
22+
const SPECIAL_TEST: &str = "tests/ui/command/need-crate-arg-ignore-tidy.x.rs";
23+
24+
pub 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 contents = std::fs::read_to_string(test).unwrap();
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.to_str().map(|s| s.split_once('.')).flatten() else {
88+
continue;
89+
};
90+
91+
test_info.insert(test_name.to_string(), (test, expected_revisions));
92+
}
93+
94+
// Our test file `foo.rs` has specified no revisions. There should not be any
95+
// `foo.rev{.stderr,.stdout}` files. rustc-dev-guide says test output files can have names
96+
// of the form: `test-name.revision.compare_mode.extension`, but our only concern is
97+
// `test-name.revision` and `extension`.
98+
for sibling in files_under_inspection.iter().filter(|f| {
99+
f.extension().map(OsStr::to_str).flatten().is_some_and(|ext| EXTENSIONS.contains(&ext))
100+
}) {
101+
let filename_components = sibling.to_str().unwrap().split('.').collect::<Vec<_>>();
102+
let file_prefix = filename_components[0];
103+
104+
let Some((test_path, expected_revisions)) = test_info.get(file_prefix) else {
105+
continue;
106+
};
107+
108+
match filename_components[..] {
109+
// Cannot have a revision component, skip.
110+
[] | [_] => return,
111+
[_, _] if !expected_revisions.is_empty() => {
112+
// Found unrevisioned output files for a revisioned test.
113+
tidy_error!(
114+
bad,
115+
"found unrevisioned output file `{}` for a revisioned test `{}`",
116+
sibling.display(),
117+
test_path.display(),
118+
);
119+
}
120+
[_, _] => return,
121+
[_, found_revision, .., extension] => {
122+
if !IGNORES.contains(&found_revision)
123+
&& !expected_revisions.contains(found_revision)
124+
// This is from `//@ stderr-per-bitwidth`
125+
&& !(extension == "stderr" && ["32bit", "64bit"].contains(&found_revision))
126+
{
127+
// Found some unexpected revision-esque component that is not a known
128+
// compare-mode or expected revision.
129+
tidy_error!(
130+
bad,
131+
"found output file `{}` for unexpected revision `{}` of test `{}`",
132+
sibling.display(),
133+
found_revision,
134+
test_path.display()
135+
);
136+
}
137+
}
138+
}
139+
}
140+
});
141+
}
142+
143+
fn filter(path: &Path) -> bool {
144+
filter_dirs(path) // ignore certain dirs
145+
|| (path.file_name().is_some_and(|name| name == "auxiliary")) // ignore auxiliary folder
146+
}

Diff for: src/tools/tidy/src/walk.rs

+17
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,20 @@ pub(crate) fn walk_no_read(
8686
}
8787
}
8888
}
89+
90+
// Walk through directories and skip symlinks.
91+
pub(crate) fn walk_dir(
92+
path: &Path,
93+
skip: impl Send + Sync + 'static + Fn(&Path) -> bool,
94+
f: &mut dyn FnMut(&DirEntry),
95+
) {
96+
let mut walker = ignore::WalkBuilder::new(path);
97+
let walker = walker.filter_entry(move |e| !skip(e.path()));
98+
for entry in walker.build() {
99+
if let Ok(entry) = entry {
100+
if entry.path().is_dir() {
101+
f(&entry);
102+
}
103+
}
104+
}
105+
}

Diff for: tests/ui/associated-type-bounds/return-type-notation/bad-inputs-and-output.current.stderr

-48
This file was deleted.

Diff for: tests/ui/associated-type-bounds/return-type-notation/bad-inputs-and-output.next.stderr

-48
This file was deleted.

Diff for: tests/ui/associated-type-bounds/return-type-notation/basic.current_with.stderr

-11
This file was deleted.

0 commit comments

Comments
 (0)