Skip to content

Commit fd900d8

Browse files
committed
Add option to write missing error annotations back to the test file.
1 parent 5a59808 commit fd900d8

File tree

2 files changed

+243
-18
lines changed

2 files changed

+243
-18
lines changed

src/lib.rs

+230-17
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,12 @@ struct TestRun {
228228
status: Box<dyn status_emitter::TestStatus>,
229229
}
230230

231+
#[derive(Debug)]
232+
struct WriteBack {
233+
level: WriteBackLevel,
234+
messages: Vec<Vec<Message>>,
235+
}
236+
231237
/// A version of `run_tests` that allows more fine-grained control over running tests.
232238
///
233239
/// All `configs` are being run in parallel.
@@ -319,7 +325,7 @@ pub fn run_tests_generic(
319325
let mut config = config.clone();
320326
per_file_config(&mut config, path, &file_contents);
321327
let result = match std::panic::catch_unwind(|| {
322-
parse_and_test_file(&build_manager, &status, config, file_contents)
328+
parse_and_test_file(&build_manager, &status, config, file_contents, path)
323329
}) {
324330
Ok(Ok(res)) => res,
325331
Ok(Err(err)) => {
@@ -438,6 +444,7 @@ fn parse_and_test_file(
438444
status: &dyn TestStatus,
439445
mut config: Config,
440446
file_contents: Vec<u8>,
447+
file_path: &Path,
441448
) -> Result<Vec<TestRun>, Errored> {
442449
let comments = parse_comments(
443450
&file_contents,
@@ -448,7 +455,9 @@ fn parse_and_test_file(
448455
// Run the test for all revisions
449456
let revisions = comments.revisions.as_deref().unwrap_or(EMPTY);
450457
let mut built_deps = false;
451-
Ok(revisions
458+
let mut write_backs = Vec::new();
459+
let mut success = true;
460+
let results: Vec<_> = revisions
452461
.iter()
453462
.map(|revision| {
454463
let status = status.for_revision(revision);
@@ -475,10 +484,209 @@ fn parse_and_test_file(
475484
built_deps = true;
476485
}
477486

478-
let result = status.run_test(build_manager, &config, &comments);
479-
TestRun { result, status }
487+
match status.run_test(build_manager, &config, &comments) {
488+
Ok((result, Some(write_back))) => {
489+
write_backs.push((&**revision, write_back));
490+
TestRun {
491+
result: Ok(result),
492+
status,
493+
}
494+
}
495+
Ok((result, None)) => TestRun {
496+
result: Ok(result),
497+
status,
498+
},
499+
Err(e) => {
500+
success = false;
501+
TestRun {
502+
result: Err(e),
503+
status,
504+
}
505+
}
506+
}
480507
})
481-
.collect())
508+
.collect();
509+
510+
if success && !write_backs.is_empty() {
511+
write_back_annotations(file_path, &file_contents, &comments, &write_backs);
512+
}
513+
514+
Ok(results)
515+
}
516+
517+
fn write_back_annotations(
518+
file_path: &Path,
519+
file_contents: &[u8],
520+
comments: &Comments,
521+
write_backs: &[(&str, WriteBack)],
522+
) {
523+
let mut buf = Vec::<u8>::with_capacity(file_contents.len() * 2);
524+
let (first_rev, revs) = write_backs.split_first().unwrap();
525+
let mut counters = Vec::new();
526+
let mut print_msgs = Vec::new();
527+
let prefix = comments
528+
.base_immut()
529+
.diagnostic_code_prefix
530+
.as_ref()
531+
.map_or("", |x| x.as_str());
532+
let mut skip_line_before_over_matcher = false;
533+
534+
match first_rev.1.level {
535+
WriteBackLevel::Code => {
536+
for (line, txt) in file_contents.lines_with_terminator().enumerate() {
537+
let mut use_over_matcher = false;
538+
let first_msgs: &[Message] =
539+
first_rev.1.messages.get(line + 1).map_or(&[], |m| &**m);
540+
541+
print_msgs.clear();
542+
print_msgs.extend(
543+
first_msgs
544+
.iter()
545+
.filter(|m| m.level == Level::Error)
546+
.filter_map(|m| {
547+
m.line_col
548+
.as_ref()
549+
.zip(m.code.as_deref().and_then(|c| c.strip_prefix(prefix)))
550+
})
551+
.inspect(|(span, _)| use_over_matcher |= span.line_start != span.line_end)
552+
.enumerate()
553+
.map(|(i, (span, code))| (i, span, code, first_rev.0)),
554+
);
555+
counters.clear();
556+
counters.resize(print_msgs.len(), 0);
557+
558+
for rev in revs {
559+
let msgs: &[Message] = rev.1.messages.get(line + 1).map_or(&[], |m| &**m);
560+
561+
for (span, code) in
562+
msgs.iter()
563+
.filter(|m| m.level == Level::Error)
564+
.filter_map(|m| {
565+
m.line_col
566+
.as_ref()
567+
.zip(m.code.as_deref().and_then(|c| c.strip_prefix(prefix)))
568+
})
569+
{
570+
let i = if let Some(&(i, ..)) = print_msgs[..counters.len()].iter().find(
571+
|&&(_, prev_span, prev_code, _)| span == prev_span && code == prev_code,
572+
) {
573+
counters[i] += 1;
574+
i
575+
} else {
576+
use_over_matcher |= span.line_start != span.line_end;
577+
usize::MAX
578+
};
579+
print_msgs.push((i, span, code, rev.0));
580+
}
581+
}
582+
583+
// partition the first revision's messages
584+
// in all revisions => only some revisions
585+
let mut i = 0;
586+
let mut j = counters.len();
587+
while i < j {
588+
if counters[i] == revs.len() {
589+
print_msgs[i].3 = "";
590+
i += 1;
591+
} else if counters[j - 1] == revs.len() {
592+
print_msgs.swap(i, j - 1);
593+
print_msgs[i].3 = "";
594+
i += 1;
595+
j -= 1;
596+
} else {
597+
j -= 1;
598+
}
599+
}
600+
// For all other revision's messages, remove the ones that exist in all revisions.
601+
print_msgs.retain(|&(i, _, _, rev)| {
602+
rev.is_empty() || counters.get(i).map_or(true, |&x| x != revs.len())
603+
});
604+
605+
// rustfmt behaves poorly when putting a comment underneath in these cases.
606+
use_over_matcher |= txt.trim_end().ends_with(b"{") || txt.contains_str(b"//");
607+
608+
match &*print_msgs {
609+
[] => {
610+
skip_line_before_over_matcher =
611+
!txt.trim_start().starts_with(b"//") && txt.contains_str(b"//");
612+
buf.extend(txt)
613+
}
614+
[(_, _, code, rev)]
615+
if !use_over_matcher && txt.len() + code.len() + rev.len() < 65 =>
616+
{
617+
skip_line_before_over_matcher = true;
618+
let (txt, end): (_, &[u8]) = if let Some(txt) = txt.strip_suffix(b"\r\n") {
619+
(txt, b"\r\n")
620+
} else if let Some(txt) = txt.strip_suffix(b"\n") {
621+
(txt, b"\n")
622+
} else {
623+
(txt, &[])
624+
};
625+
626+
buf.extend(txt);
627+
buf.extend(b" //~");
628+
if !rev.is_empty() {
629+
buf.push(b'[');
630+
buf.extend(rev.as_bytes());
631+
buf.push(b']');
632+
}
633+
buf.push(b' ');
634+
buf.extend(code.as_bytes());
635+
buf.extend(end);
636+
}
637+
[..] => {
638+
if !use_over_matcher {
639+
buf.extend(txt);
640+
skip_line_before_over_matcher = true;
641+
if !buf.ends_with(b"\n") {
642+
buf.push(b'\n');
643+
}
644+
}
645+
let indent = &txt[..txt
646+
.iter()
647+
.position(|x| !matches!(x, b' ' | b'\t'))
648+
.unwrap_or(txt.len())];
649+
let end: &[u8] = if txt.ends_with(b"\r\n") {
650+
b"\r\n"
651+
} else {
652+
b"\n"
653+
};
654+
if use_over_matcher && skip_line_before_over_matcher {
655+
buf.extend(end);
656+
}
657+
658+
let mut msg_num = 1;
659+
let msg_end = print_msgs.len();
660+
for (_, _, code, rev) in &print_msgs {
661+
buf.extend(indent);
662+
buf.extend(b"//~");
663+
if !rev.is_empty() {
664+
buf.push(b'[');
665+
buf.extend(rev.as_bytes());
666+
buf.push(b']');
667+
}
668+
buf.push(match (use_over_matcher, msg_num) {
669+
(false, 1) => b'^',
670+
(true, x) if x == msg_end => b'v',
671+
_ => b'|',
672+
});
673+
buf.push(b' ');
674+
buf.extend(code.as_bytes());
675+
buf.extend(end);
676+
msg_num += 1;
677+
}
678+
679+
if use_over_matcher {
680+
skip_line_before_over_matcher = false;
681+
buf.extend(txt);
682+
}
683+
}
684+
}
685+
}
686+
}
687+
}
688+
689+
let _ = std::fs::write(file_path, buf);
482690
}
483691

484692
fn parse_comments(
@@ -635,7 +843,7 @@ impl dyn TestStatus {
635843
build_manager: &BuildManager<'_>,
636844
config: &Config,
637845
comments: &Comments,
638-
) -> TestResult {
846+
) -> Result<(TestOk, Option<WriteBack>), Errored> {
639847
let path = self.path();
640848
let revision = self.revision();
641849

@@ -669,7 +877,7 @@ impl dyn TestStatus {
669877
let (cmd, status, stderr, stdout) = self.run_command(cmd)?;
670878

671879
let mode = comments.mode(revision)?;
672-
let cmd = check_test_result(
880+
let (cmd, write_back) = check_test_result(
673881
cmd,
674882
match *mode {
675883
Mode::Run { .. } => Mode::Pass,
@@ -685,13 +893,14 @@ impl dyn TestStatus {
685893
)?;
686894

687895
if let Mode::Run { .. } = *mode {
688-
return run_test_binary(mode, path, revision, comments, cmd, &config);
896+
return run_test_binary(mode, path, revision, comments, cmd, &config)
897+
.map(|x| (x, None));
689898
}
690899

691900
run_rustfix(
692901
&stderr, &stdout, path, comments, revision, &config, *mode, extra_args,
693902
)?;
694-
Ok(TestOk::Ok)
903+
Ok((TestOk::Ok, write_back))
695904
}
696905

697906
/// Run a command, and if it takes more than 100ms, start appending the last stderr/stdout
@@ -850,7 +1059,7 @@ fn run_rustfix(
8501059

8511060
let global_rustfix = match mode {
8521061
Mode::Pass | Mode::Run { .. } | Mode::Panic => RustfixMode::Disabled,
853-
Mode::Fail { rustfix, .. } | Mode::Yolo { rustfix } => rustfix,
1062+
Mode::Fail { rustfix, .. } | Mode::Yolo { rustfix, .. } => rustfix,
8541063
};
8551064

8561065
let fixed_code = (no_run_rustfix.is_none() && global_rustfix.enabled())
@@ -1009,7 +1218,7 @@ fn check_test_result(
10091218
status: ExitStatus,
10101219
stdout: &[u8],
10111220
stderr: &[u8],
1012-
) -> Result<Command, Errored> {
1221+
) -> Result<(Command, Option<WriteBack>), Errored> {
10131222
let mut errors = vec![];
10141223
errors.extend(mode.ok(status).err());
10151224
// Always remove annotation comments from stderr.
@@ -1024,7 +1233,7 @@ fn check_test_result(
10241233
&diagnostics.rendered,
10251234
);
10261235
// Check error annotations in the source against output
1027-
check_annotations(
1236+
let write_back = check_annotations(
10281237
diagnostics.messages,
10291238
diagnostics.messages_from_unknown_file_or_line,
10301239
path,
@@ -1033,7 +1242,7 @@ fn check_test_result(
10331242
comments,
10341243
)?;
10351244
if errors.is_empty() {
1036-
Ok(command)
1245+
Ok((command, write_back))
10371246
} else {
10381247
Err(Errored {
10391248
command,
@@ -1066,7 +1275,7 @@ fn check_annotations(
10661275
errors: &mut Errors,
10671276
revision: &str,
10681277
comments: &Comments,
1069-
) -> Result<(), Errored> {
1278+
) -> Result<Option<WriteBack>, Errored> {
10701279
let error_patterns = comments
10711280
.for_revision(revision)
10721281
.flat_map(|r| r.error_in_other_files.iter());
@@ -1177,7 +1386,9 @@ fn check_annotations(
11771386

11781387
let mode = comments.mode(revision)?;
11791388

1180-
if !matches!(*mode, Mode::Yolo { .. }) {
1389+
let write_back = if let Mode::Yolo { write_back, .. } = *mode {
1390+
write_back.map(|level| WriteBack { level, messages })
1391+
} else {
11811392
let messages_from_unknown_file_or_line = filter(messages_from_unknown_file_or_line);
11821393
if !messages_from_unknown_file_or_line.is_empty() {
11831394
errors.push(Error::ErrorsWithoutPattern {
@@ -1202,7 +1413,9 @@ fn check_annotations(
12021413
});
12031414
}
12041415
}
1205-
}
1416+
1417+
None
1418+
};
12061419

12071420
match (*mode, seen_error_match) {
12081421
(Mode::Pass, Some(span)) | (Mode::Panic, Some(span)) => {
@@ -1220,7 +1433,7 @@ fn check_annotations(
12201433
) => errors.push(Error::NoPatternsFound),
12211434
_ => {}
12221435
}
1223-
Ok(())
1436+
Ok(write_back)
12241437
}
12251438

12261439
fn check_output(

src/mode.rs

+13-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ impl RustfixMode {
1919
}
2020
}
2121

22+
/// What kind of annotations to write back to the test file.
23+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
24+
pub enum WriteBackLevel {
25+
/// Write annotations only with a diagnostic code.
26+
Code,
27+
}
28+
2229
#[derive(Copy, Clone, Debug)]
2330
/// Decides what is expected of each test's exit status.
2431
pub enum Mode {
@@ -42,6 +49,8 @@ pub enum Mode {
4249
Yolo {
4350
/// When to run rustfix on the test
4451
rustfix: RustfixMode,
52+
/// Whether to write back missing annotations to the test file.
53+
write_back: Option<WriteBackLevel>,
4554
},
4655
}
4756

@@ -77,7 +86,10 @@ impl Display for Mode {
7786
require_patterns: _,
7887
rustfix: _,
7988
} => write!(f, "fail"),
80-
Mode::Yolo { rustfix: _ } => write!(f, "yolo"),
89+
Mode::Yolo {
90+
rustfix: _,
91+
write_back: _,
92+
} => write!(f, "yolo"),
8193
}
8294
}
8395
}

0 commit comments

Comments
 (0)