Skip to content

Commit 3972c10

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

File tree

2 files changed

+242
-18
lines changed

2 files changed

+242
-18
lines changed

src/lib.rs

Lines changed: 229 additions & 17 deletions
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,208 @@ 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 write_under_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 single_line = true;
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, _)| single_line &= 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+
single_line &= 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+
single_line &= !txt.trim_end().ends_with(b"{") && !txt.contains_str(b"//");
607+
608+
match &*print_msgs {
609+
[] => {
610+
write_under_matcher = false;
611+
buf.extend(txt)
612+
}
613+
[(_, _, code, rev)]
614+
if single_line && txt.len() + code.len() + rev.len() < 65 =>
615+
{
616+
write_under_matcher = false;
617+
let (txt, end): (_, &[u8]) = if let Some(txt) = txt.strip_suffix(b"\r\n") {
618+
(txt, b"\r\n")
619+
} else if let Some(txt) = txt.strip_suffix(b"\n") {
620+
(txt, b"\n")
621+
} else {
622+
(txt, &[])
623+
};
624+
625+
buf.extend(txt);
626+
buf.extend(b" //~");
627+
if !rev.is_empty() {
628+
buf.push(b'[');
629+
buf.extend(rev.as_bytes());
630+
buf.push(b']');
631+
}
632+
buf.push(b' ');
633+
buf.extend(code.as_bytes());
634+
buf.extend(end);
635+
}
636+
[..] => {
637+
if single_line {
638+
buf.extend(txt);
639+
write_under_matcher = true;
640+
if !buf.ends_with(b"\n") {
641+
buf.push(b'\n');
642+
}
643+
}
644+
let indent = &txt[..txt
645+
.iter()
646+
.position(|x| !matches!(x, b' ' | b'\t'))
647+
.unwrap_or(txt.len())];
648+
let end: &[u8] = if txt.ends_with(b"\r\n") {
649+
b"\r\n"
650+
} else {
651+
b"\n"
652+
};
653+
if !single_line && write_under_matcher {
654+
write_under_matcher = false;
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 (single_line, msg_num) {
669+
(true, 1) => b'^',
670+
(false, 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 !single_line {
680+
buf.extend(txt);
681+
}
682+
}
683+
}
684+
}
685+
}
686+
}
687+
688+
let _ = std::fs::write(file_path, buf);
482689
}
483690

484691
fn parse_comments(
@@ -635,7 +842,7 @@ impl dyn TestStatus {
635842
build_manager: &BuildManager<'_>,
636843
config: &Config,
637844
comments: &Comments,
638-
) -> TestResult {
845+
) -> Result<(TestOk, Option<WriteBack>), Errored> {
639846
let path = self.path();
640847
let revision = self.revision();
641848

@@ -669,7 +876,7 @@ impl dyn TestStatus {
669876
let (cmd, status, stderr, stdout) = self.run_command(cmd)?;
670877

671878
let mode = comments.mode(revision)?;
672-
let cmd = check_test_result(
879+
let (cmd, write_back) = check_test_result(
673880
cmd,
674881
match *mode {
675882
Mode::Run { .. } => Mode::Pass,
@@ -685,13 +892,14 @@ impl dyn TestStatus {
685892
)?;
686893

687894
if let Mode::Run { .. } = *mode {
688-
return run_test_binary(mode, path, revision, comments, cmd, &config);
895+
return run_test_binary(mode, path, revision, comments, cmd, &config)
896+
.map(|x| (x, None));
689897
}
690898

691899
run_rustfix(
692900
&stderr, &stdout, path, comments, revision, &config, *mode, extra_args,
693901
)?;
694-
Ok(TestOk::Ok)
902+
Ok((TestOk::Ok, write_back))
695903
}
696904

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

8511059
let global_rustfix = match mode {
8521060
Mode::Pass | Mode::Run { .. } | Mode::Panic => RustfixMode::Disabled,
853-
Mode::Fail { rustfix, .. } | Mode::Yolo { rustfix } => rustfix,
1061+
Mode::Fail { rustfix, .. } | Mode::Yolo { rustfix, .. } => rustfix,
8541062
};
8551063

8561064
let fixed_code = (no_run_rustfix.is_none() && global_rustfix.enabled())
@@ -1009,7 +1217,7 @@ fn check_test_result(
10091217
status: ExitStatus,
10101218
stdout: &[u8],
10111219
stderr: &[u8],
1012-
) -> Result<Command, Errored> {
1220+
) -> Result<(Command, Option<WriteBack>), Errored> {
10131221
let mut errors = vec![];
10141222
errors.extend(mode.ok(status).err());
10151223
// Always remove annotation comments from stderr.
@@ -1024,7 +1232,7 @@ fn check_test_result(
10241232
&diagnostics.rendered,
10251233
);
10261234
// Check error annotations in the source against output
1027-
check_annotations(
1235+
let write_back = check_annotations(
10281236
diagnostics.messages,
10291237
diagnostics.messages_from_unknown_file_or_line,
10301238
path,
@@ -1033,7 +1241,7 @@ fn check_test_result(
10331241
comments,
10341242
)?;
10351243
if errors.is_empty() {
1036-
Ok(command)
1244+
Ok((command, write_back))
10371245
} else {
10381246
Err(Errored {
10391247
command,
@@ -1066,7 +1274,7 @@ fn check_annotations(
10661274
errors: &mut Errors,
10671275
revision: &str,
10681276
comments: &Comments,
1069-
) -> Result<(), Errored> {
1277+
) -> Result<Option<WriteBack>, Errored> {
10701278
let error_patterns = comments
10711279
.for_revision(revision)
10721280
.flat_map(|r| r.error_in_other_files.iter());
@@ -1177,7 +1385,9 @@ fn check_annotations(
11771385

11781386
let mode = comments.mode(revision)?;
11791387

1180-
if !matches!(*mode, Mode::Yolo { .. }) {
1388+
let write_back = if let Mode::Yolo { write_back, .. } = *mode {
1389+
write_back.map(|level| WriteBack { level, messages })
1390+
} else {
11811391
let messages_from_unknown_file_or_line = filter(messages_from_unknown_file_or_line);
11821392
if !messages_from_unknown_file_or_line.is_empty() {
11831393
errors.push(Error::ErrorsWithoutPattern {
@@ -1202,7 +1412,9 @@ fn check_annotations(
12021412
});
12031413
}
12041414
}
1205-
}
1415+
1416+
None
1417+
};
12061418

12071419
match (*mode, seen_error_match) {
12081420
(Mode::Pass, Some(span)) | (Mode::Panic, Some(span)) => {
@@ -1220,7 +1432,7 @@ fn check_annotations(
12201432
) => errors.push(Error::NoPatternsFound),
12211433
_ => {}
12221434
}
1223-
Ok(())
1435+
Ok(write_back)
12241436
}
12251437

12261438
fn check_output(

src/mode.rs

Lines changed: 13 additions & 1 deletion
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)