Skip to content

Commit 3f22df6

Browse files
committed
compiletest: Improve diagnostics for line annotation mismatches
1 parent ea34650 commit 3f22df6

File tree

6 files changed

+213
-75
lines changed

6 files changed

+213
-75
lines changed

src/tools/compiletest/src/errors.rs

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub enum ErrorKind {
1616
Suggestion,
1717
Warning,
1818
Raw,
19+
/// Used for better recovery and diagnostics in compiletest.
20+
Unknown,
1921
}
2022

2123
impl ErrorKind {
@@ -31,21 +33,25 @@ impl ErrorKind {
3133

3234
/// Either the canonical uppercase string, or some additional versions for compatibility.
3335
/// FIXME: consider keeping only the canonical versions here.
34-
pub fn from_user_str(s: &str) -> ErrorKind {
35-
match s {
36+
fn from_user_str(s: &str) -> Option<ErrorKind> {
37+
Some(match s {
3638
"HELP" | "help" => ErrorKind::Help,
3739
"ERROR" | "error" => ErrorKind::Error,
38-
// `MONO_ITEM` makes annotations in `codegen-units` tests syntactically correct,
39-
// but those tests never use the error kind later on.
40-
"NOTE" | "note" | "MONO_ITEM" => ErrorKind::Note,
40+
"NOTE" | "note" => ErrorKind::Note,
4141
"SUGGESTION" => ErrorKind::Suggestion,
4242
"WARN" | "WARNING" | "warn" | "warning" => ErrorKind::Warning,
4343
"RAW" => ErrorKind::Raw,
44-
_ => panic!(
44+
_ => return None,
45+
})
46+
}
47+
48+
pub fn expect_from_user_str(s: &str) -> ErrorKind {
49+
ErrorKind::from_user_str(s).unwrap_or_else(|| {
50+
panic!(
4551
"unexpected diagnostic kind `{s}`, expected \
46-
`ERROR`, `WARN`, `NOTE`, `HELP` or `SUGGESTION`"
47-
),
48-
}
52+
`ERROR`, `WARN`, `NOTE`, `HELP`, `SUGGESTION` or `RAW`"
53+
)
54+
})
4955
}
5056
}
5157

@@ -58,13 +64,15 @@ impl fmt::Display for ErrorKind {
5864
ErrorKind::Suggestion => write!(f, "SUGGESTION"),
5965
ErrorKind::Warning => write!(f, "WARN"),
6066
ErrorKind::Raw => write!(f, "RAW"),
67+
ErrorKind::Unknown => write!(f, "UNKNOWN"),
6168
}
6269
}
6370
}
6471

6572
#[derive(Debug)]
6673
pub struct Error {
6774
pub line_num: Option<usize>,
75+
pub column_num: Option<usize>,
6876
/// What kind of message we expect (e.g., warning, error, suggestion).
6977
pub kind: ErrorKind,
7078
pub msg: String,
@@ -74,17 +82,6 @@ pub struct Error {
7482
pub require_annotation: bool,
7583
}
7684

77-
impl Error {
78-
pub fn render_for_expected(&self) -> String {
79-
use colored::Colorize;
80-
format!("{: <10}line {: >3}: {}", self.kind, self.line_num_str(), self.msg.cyan())
81-
}
82-
83-
pub fn line_num_str(&self) -> String {
84-
self.line_num.map_or("?".to_string(), |line_num| line_num.to_string())
85-
}
86-
}
87-
8885
/// Looks for either "//~| KIND MESSAGE" or "//~^^... KIND MESSAGE"
8986
/// The former is a "follow" that inherits its target from the preceding line;
9087
/// the latter is an "adjusts" that goes that many lines up.
@@ -168,8 +165,10 @@ fn parse_expected(
168165
let rest = line[tag.end()..].trim_start();
169166
let (kind_str, _) =
170167
rest.split_once(|c: char| c != '_' && !c.is_ascii_alphabetic()).unwrap_or((rest, ""));
171-
let kind = ErrorKind::from_user_str(kind_str);
172-
let untrimmed_msg = &rest[kind_str.len()..];
168+
let (kind, untrimmed_msg) = match ErrorKind::from_user_str(kind_str) {
169+
Some(kind) => (kind, &rest[kind_str.len()..]),
170+
None => (ErrorKind::Unknown, rest),
171+
};
173172
let msg = untrimmed_msg.strip_prefix(':').unwrap_or(untrimmed_msg).trim().to_owned();
174173

175174
let line_num_adjust = &captures["adjust"];
@@ -182,6 +181,7 @@ fn parse_expected(
182181
} else {
183182
(false, Some(line_num - line_num_adjust.len()))
184183
};
184+
let column_num = Some(tag.start() + 1);
185185

186186
debug!(
187187
"line={:?} tag={:?} follow_prev={:?} kind={:?} msg={:?}",
@@ -191,7 +191,7 @@ fn parse_expected(
191191
kind,
192192
msg
193193
);
194-
Some((follow_prev, Error { line_num, kind, msg, require_annotation: true }))
194+
Some((follow_prev, Error { line_num, column_num, kind, msg, require_annotation: true }))
195195
}
196196

197197
#[cfg(test)]

src/tools/compiletest/src/header.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ impl TestProps {
593593
config.parse_name_value_directive(ln, DONT_REQUIRE_ANNOTATIONS)
594594
{
595595
self.dont_require_annotations
596-
.insert(ErrorKind::from_user_str(err_kind.trim()));
596+
.insert(ErrorKind::expect_from_user_str(err_kind.trim()));
597597
}
598598
},
599599
);

src/tools/compiletest/src/json.rs

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ struct UnusedExternNotification {
3636
struct DiagnosticSpan {
3737
file_name: String,
3838
line_start: usize,
39-
line_end: usize,
4039
column_start: usize,
41-
column_end: usize,
4240
is_primary: bool,
4341
label: Option<String>,
4442
suggested_replacement: Option<String>,
@@ -148,6 +146,7 @@ pub fn parse_output(file_name: &str, output: &str) -> Vec<Error> {
148146
Ok(diagnostic) => push_actual_errors(&mut errors, &diagnostic, &[], file_name),
149147
Err(_) => errors.push(Error {
150148
line_num: None,
149+
column_num: None,
151150
kind: ErrorKind::Raw,
152151
msg: line.to_string(),
153152
require_annotation: false,
@@ -193,25 +192,9 @@ fn push_actual_errors(
193192
// also ensure that `//~ ERROR E123` *always* works. The
194193
// assumption is that these multi-line error messages are on their
195194
// way out anyhow.
196-
let with_code = |span: Option<&DiagnosticSpan>, text: &str| {
197-
// FIXME(#33000) -- it'd be better to use a dedicated
198-
// UI harness than to include the line/col number like
199-
// this, but some current tests rely on it.
200-
//
201-
// Note: Do NOT include the filename. These can easily
202-
// cause false matches where the expected message
203-
// appears in the filename, and hence the message
204-
// changes but the test still passes.
205-
let span_str = match span {
206-
Some(DiagnosticSpan { line_start, column_start, line_end, column_end, .. }) => {
207-
format!("{line_start}:{column_start}: {line_end}:{column_end}")
208-
}
209-
None => format!("?:?: ?:?"),
210-
};
211-
match &diagnostic.code {
212-
Some(code) => format!("{span_str}: {text} [{}]", code.code),
213-
None => format!("{span_str}: {text}"),
214-
}
195+
let with_code = |text| match &diagnostic.code {
196+
Some(code) => format!("{text} [{}]", code.code),
197+
None => format!("{text}"),
215198
};
216199

217200
// Convert multi-line messages into multiple errors.
@@ -225,17 +208,19 @@ fn push_actual_errors(
225208
|| Regex::new(r"aborting due to \d+ previous errors?|\d+ warnings? emitted").unwrap();
226209
errors.push(Error {
227210
line_num: None,
211+
column_num: None,
228212
kind,
229-
msg: with_code(None, first_line),
213+
msg: with_code(first_line),
230214
require_annotation: diagnostic.level != "failure-note"
231215
&& !RE.get_or_init(re_init).is_match(first_line),
232216
});
233217
} else {
234218
for span in primary_spans {
235219
errors.push(Error {
236220
line_num: Some(span.line_start),
221+
column_num: Some(span.column_start),
237222
kind,
238-
msg: with_code(Some(span), first_line),
223+
msg: with_code(first_line),
239224
require_annotation: true,
240225
});
241226
}
@@ -244,16 +229,18 @@ fn push_actual_errors(
244229
if primary_spans.is_empty() {
245230
errors.push(Error {
246231
line_num: None,
232+
column_num: None,
247233
kind,
248-
msg: with_code(None, next_line),
234+
msg: with_code(next_line),
249235
require_annotation: false,
250236
});
251237
} else {
252238
for span in primary_spans {
253239
errors.push(Error {
254240
line_num: Some(span.line_start),
241+
column_num: Some(span.column_start),
255242
kind,
256-
msg: with_code(Some(span), next_line),
243+
msg: with_code(next_line),
257244
require_annotation: false,
258245
});
259246
}
@@ -266,6 +253,7 @@ fn push_actual_errors(
266253
for (index, line) in suggested_replacement.lines().enumerate() {
267254
errors.push(Error {
268255
line_num: Some(span.line_start + index),
256+
column_num: Some(span.column_start),
269257
kind: ErrorKind::Suggestion,
270258
msg: line.to_string(),
271259
// Empty suggestions (suggestions to remove something) are common
@@ -288,6 +276,7 @@ fn push_actual_errors(
288276
if let Some(label) = &span.label {
289277
errors.push(Error {
290278
line_num: Some(span.line_start),
279+
column_num: Some(span.column_start),
291280
kind: ErrorKind::Note,
292281
msg: label.clone(),
293282
// Empty labels (only underlining spans) are common and do not need annotations.
@@ -310,6 +299,7 @@ fn push_backtrace(
310299
if Path::new(&expansion.span.file_name) == Path::new(&file_name) {
311300
errors.push(Error {
312301
line_num: Some(expansion.span.line_start),
302+
column_num: Some(expansion.span.column_start),
313303
kind: ErrorKind::Note,
314304
msg: format!("in this expansion of {}", expansion.macro_decl_name),
315305
require_annotation: true,

src/tools/compiletest/src/runtest.rs

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -677,9 +677,6 @@ impl<'test> TestCx<'test> {
677677
return;
678678
}
679679

680-
// On Windows, translate all '\' path separators to '/'
681-
let file_name = self.testpaths.file.to_string().replace(r"\", "/");
682-
683680
// On Windows, keep all '\' path separators to match the paths reported in the JSON output
684681
// from the compiler
685682
let diagnostic_file_name = if self.props.remap_src_base {
@@ -704,6 +701,7 @@ impl<'test> TestCx<'test> {
704701
.map(|e| Error { msg: self.normalize_output(&e.msg, &[]), ..e });
705702

706703
let mut unexpected = Vec::new();
704+
let mut unimportant = Vec::new();
707705
let mut found = vec![false; expected_errors.len()];
708706
for actual_error in actual_errors {
709707
for pattern in &self.props.error_patterns {
@@ -738,14 +736,9 @@ impl<'test> TestCx<'test> {
738736
&& expected_kinds.contains(&actual_error.kind)
739737
&& !self.props.dont_require_annotations.contains(&actual_error.kind)
740738
{
741-
self.error(&format!(
742-
"{}:{}: unexpected {}: '{}'",
743-
file_name,
744-
actual_error.line_num_str(),
745-
actual_error.kind,
746-
actual_error.msg
747-
));
748739
unexpected.push(actual_error);
740+
} else {
741+
unimportant.push(actual_error);
749742
}
750743
}
751744
}
@@ -755,39 +748,92 @@ impl<'test> TestCx<'test> {
755748
// anything not yet found is a problem
756749
for (index, expected_error) in expected_errors.iter().enumerate() {
757750
if !found[index] {
758-
self.error(&format!(
759-
"{}:{}: expected {} not found: {}",
760-
file_name,
761-
expected_error.line_num_str(),
762-
expected_error.kind,
763-
expected_error.msg
764-
));
765751
not_found.push(expected_error);
766752
}
767753
}
768754

769755
if !unexpected.is_empty() || !not_found.is_empty() {
756+
// Show relative path for brevity, and normalize path separators to `/`.
757+
let file_name = self
758+
.testpaths
759+
.file
760+
.strip_prefix(self.config.src_root.as_str())
761+
.unwrap_or(&self.testpaths.file)
762+
.to_string()
763+
.replace(r"\", "/");
764+
770765
self.error(&format!(
771-
"{} unexpected errors found, {} expected errors not found",
766+
"{} unexpected diagnostics reported, {} expected diagnostics not reported",
772767
unexpected.len(),
773768
not_found.len()
774769
));
775-
println!("status: {}\ncommand: {}\n", proc_res.status, proc_res.cmdline);
770+
let print = |e: &Error| {
771+
let line_num = e.line_num.map_or("?".to_string(), |line_num| line_num.to_string());
772+
// `file:?:NUM` may be confusing to editors and unclickable.
773+
let opt_col_num = match e.column_num {
774+
Some(col_num) if line_num != "?" => format!("{col_num}:"),
775+
_ => "".to_string(),
776+
};
777+
println!("{file_name}:{line_num}:{opt_col_num} {}: {}", e.kind, e.msg.cyan())
778+
};
779+
// Fuzzy matching quality:
780+
// - message and line / message and kind - great, suggested
781+
// - only message - good, suggested
782+
// - known line and kind - ok, suggested
783+
// - only known line - meh, but suggested
784+
// - others are not worth suggesting
776785
if !unexpected.is_empty() {
777-
println!("{}", "--- unexpected errors (from JSON output) ---".green());
786+
println!("{}", "--- reported but not expected (from JSON output) ---".green());
778787
for error in &unexpected {
779-
println!("{}", error.render_for_expected());
788+
print(error);
789+
for candidate in &not_found {
790+
if error.msg.contains(&candidate.msg) {
791+
let prefix = if candidate.line_num != error.line_num {
792+
"expected on a different line"
793+
} else {
794+
"expected with a different kind"
795+
}
796+
.red();
797+
print!(" {prefix}: ");
798+
print(candidate);
799+
} else if candidate.line_num.is_some()
800+
&& candidate.line_num == error.line_num
801+
{
802+
print!(" {}: ", "expected with a different message".red());
803+
print(candidate);
804+
}
805+
}
780806
}
781807
println!("{}", "---".green());
782808
}
783809
if !not_found.is_empty() {
784-
println!("{}", "--- not found errors (from test file) ---".red());
810+
println!("{}", "--- expected but not reported (from test file) ---".red());
785811
for error in &not_found {
786-
println!("{}", error.render_for_expected());
812+
print(error);
813+
for candidate in unexpected.iter().chain(&unimportant) {
814+
if candidate.msg.contains(&error.msg) {
815+
let prefix = if candidate.line_num != error.line_num {
816+
"reported on a different line"
817+
} else {
818+
"reported with a different kind"
819+
}
820+
.green();
821+
print!(" {prefix}: ");
822+
print(candidate);
823+
} else if candidate.line_num.is_some()
824+
&& candidate.line_num == error.line_num
825+
{
826+
print!(" {}: ", "reported with a different message".green());
827+
print(candidate);
828+
}
829+
}
787830
}
788-
println!("{}", "---\n".red());
831+
println!("{}", "---".red());
789832
}
790-
panic!("errors differ from expected");
833+
panic!(
834+
"errors differ from expected\nstatus: {}\ncommand: {}\n",
835+
proc_res.status, proc_res.cmdline
836+
);
791837
}
792838
}
793839

@@ -2073,7 +2119,6 @@ impl<'test> TestCx<'test> {
20732119
println!("{}", String::from_utf8_lossy(&output.stdout));
20742120
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
20752121
} else {
2076-
use colored::Colorize;
20772122
eprintln!("warning: no pager configured, falling back to unified diff");
20782123
eprintln!(
20792124
"help: try configuring a git pager (e.g. `delta`) with `git config --global core.pager delta`"

0 commit comments

Comments
 (0)