1//! Variaous schemes for reporting messages during testing or after testing is done.
2
3use annotate_snippets::{
4 display_list::{DisplayList, FormatOptions},
5 snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
6};
7use bstr::ByteSlice;
8use colored::Colorize;
9use crossbeam_channel::{Sender, TryRecvError};
10use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
11
12use crate::{
13 github_actions,
14 parser::Pattern,
15 rustc_stderr::{Message, Span},
16 Error, Errored, Errors, Format, TestOk, TestResult,
17};
18use 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.
31pub 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.
48pub 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.
75pub trait Summary {
76 /// A test has finished, handle the result.
77 fn test_failure(&mut self, _status: &dyn TestStatus, _errors: &Errors) {}
78}
79
80impl Summary for () {}
81
82/// A human readable output emitter.
83#[derive(Clone)]
84pub struct Text {
85 sender: Sender<Msg>,
86 progress: bool,
87}
88
89#[derive(Debug)]
90enum Msg {
91 Pop(String, Option<String>),
92 Push(String),
93 Inc,
94 IncLength,
95 Finish,
96 Status(String, String),
97}
98
99impl 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
187impl 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
196struct TextTest {
197 text: Text,
198 path: PathBuf,
199 revision: String,
200 first: AtomicBool,
201}
202
203impl 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
214impl 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
298impl 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
401fn 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)]
566fn 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
626fn 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.
782pub struct Gha<const GROUP: bool> {
783 /// Show a specific name for the final summary.
784 pub name: String,
785}
786
787#[derive(Clone)]
788struct PathAndRev<const GROUP: bool> {
789 path: PathBuf,
790 revision: String,
791}
792
793impl<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
825impl<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
895impl<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
935impl<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
957impl<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
988impl<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
1004impl 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