Skip to content

Commit 8f873a6

Browse files
committed
Error on stray .stderr/.stdout files for (un-)revisioned tests
1 parent f62f490 commit 8f873a6

File tree

5 files changed

+161
-24
lines changed

5 files changed

+161
-24
lines changed

src/tools/tidy/src/iter_header.rs

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
if ln.starts_with(COMMENT) && ln[COMMENT.len()..].trim_start().starts_with('[') {
17+
if let Some(close_brace) = ln.find(']') {
18+
let open_brace = ln.find('[').unwrap();
19+
let revision = &ln[open_brace + 1..close_brace];
20+
it(HeaderLine {
21+
revision: Some(revision),
22+
directive: ln[(close_brace + 1)..].trim_start(),
23+
});
24+
} else {
25+
panic!("malformed condition directive: expected `//@[foo]`, found `{ln}`")
26+
}
27+
} else if ln.starts_with(COMMENT) {
28+
it(HeaderLine { revision: None, directive: ln[COMMENT.len()..].trim_start() });
29+
}
30+
}
31+
}

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;

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);

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,123 @@
1+
//! Checks that there are no unpaired `.stderr` or `.stdout` for a test with and without revisions.
2+
3+
#![allow(unused_variables, dead_code)]
4+
use std::collections::BTreeSet;
5+
use std::ffi::OsStr;
6+
use std::path::Path;
7+
8+
use walkdir::WalkDir;
9+
10+
use crate::iter_header::*;
11+
use crate::walk::*;
12+
13+
// Should be kept in sync with `CompareMode` in `src/tools/compiletest/src/common.rs`,
14+
// as well as `run`.
15+
const IGNORES: &[&str] = &[
16+
"polonius",
17+
"chalk",
18+
"split-dwarf",
19+
"split-dwarf-single",
20+
"next-solver-coherence",
21+
"next-solver",
22+
"run",
23+
];
24+
const EXTENSIONS: &[&str] = &["stdout", "stderr"];
25+
26+
pub fn check(tests_path: impl AsRef<Path>, bad: &mut bool) {
27+
walk(tests_path.as_ref(), filter, &mut |entry, contents| {
28+
let mut expected_revisions = BTreeSet::new();
29+
30+
// Step 1: collect directives.
31+
iter_header(contents, &mut |HeaderLine { revision, directive }| {
32+
// We're trying to *find* `//@ revision: xxx` directives themselves, not revisioned
33+
// directives.
34+
if revision.is_some() {
35+
return;
36+
}
37+
38+
let directive = directive.trim();
39+
40+
if directive.starts_with("revisions") {
41+
let Some((name, value)) = directive.split_once([':', ' ']) else {
42+
return;
43+
};
44+
45+
if name == "revisions" {
46+
let revs = value.split(' ');
47+
for rev in revs {
48+
expected_revisions.insert(rev);
49+
}
50+
}
51+
}
52+
});
53+
54+
// Step 2: seek sibling output files in the same directory as "us" the current `.rs`
55+
// test file (`.stderr` and `.stdout`).
56+
//
57+
// Our test file `foo.rs` has specified no revisions. There should not be any
58+
// `foo.rev{.stderr,.stdout}` files. rustc-dev-guide says test output files can have names
59+
// of the form: `test-name.revision.compare_mode.extension`, but our only concern is
60+
// `test-name.revision` and `extension`.
61+
let walker = WalkDir::new(entry.path().parent().unwrap()).max_depth(1).into_iter();
62+
for sibling in walker.filter_map(|e| e.ok()) {
63+
let Some(ext) = sibling.path().extension().map(OsStr::to_str).flatten() else {
64+
continue;
65+
};
66+
67+
if !sibling.metadata().unwrap().is_file() || !EXTENSIONS.contains(&ext) {
68+
continue;
69+
}
70+
71+
let filename_components =
72+
sibling.path().to_str().unwrap().split('.').collect::<Vec<_>>();
73+
74+
let Some((entry_filename, _)) =
75+
entry.path().to_str().map(|s| s.split_once('.')).flatten()
76+
else {
77+
continue;
78+
};
79+
80+
match filename_components[..] {
81+
// Cannot have a revision component, skip.
82+
[] | [_] => return,
83+
[name, extension] if !expected_revisions.is_empty() => {
84+
if entry_filename == name {
85+
// Found unrevisioned output files for a revisioned test.
86+
tidy_error!(
87+
bad,
88+
"found unrevisioned output file `{}` for a revisioned test `{}`",
89+
sibling.path().display(),
90+
entry.path().display()
91+
);
92+
}
93+
}
94+
[_, _] => return,
95+
[name, found_revision, .., extension] => {
96+
if entry_filename == name
97+
&& !IGNORES.contains(&found_revision)
98+
&& !expected_revisions.contains(found_revision)
99+
// This is from `//@ stderr-per-bitwidth`
100+
&& !(extension == "stderr" && ["32bit", "64bit"].contains(&found_revision))
101+
{
102+
// Found some unexpected revision-esque component that is not a known
103+
// compare-mode or expected revision.
104+
tidy_error!(
105+
bad,
106+
"found output file `{}` for unexpected revision `{}` of test `{}`",
107+
sibling.path().display(),
108+
found_revision,
109+
entry.path().display()
110+
);
111+
}
112+
}
113+
}
114+
}
115+
});
116+
}
117+
118+
fn filter(path: &Path, _is_dir: bool) -> bool {
119+
filter_dirs(path) // ignore dirs
120+
|| filter_not_rust(path) // ignore non .rs files
121+
|| path.file_name().is_some_and(|name| name == "auxiliary") // ignore auxiliary folder
122+
|| path.ends_with("tests/ui/command/need-crate-arg-ignore-tidy.x.rs") // ignore special test
123+
}

0 commit comments

Comments
 (0)