1 | //! Variaous schemes for reporting messages during testing or after testing is done. |
2 | |
3 | use annotate_snippets::{ |
4 | display_list::{DisplayList, FormatOptions}, |
5 | snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}, |
6 | }; |
7 | use bstr::ByteSlice; |
8 | use colored::Colorize; |
9 | use crossbeam_channel::{Sender, TryRecvError}; |
10 | use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; |
11 | |
12 | use crate::{ |
13 | github_actions, |
14 | parser::Pattern, |
15 | rustc_stderr::{Message, Span}, |
16 | Error, Errored, Errors, Format, TestOk, TestResult, |
17 | }; |
18 | use std::{ |
19 | collections::HashMap, |
20 | fmt::{Debug, Display, Write as _}, |
21 | io::Write as _, |
22 | num::NonZeroUsize, |
23 | panic::RefUnwindSafe, |
24 | path::{Path, PathBuf}, |
25 | process::Command, |
26 | sync::atomic::AtomicBool, |
27 | time::Duration, |
28 | }; |
29 | |
30 | /// A generic way to handle the output of this crate. |
31 | pub trait StatusEmitter: Sync + RefUnwindSafe { |
32 | /// Invoked the moment we know a test will later be run. |
33 | /// Useful for progress bars and such. |
34 | fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus>; |
35 | |
36 | /// Create a report about the entire test run at the end. |
37 | #[allow (clippy::type_complexity)] |
38 | fn finalize( |
39 | &self, |
40 | failed: usize, |
41 | succeeded: usize, |
42 | ignored: usize, |
43 | filtered: usize, |
44 | ) -> Box<dyn Summary>; |
45 | } |
46 | |
47 | /// Information about a specific test run. |
48 | pub trait TestStatus: Send + Sync + RefUnwindSafe { |
49 | /// Create a copy of this test for a new revision. |
50 | fn for_revision(&self, revision: &str) -> Box<dyn TestStatus>; |
51 | |
52 | /// Invoked before each failed test prints its errors along with a drop guard that can |
53 | /// gets invoked afterwards. |
54 | fn failed_test<'a>( |
55 | &'a self, |
56 | cmd: &'a Command, |
57 | stderr: &'a [u8], |
58 | stdout: &'a [u8], |
59 | ) -> Box<dyn Debug + 'a>; |
60 | |
61 | /// Change the status of the test while it is running to supply some kind of progress |
62 | fn update_status(&self, msg: String); |
63 | |
64 | /// A test has finished, handle the result immediately. |
65 | fn done(&self, _result: &TestResult) {} |
66 | |
67 | /// The path of the test file. |
68 | fn path(&self) -> &Path; |
69 | |
70 | /// The revision, usually an empty string. |
71 | fn revision(&self) -> &str; |
72 | } |
73 | |
74 | /// Report a summary at the end of a test run. |
75 | pub trait Summary { |
76 | /// A test has finished, handle the result. |
77 | fn test_failure(&mut self, _status: &dyn TestStatus, _errors: &Errors) {} |
78 | } |
79 | |
80 | impl Summary for () {} |
81 | |
82 | /// A human readable output emitter. |
83 | #[derive (Clone)] |
84 | pub struct Text { |
85 | sender: Sender<Msg>, |
86 | progress: bool, |
87 | } |
88 | |
89 | #[derive (Debug)] |
90 | enum Msg { |
91 | Pop(String, Option<String>), |
92 | Push(String), |
93 | Inc, |
94 | IncLength, |
95 | Finish, |
96 | Status(String, String), |
97 | } |
98 | |
99 | impl Text { |
100 | fn start_thread() -> Sender<Msg> { |
101 | let (sender, receiver) = crossbeam_channel::unbounded(); |
102 | std::thread::spawn(move || { |
103 | let bars = MultiProgress::new(); |
104 | let mut progress = None; |
105 | let mut threads: HashMap<String, ProgressBar> = HashMap::new(); |
106 | 'outer: loop { |
107 | std::thread::sleep(Duration::from_millis(100)); |
108 | loop { |
109 | match receiver.try_recv() { |
110 | Ok(val) => match val { |
111 | Msg::Pop(msg, new_msg) => { |
112 | let Some(spinner) = threads.remove(&msg) else { |
113 | // This can happen when a test was not run at all, because it failed directly during |
114 | // comment parsing. |
115 | continue; |
116 | }; |
117 | spinner.set_style( |
118 | ProgressStyle::with_template("{prefix} {msg}" ).unwrap(), |
119 | ); |
120 | if let Some(new_msg) = new_msg { |
121 | bars.remove(&spinner); |
122 | let spinner = bars.insert(0, spinner); |
123 | spinner.tick(); |
124 | spinner.finish_with_message(new_msg); |
125 | } else { |
126 | spinner.finish_and_clear(); |
127 | } |
128 | } |
129 | Msg::Status(msg, status) => { |
130 | threads.get_mut(&msg).unwrap().set_message(status); |
131 | } |
132 | Msg::Push(msg) => { |
133 | let spinner = |
134 | bars.add(ProgressBar::new_spinner().with_prefix(msg.clone())); |
135 | spinner.set_style( |
136 | ProgressStyle::with_template("{prefix} {spinner} {msg}" ) |
137 | .unwrap(), |
138 | ); |
139 | threads.insert(msg, spinner); |
140 | } |
141 | Msg::IncLength => { |
142 | progress |
143 | .get_or_insert_with(|| bars.add(ProgressBar::new(0))) |
144 | .inc_length(1); |
145 | } |
146 | Msg::Inc => { |
147 | progress.as_ref().unwrap().inc(1); |
148 | } |
149 | Msg::Finish => return, |
150 | }, |
151 | Err(TryRecvError::Disconnected) => break 'outer, |
152 | Err(TryRecvError::Empty) => break, |
153 | } |
154 | } |
155 | for spinner in threads.values() { |
156 | spinner.tick() |
157 | } |
158 | if let Some(progress) = &progress { |
159 | progress.tick() |
160 | } |
161 | } |
162 | assert_eq!(threads.len(), 0); |
163 | if let Some(progress) = progress { |
164 | progress.tick(); |
165 | assert!(progress.is_finished()); |
166 | } |
167 | }); |
168 | sender |
169 | } |
170 | |
171 | /// Print one line per test that gets run. |
172 | pub fn verbose() -> Self { |
173 | Self { |
174 | sender: Self::start_thread(), |
175 | progress: false, |
176 | } |
177 | } |
178 | /// Print a progress bar. |
179 | pub fn quiet() -> Self { |
180 | Self { |
181 | sender: Self::start_thread(), |
182 | progress: true, |
183 | } |
184 | } |
185 | } |
186 | |
187 | impl From<Format> for Text { |
188 | fn from(format: Format) -> Self { |
189 | match format { |
190 | Format::Terse => Text::quiet(), |
191 | Format::Pretty => Text::verbose(), |
192 | } |
193 | } |
194 | } |
195 | |
196 | struct TextTest { |
197 | text: Text, |
198 | path: PathBuf, |
199 | revision: String, |
200 | first: AtomicBool, |
201 | } |
202 | |
203 | impl TextTest { |
204 | /// Prints the user-visible name for this test. |
205 | fn msg(&self) -> String { |
206 | if self.revision.is_empty() { |
207 | self.path.display().to_string() |
208 | } else { |
209 | format!(" {} (revision ` {}`)" , self.path.display(), self.revision) |
210 | } |
211 | } |
212 | } |
213 | |
214 | impl TestStatus for TextTest { |
215 | fn done(&self, result: &TestResult) { |
216 | if self.text.progress { |
217 | self.text.sender.send(Msg::Inc).unwrap(); |
218 | self.text.sender.send(Msg::Pop(self.msg(), None)).unwrap(); |
219 | } else { |
220 | let result = match result { |
221 | Ok(TestOk::Ok) => "ok" .green(), |
222 | Err(Errored { .. }) => "FAILED" .bright_red().bold(), |
223 | Ok(TestOk::Ignored) => "ignored (in-test comment)" .yellow(), |
224 | Ok(TestOk::Filtered) => return, |
225 | }; |
226 | let old_msg = self.msg(); |
227 | let msg = format!("... {result}" ); |
228 | if ProgressDrawTarget::stdout().is_hidden() { |
229 | println!(" {old_msg} {msg}" ); |
230 | std::io::stdout().flush().unwrap(); |
231 | } else { |
232 | self.text.sender.send(Msg::Pop(old_msg, Some(msg))).unwrap(); |
233 | } |
234 | } |
235 | } |
236 | |
237 | fn update_status(&self, msg: String) { |
238 | self.text.sender.send(Msg::Status(self.msg(), msg)).unwrap(); |
239 | } |
240 | |
241 | fn failed_test<'a>( |
242 | &self, |
243 | cmd: &Command, |
244 | stderr: &'a [u8], |
245 | stdout: &'a [u8], |
246 | ) -> Box<dyn Debug + 'a> { |
247 | let text = format!(" {} {}" , "FAILED TEST:" .bright_red(), self.msg()); |
248 | |
249 | println!(); |
250 | println!(" {}" , text.bold().underline()); |
251 | println!("command: {cmd:?}" ); |
252 | println!(); |
253 | |
254 | #[derive (Debug)] |
255 | struct Guard<'a> { |
256 | stderr: &'a [u8], |
257 | stdout: &'a [u8], |
258 | } |
259 | impl<'a> Drop for Guard<'a> { |
260 | fn drop(&mut self) { |
261 | println!(" {}" , "full stderr:" .bold()); |
262 | std::io::stdout().write_all(self.stderr).unwrap(); |
263 | println!(); |
264 | println!(" {}" , "full stdout:" .bold()); |
265 | std::io::stdout().write_all(self.stdout).unwrap(); |
266 | println!(); |
267 | println!(); |
268 | } |
269 | } |
270 | Box::new(Guard { stderr, stdout }) |
271 | } |
272 | |
273 | fn path(&self) -> &Path { |
274 | &self.path |
275 | } |
276 | |
277 | fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> { |
278 | assert_eq!(self.revision, "" ); |
279 | if !self.first.swap(false, std::sync::atomic::Ordering::Relaxed) && self.text.progress { |
280 | self.text.sender.send(Msg::IncLength).unwrap(); |
281 | } |
282 | |
283 | let text = Self { |
284 | text: self.text.clone(), |
285 | path: self.path.clone(), |
286 | revision: revision.to_owned(), |
287 | first: AtomicBool::new(false), |
288 | }; |
289 | self.text.sender.send(Msg::Push(text.msg())).unwrap(); |
290 | Box::new(text) |
291 | } |
292 | |
293 | fn revision(&self) -> &str { |
294 | &self.revision |
295 | } |
296 | } |
297 | |
298 | impl StatusEmitter for Text { |
299 | fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> { |
300 | if self.progress { |
301 | self.sender.send(Msg::IncLength).unwrap(); |
302 | } |
303 | Box::new(TextTest { |
304 | text: self.clone(), |
305 | path, |
306 | revision: String::new(), |
307 | first: AtomicBool::new(true), |
308 | }) |
309 | } |
310 | |
311 | fn finalize( |
312 | &self, |
313 | failures: usize, |
314 | succeeded: usize, |
315 | ignored: usize, |
316 | filtered: usize, |
317 | ) -> Box<dyn Summary> { |
318 | self.sender.send(Msg::Finish).unwrap(); |
319 | while !self.sender.is_empty() { |
320 | std::thread::sleep(Duration::from_millis(10)); |
321 | } |
322 | if !ProgressDrawTarget::stdout().is_hidden() { |
323 | // The progress bars do not have a trailing newline, so let's |
324 | // add it here. |
325 | println!(); |
326 | } |
327 | // Print all errors in a single thread to show reliable output |
328 | if failures == 0 { |
329 | println!(); |
330 | print!("test result: {}." , "ok" .green()); |
331 | if succeeded > 0 { |
332 | print!(" {} passed;" , succeeded.to_string().green()); |
333 | } |
334 | if ignored > 0 { |
335 | print!(" {} ignored;" , ignored.to_string().yellow()); |
336 | } |
337 | if filtered > 0 { |
338 | print!(" {} filtered out;" , filtered.to_string().yellow()); |
339 | } |
340 | println!(); |
341 | println!(); |
342 | Box::new(()) |
343 | } else { |
344 | struct Summarizer { |
345 | failures: Vec<String>, |
346 | succeeded: usize, |
347 | ignored: usize, |
348 | filtered: usize, |
349 | } |
350 | |
351 | impl Summary for Summarizer { |
352 | fn test_failure(&mut self, status: &dyn TestStatus, errors: &Errors) { |
353 | for error in errors { |
354 | print_error(error, status.path()); |
355 | } |
356 | |
357 | self.failures.push(if status.revision().is_empty() { |
358 | format!(" {}" , status.path().display()) |
359 | } else { |
360 | format!( |
361 | " {} (revision {})" , |
362 | status.path().display(), |
363 | status.revision() |
364 | ) |
365 | }); |
366 | } |
367 | } |
368 | |
369 | impl Drop for Summarizer { |
370 | fn drop(&mut self) { |
371 | println!(" {}" , "FAILURES:" .bright_red().underline().bold()); |
372 | for line in &self.failures { |
373 | println!(" {line}" ); |
374 | } |
375 | println!(); |
376 | print!("test result: {}." , "FAIL" .bright_red()); |
377 | print!(" {} failed;" , self.failures.len().to_string().green()); |
378 | if self.succeeded > 0 { |
379 | print!(" {} passed;" , self.succeeded.to_string().green()); |
380 | } |
381 | if self.ignored > 0 { |
382 | print!(" {} ignored;" , self.ignored.to_string().yellow()); |
383 | } |
384 | if self.filtered > 0 { |
385 | print!(" {} filtered out;" , self.filtered.to_string().yellow()); |
386 | } |
387 | println!(); |
388 | println!(); |
389 | } |
390 | } |
391 | Box::new(Summarizer { |
392 | failures: vec![], |
393 | succeeded, |
394 | ignored, |
395 | filtered, |
396 | }) |
397 | } |
398 | } |
399 | } |
400 | |
401 | fn print_error(error: &Error, path: &Path) { |
402 | /// Every error starts with a header like that, to make them all easy to find. |
403 | /// It is made to look like the headers printed for spanned errors. |
404 | fn print_error_header(msg: impl Display) { |
405 | let text = format!(" {} {msg}" , "error:" .bright_red()); |
406 | println!(" {}" , text.bold()); |
407 | } |
408 | |
409 | match error { |
410 | Error::ExitStatus { |
411 | mode, |
412 | status, |
413 | expected, |
414 | } => { |
415 | // `status` prints as `exit status: N`. |
416 | print_error_header(format_args!( |
417 | " {mode} test got {status}, but expected {expected}" |
418 | )) |
419 | } |
420 | Error::Command { kind, status } => { |
421 | // `status` prints as `exit status: N`. |
422 | print_error_header(format_args!(" {kind} failed with {status}" )); |
423 | } |
424 | Error::PatternNotFound { |
425 | pattern, |
426 | expected_line, |
427 | } => { |
428 | let line = match expected_line { |
429 | Some(line) => format!("on line {line}" ), |
430 | None => format!("outside the testfile" ), |
431 | }; |
432 | let msg = match &**pattern { |
433 | Pattern::SubString(s) => { |
434 | format!("` {s}` not found in diagnostics {line}" ) |
435 | } |
436 | Pattern::Regex(r) => { |
437 | format!("`/ {r}/` does not match diagnostics {line}" ,) |
438 | } |
439 | }; |
440 | // This will print a suitable error header. |
441 | create_error( |
442 | msg, |
443 | &[( |
444 | &[("expected because of this pattern" , Some(pattern.span()))], |
445 | pattern.line(), |
446 | )], |
447 | path, |
448 | ); |
449 | } |
450 | Error::NoPatternsFound => { |
451 | print_error_header("no error patterns found in fail test" ); |
452 | } |
453 | Error::PatternFoundInPassTest { mode, span } => { |
454 | let annot = [("expected because of this annotation" , Some(*span))]; |
455 | let mut lines: Vec<(&[_], _)> = vec![(&annot, span.line_start)]; |
456 | let annot = [("expected because of this mode change" , *mode)]; |
457 | if let Some(mode) = mode { |
458 | lines.push((&annot, mode.line_start)) |
459 | } |
460 | // This will print a suitable error header. |
461 | create_error("error pattern found in pass test" , &lines, path); |
462 | } |
463 | Error::OutputDiffers { |
464 | path: output_path, |
465 | actual, |
466 | expected, |
467 | bless_command, |
468 | } => { |
469 | print_error_header("actual output differed from expected" ); |
470 | println!( |
471 | "Execute ` {}` to update ` {}` to the actual output" , |
472 | bless_command, |
473 | output_path.display() |
474 | ); |
475 | println!(" {}" , format!("--- {}" , output_path.display()).red()); |
476 | println!( |
477 | " {}" , |
478 | format!( |
479 | "+++ < {} output>" , |
480 | output_path.extension().unwrap().to_str().unwrap() |
481 | ) |
482 | .green() |
483 | ); |
484 | crate::diff::print_diff(expected, actual); |
485 | } |
486 | Error::ErrorsWithoutPattern { path, msgs } => { |
487 | if let Some(path) = path.as_ref() { |
488 | let line = path.line(); |
489 | let msgs = msgs |
490 | .iter() |
491 | .map(|msg| (format!(" {:?}: {}" , msg.level, msg.message), msg.line_col)) |
492 | .collect::<Vec<_>>(); |
493 | // This will print a suitable error header. |
494 | create_error( |
495 | format!("there were {} unmatched diagnostics" , msgs.len()), |
496 | &[( |
497 | &msgs |
498 | .iter() |
499 | .map(|(msg, lc)| (msg.as_ref(), *lc)) |
500 | .collect::<Vec<_>>(), |
501 | line, |
502 | )], |
503 | path, |
504 | ); |
505 | } else { |
506 | print_error_header(format_args!( |
507 | "there were {} unmatched diagnostics that occurred outside the testfile and had no pattern" , |
508 | msgs.len(), |
509 | )); |
510 | for Message { |
511 | level, |
512 | message, |
513 | line_col: _, |
514 | } in msgs |
515 | { |
516 | println!(" {level:?}: {message}" ) |
517 | } |
518 | } |
519 | } |
520 | Error::InvalidComment { msg, span } => { |
521 | // This will print a suitable error header. |
522 | create_error(msg, &[(&[("" , Some(*span))], span.line_start)], path) |
523 | } |
524 | Error::MultipleRevisionsWithResults { kind, lines } => { |
525 | let title = format!("multiple {kind} found" ); |
526 | // This will print a suitable error header. |
527 | create_error( |
528 | title, |
529 | &lines |
530 | .iter() |
531 | .map(|&line| (&[] as &[_], line)) |
532 | .collect::<Vec<_>>(), |
533 | path, |
534 | ) |
535 | } |
536 | Error::Bug(msg) => { |
537 | print_error_header("a bug in `ui_test` occurred" ); |
538 | println!(" {msg}" ); |
539 | } |
540 | Error::Aux { |
541 | path: aux_path, |
542 | errors, |
543 | line, |
544 | } => { |
545 | print_error_header(format_args!( |
546 | "aux build from {}: {line} failed" , |
547 | path.display() |
548 | )); |
549 | for error in errors { |
550 | print_error(error, aux_path); |
551 | } |
552 | } |
553 | Error::Rustfix(error) => { |
554 | print_error_header(format_args!( |
555 | "failed to apply suggestions for {} with rustfix" , |
556 | path.display() |
557 | )); |
558 | println!(" {error}" ); |
559 | println!("Add //@no-rustfix to the test file to ignore rustfix suggestions" ); |
560 | } |
561 | } |
562 | println!(); |
563 | } |
564 | |
565 | #[allow (clippy::type_complexity)] |
566 | fn create_error( |
567 | s: impl AsRef<str>, |
568 | lines: &[(&[(&str, Option<Span>)], NonZeroUsize)], |
569 | file: &Path, |
570 | ) { |
571 | let source = std::fs::read_to_string(file).unwrap(); |
572 | let source: Vec<_> = source.split_inclusive(' \n' ).collect(); |
573 | let file = file.display().to_string(); |
574 | let msg = Snippet { |
575 | title: Some(Annotation { |
576 | id: None, |
577 | annotation_type: AnnotationType::Error, |
578 | label: Some(s.as_ref()), |
579 | }), |
580 | slices: lines |
581 | .iter() |
582 | .map(|(label, line)| { |
583 | let source = source[line.get() - 1]; |
584 | let len = source.chars().count(); |
585 | Slice { |
586 | source, |
587 | line_start: line.get(), |
588 | origin: Some(&file), |
589 | annotations: label |
590 | .iter() |
591 | .map(|(label, lc)| SourceAnnotation { |
592 | range: lc.map_or((0, len - 1), |lc| { |
593 | assert_eq!(lc.line_start, *line); |
594 | if lc.line_end > lc.line_start { |
595 | (lc.column_start.get() - 1, len - 1) |
596 | } else if lc.column_start == lc.column_end { |
597 | if lc.column_start.get() - 1 == len { |
598 | // rustc sometimes produces spans pointing *after* the `\n` at the end of the line, |
599 | // but we want to render an annotation at the end. |
600 | (lc.column_start.get() - 2, lc.column_start.get() - 1) |
601 | } else { |
602 | (lc.column_start.get() - 1, lc.column_start.get()) |
603 | } |
604 | } else { |
605 | (lc.column_start.get() - 1, lc.column_end.get() - 1) |
606 | } |
607 | }), |
608 | label, |
609 | annotation_type: AnnotationType::Error, |
610 | }) |
611 | .collect(), |
612 | fold: false, |
613 | } |
614 | }) |
615 | .collect(), |
616 | footer: vec![], |
617 | opt: FormatOptions { |
618 | color: colored::control::SHOULD_COLORIZE.should_colorize(), |
619 | anonymized_line_numbers: false, |
620 | margin: None, |
621 | }, |
622 | }; |
623 | println!(" {}" , DisplayList::from(msg)); |
624 | } |
625 | |
626 | fn gha_error(error: &Error, test_path: &str, revision: &str) { |
627 | match error { |
628 | Error::ExitStatus { |
629 | mode, |
630 | status, |
631 | expected, |
632 | } => { |
633 | github_actions::error( |
634 | test_path, |
635 | format!(" {mode} test {revision} got {status}, but expected {expected}" ), |
636 | ); |
637 | } |
638 | Error::Command { kind, status } => { |
639 | github_actions::error(test_path, format!(" {kind}{revision} failed with {status}" )); |
640 | } |
641 | Error::PatternNotFound { pattern, .. } => { |
642 | github_actions::error(test_path, format!("Pattern not found {revision}" )) |
643 | .line(pattern.line()); |
644 | } |
645 | Error::NoPatternsFound => { |
646 | github_actions::error( |
647 | test_path, |
648 | format!("no error patterns found in fail test {revision}" ), |
649 | ); |
650 | } |
651 | Error::PatternFoundInPassTest { .. } => { |
652 | github_actions::error( |
653 | test_path, |
654 | format!("error pattern found in pass test {revision}" ), |
655 | ); |
656 | } |
657 | Error::OutputDiffers { |
658 | path: output_path, |
659 | actual, |
660 | expected, |
661 | bless_command, |
662 | } => { |
663 | if expected.is_empty() { |
664 | let mut err = github_actions::error( |
665 | test_path, |
666 | "test generated output, but there was no output file" , |
667 | ); |
668 | writeln!( |
669 | err, |
670 | "you likely need to bless the tests with ` {bless_command}`" |
671 | ) |
672 | .unwrap(); |
673 | return; |
674 | } |
675 | |
676 | let mut line = 1; |
677 | for r in |
678 | prettydiff::diff_lines(expected.to_str().unwrap(), actual.to_str().unwrap()).diff() |
679 | { |
680 | use prettydiff::basic::DiffOp::*; |
681 | match r { |
682 | Equal(s) => { |
683 | line += s.len(); |
684 | continue; |
685 | } |
686 | Replace(l, r) => { |
687 | let mut err = github_actions::error( |
688 | output_path.display().to_string(), |
689 | "actual output differs from expected" , |
690 | ) |
691 | .line(NonZeroUsize::new(line + 1).unwrap()); |
692 | writeln!(err, "this line was expected to be ` {}`" , r[0]).unwrap(); |
693 | line += l.len(); |
694 | } |
695 | Remove(l) => { |
696 | let mut err = github_actions::error( |
697 | output_path.display().to_string(), |
698 | "extraneous lines in output" , |
699 | ) |
700 | .line(NonZeroUsize::new(line + 1).unwrap()); |
701 | writeln!( |
702 | err, |
703 | "remove this line and possibly later ones by blessing the test" |
704 | ) |
705 | .unwrap(); |
706 | line += l.len(); |
707 | } |
708 | Insert(r) => { |
709 | let mut err = github_actions::error( |
710 | output_path.display().to_string(), |
711 | "missing line in output" , |
712 | ) |
713 | .line(NonZeroUsize::new(line + 1).unwrap()); |
714 | writeln!(err, "bless the test to create a line containing ` {}`" , r[0]) |
715 | .unwrap(); |
716 | // Do not count these lines, they don't exist in the original file and |
717 | // would thus mess up the line number. |
718 | } |
719 | } |
720 | } |
721 | } |
722 | Error::ErrorsWithoutPattern { path, msgs } => { |
723 | if let Some(path) = path.as_ref() { |
724 | let line = path.line(); |
725 | let path = path.display(); |
726 | let mut err = |
727 | github_actions::error(&path, format!("Unmatched diagnostics {revision}" )) |
728 | .line(line); |
729 | for Message { |
730 | level, |
731 | message, |
732 | line_col: _, |
733 | } in msgs |
734 | { |
735 | writeln!(err, " {level:?}: {message}" ).unwrap(); |
736 | } |
737 | } else { |
738 | let mut err = github_actions::error( |
739 | test_path, |
740 | format!("Unmatched diagnostics outside the testfile {revision}" ), |
741 | ); |
742 | for Message { |
743 | level, |
744 | message, |
745 | line_col: _, |
746 | } in msgs |
747 | { |
748 | writeln!(err, " {level:?}: {message}" ).unwrap(); |
749 | } |
750 | } |
751 | } |
752 | Error::InvalidComment { msg, span } => { |
753 | let mut err = github_actions::error(test_path, format!("Could not parse comment" )) |
754 | .line(span.line_start); |
755 | writeln!(err, " {msg}" ).unwrap(); |
756 | } |
757 | Error::MultipleRevisionsWithResults { kind, lines } => { |
758 | github_actions::error(test_path, format!("multiple {kind} found" )).line(lines[0]); |
759 | } |
760 | Error::Bug(_) => {} |
761 | Error::Aux { |
762 | path: aux_path, |
763 | errors, |
764 | line, |
765 | } => { |
766 | github_actions::error(test_path, format!("Aux build failed" )).line(*line); |
767 | for error in errors { |
768 | gha_error(error, &aux_path.display().to_string(), "" ) |
769 | } |
770 | } |
771 | Error::Rustfix(error) => { |
772 | github_actions::error( |
773 | test_path, |
774 | format!("failed to apply suggestions with rustfix: {error}" ), |
775 | ); |
776 | } |
777 | } |
778 | } |
779 | |
780 | /// Emits Github Actions Workspace commands to show the failures directly in the github diff view. |
781 | /// If the const generic `GROUP` boolean is `true`, also emit `::group` commands. |
782 | pub struct Gha<const GROUP: bool> { |
783 | /// Show a specific name for the final summary. |
784 | pub name: String, |
785 | } |
786 | |
787 | #[derive (Clone)] |
788 | struct PathAndRev<const GROUP: bool> { |
789 | path: PathBuf, |
790 | revision: String, |
791 | } |
792 | |
793 | impl<const GROUP: bool> TestStatus for PathAndRev<GROUP> { |
794 | fn path(&self) -> &Path { |
795 | &self.path |
796 | } |
797 | |
798 | fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> { |
799 | assert_eq!(self.revision, "" ); |
800 | Box::new(Self { |
801 | path: self.path.clone(), |
802 | revision: revision.to_owned(), |
803 | }) |
804 | } |
805 | |
806 | fn failed_test(&self, _cmd: &Command, _stderr: &[u8], _stdout: &[u8]) -> Box<dyn Debug> { |
807 | if GROUP { |
808 | Box::new(github_actions::group(format_args!( |
809 | " {}: {}" , |
810 | self.path.display(), |
811 | self.revision |
812 | ))) |
813 | } else { |
814 | Box::new(()) |
815 | } |
816 | } |
817 | |
818 | fn revision(&self) -> &str { |
819 | &self.revision |
820 | } |
821 | |
822 | fn update_status(&self, _msg: String) {} |
823 | } |
824 | |
825 | impl<const GROUP: bool> StatusEmitter for Gha<GROUP> { |
826 | fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> { |
827 | Box::new(PathAndRev::<GROUP> { |
828 | path, |
829 | revision: String::new(), |
830 | }) |
831 | } |
832 | |
833 | fn finalize( |
834 | &self, |
835 | _failures: usize, |
836 | succeeded: usize, |
837 | ignored: usize, |
838 | filtered: usize, |
839 | ) -> Box<dyn Summary> { |
840 | struct Summarizer<const GROUP: bool> { |
841 | failures: Vec<String>, |
842 | succeeded: usize, |
843 | ignored: usize, |
844 | filtered: usize, |
845 | name: String, |
846 | } |
847 | |
848 | impl<const GROUP: bool> Summary for Summarizer<GROUP> { |
849 | fn test_failure(&mut self, status: &dyn TestStatus, errors: &Errors) { |
850 | let revision = if status.revision().is_empty() { |
851 | "" .to_string() |
852 | } else { |
853 | format!(" (revision: {})" , status.revision()) |
854 | }; |
855 | for error in errors { |
856 | gha_error(error, &status.path().display().to_string(), &revision); |
857 | } |
858 | self.failures |
859 | .push(format!(" {}{revision}" , status.path().display())); |
860 | } |
861 | } |
862 | impl<const GROUP: bool> Drop for Summarizer<GROUP> { |
863 | fn drop(&mut self) { |
864 | if let Some(mut file) = github_actions::summary() { |
865 | writeln!(file, "### {}" , self.name).unwrap(); |
866 | for line in &self.failures { |
867 | writeln!(file, "* {line}" ).unwrap(); |
868 | } |
869 | writeln!(file).unwrap(); |
870 | writeln!(file, "| failed | passed | ignored | filtered out |" ).unwrap(); |
871 | writeln!(file, "| --- | --- | --- | --- |" ).unwrap(); |
872 | writeln!( |
873 | file, |
874 | "| {} | {} | {} | {} |" , |
875 | self.failures.len(), |
876 | self.succeeded, |
877 | self.ignored, |
878 | self.filtered, |
879 | ) |
880 | .unwrap(); |
881 | } |
882 | } |
883 | } |
884 | |
885 | Box::new(Summarizer::<GROUP> { |
886 | failures: vec![], |
887 | succeeded, |
888 | ignored, |
889 | filtered, |
890 | name: self.name.clone(), |
891 | }) |
892 | } |
893 | } |
894 | |
895 | impl<T: TestStatus, U: TestStatus> TestStatus for (T, U) { |
896 | fn done(&self, result: &TestResult) { |
897 | self.0.done(result); |
898 | self.1.done(result); |
899 | } |
900 | |
901 | fn failed_test<'a>( |
902 | &'a self, |
903 | cmd: &'a Command, |
904 | stderr: &'a [u8], |
905 | stdout: &'a [u8], |
906 | ) -> Box<dyn Debug + 'a> { |
907 | Box::new(( |
908 | self.0.failed_test(cmd, stderr, stdout), |
909 | self.1.failed_test(cmd, stderr, stdout), |
910 | )) |
911 | } |
912 | |
913 | fn path(&self) -> &Path { |
914 | let path = self.0.path(); |
915 | assert_eq!(path, self.1.path()); |
916 | path |
917 | } |
918 | |
919 | fn revision(&self) -> &str { |
920 | let rev = self.0.revision(); |
921 | assert_eq!(rev, self.1.revision()); |
922 | rev |
923 | } |
924 | |
925 | fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> { |
926 | Box::new((self.0.for_revision(revision), self.1.for_revision(revision))) |
927 | } |
928 | |
929 | fn update_status(&self, msg: String) { |
930 | self.0.update_status(msg.clone()); |
931 | self.1.update_status(msg) |
932 | } |
933 | } |
934 | |
935 | impl<T: StatusEmitter, U: StatusEmitter> StatusEmitter for (T, U) { |
936 | fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> { |
937 | Box::new(( |
938 | self.0.register_test(path.clone()), |
939 | self.1.register_test(path), |
940 | )) |
941 | } |
942 | |
943 | fn finalize( |
944 | &self, |
945 | failures: usize, |
946 | succeeded: usize, |
947 | ignored: usize, |
948 | filtered: usize, |
949 | ) -> Box<dyn Summary> { |
950 | Box::new(( |
951 | self.1.finalize(failed:failures, succeeded, ignored, filtered), |
952 | self.0.finalize(failed:failures, succeeded, ignored, filtered), |
953 | )) |
954 | } |
955 | } |
956 | |
957 | impl<T: TestStatus + ?Sized> TestStatus for Box<T> { |
958 | fn done(&self, result: &TestResult) { |
959 | (**self).done(result); |
960 | } |
961 | |
962 | fn path(&self) -> &Path { |
963 | (**self).path() |
964 | } |
965 | |
966 | fn revision(&self) -> &str { |
967 | (**self).revision() |
968 | } |
969 | |
970 | fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> { |
971 | (**self).for_revision(revision) |
972 | } |
973 | |
974 | fn failed_test<'a>( |
975 | &'a self, |
976 | cmd: &'a Command, |
977 | stderr: &'a [u8], |
978 | stdout: &'a [u8], |
979 | ) -> Box<dyn Debug + 'a> { |
980 | (**self).failed_test(cmd, stderr, stdout) |
981 | } |
982 | |
983 | fn update_status(&self, msg: String) { |
984 | (**self).update_status(msg) |
985 | } |
986 | } |
987 | |
988 | impl<T: StatusEmitter + ?Sized> StatusEmitter for Box<T> { |
989 | fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> { |
990 | (**self).register_test(path) |
991 | } |
992 | |
993 | fn finalize( |
994 | &self, |
995 | failures: usize, |
996 | succeeded: usize, |
997 | ignored: usize, |
998 | filtered: usize, |
999 | ) -> Box<dyn Summary> { |
1000 | (**self).finalize(failed:failures, succeeded, ignored, filtered) |
1001 | } |
1002 | } |
1003 | |
1004 | impl Summary for (Box<dyn Summary>, Box<dyn Summary>) { |
1005 | fn test_failure(&mut self, status: &dyn TestStatus, errors: &Errors) { |
1006 | self.0.test_failure(status, errors); |
1007 | self.1.test_failure(status, errors); |
1008 | } |
1009 | } |
1010 | |