summaryrefslogtreecommitdiffstats
path: root/vendor/ui_test-0.20.0/src/status_emitter.rs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/ui_test-0.20.0/src/status_emitter.rs')
-rw-r--r--vendor/ui_test-0.20.0/src/status_emitter.rs972
1 files changed, 972 insertions, 0 deletions
diff --git a/vendor/ui_test-0.20.0/src/status_emitter.rs b/vendor/ui_test-0.20.0/src/status_emitter.rs
new file mode 100644
index 000000000..b32235907
--- /dev/null
+++ b/vendor/ui_test-0.20.0/src/status_emitter.rs
@@ -0,0 +1,972 @@
+//! Variaous schemes for reporting messages during testing or after testing is done.
+
+use annotate_snippets::{
+ display_list::{DisplayList, FormatOptions},
+ snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
+};
+use bstr::ByteSlice;
+use colored::Colorize;
+use crossbeam_channel::{Sender, TryRecvError};
+use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
+
+use crate::{
+ github_actions,
+ parser::Pattern,
+ rustc_stderr::{Message, Span},
+ Error, Errored, Errors, TestOk, TestResult,
+};
+use std::{
+ collections::HashMap,
+ fmt::{Debug, Write as _},
+ io::Write as _,
+ num::NonZeroUsize,
+ panic::RefUnwindSafe,
+ path::{Path, PathBuf},
+ process::Command,
+ sync::atomic::AtomicBool,
+ time::Duration,
+};
+
+/// A generic way to handle the output of this crate.
+pub trait StatusEmitter: Sync + RefUnwindSafe {
+ /// Invoked the moment we know a test will later be run.
+ /// Useful for progress bars and such.
+ fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus>;
+
+ /// Create a report about the entire test run at the end.
+ #[allow(clippy::type_complexity)]
+ fn finalize(
+ &self,
+ failed: usize,
+ succeeded: usize,
+ ignored: usize,
+ filtered: usize,
+ ) -> Box<dyn Summary>;
+}
+
+/// Information about a specific test run.
+pub trait TestStatus: Send + Sync + RefUnwindSafe {
+ /// Create a copy of this test for a new revision.
+ fn for_revision(&self, revision: &str) -> Box<dyn TestStatus>;
+
+ /// Invoked before each failed test prints its errors along with a drop guard that can
+ /// gets invoked afterwards.
+ fn failed_test<'a>(
+ &'a self,
+ cmd: &'a Command,
+ stderr: &'a [u8],
+ stdout: &'a [u8],
+ ) -> Box<dyn Debug + 'a>;
+
+ /// Change the status of the test while it is running to supply some kind of progress
+ fn update_status(&self, msg: String);
+
+ /// A test has finished, handle the result immediately.
+ fn done(&self, _result: &TestResult) {}
+
+ /// The path of the test file.
+ fn path(&self) -> &Path;
+
+ /// The revision, usually an empty string.
+ fn revision(&self) -> &str;
+}
+
+/// Report a summary at the end of a test run.
+pub trait Summary {
+ /// A test has finished, handle the result.
+ fn test_failure(&mut self, _status: &dyn TestStatus, _errors: &Errors) {}
+}
+
+impl Summary for () {}
+
+/// A human readable output emitter.
+#[derive(Clone)]
+pub struct Text {
+ sender: Sender<Msg>,
+ progress: bool,
+}
+
+#[derive(Debug)]
+enum Msg {
+ Pop(String, Option<String>),
+ Push(String),
+ Inc,
+ IncLength,
+ Finish,
+ Status(String, String),
+}
+
+impl Text {
+ fn start_thread() -> Sender<Msg> {
+ let (sender, receiver) = crossbeam_channel::unbounded();
+ std::thread::spawn(move || {
+ let bars = MultiProgress::new();
+ let mut progress = None;
+ let mut threads: HashMap<String, ProgressBar> = HashMap::new();
+ 'outer: loop {
+ std::thread::sleep(Duration::from_millis(100));
+ loop {
+ match receiver.try_recv() {
+ Ok(val) => match val {
+ Msg::Pop(msg, new_msg) => {
+ let Some(spinner) = threads.remove(&msg) else {
+ // This can happen when a test was not run at all, because it failed directly during
+ // comment parsing.
+ continue;
+ };
+ spinner.set_style(
+ ProgressStyle::with_template("{prefix} {msg}").unwrap(),
+ );
+ if let Some(new_msg) = new_msg {
+ bars.remove(&spinner);
+ let spinner = bars.insert(0, spinner);
+ spinner.tick();
+ spinner.finish_with_message(new_msg);
+ } else {
+ spinner.finish_and_clear();
+ }
+ }
+ Msg::Status(msg, status) => {
+ threads.get_mut(&msg).unwrap().set_message(status);
+ }
+ Msg::Push(msg) => {
+ let spinner =
+ bars.add(ProgressBar::new_spinner().with_prefix(msg.clone()));
+ spinner.set_style(
+ ProgressStyle::with_template("{prefix} {spinner} {msg}")
+ .unwrap(),
+ );
+ threads.insert(msg, spinner);
+ }
+ Msg::IncLength => {
+ progress
+ .get_or_insert_with(|| bars.add(ProgressBar::new(0)))
+ .inc_length(1);
+ }
+ Msg::Inc => {
+ progress.as_ref().unwrap().inc(1);
+ }
+ Msg::Finish => return,
+ },
+ Err(TryRecvError::Disconnected) => break 'outer,
+ Err(TryRecvError::Empty) => break,
+ }
+ }
+ for spinner in threads.values() {
+ spinner.tick()
+ }
+ if let Some(progress) = &progress {
+ progress.tick()
+ }
+ }
+ assert_eq!(threads.len(), 0);
+ if let Some(progress) = progress {
+ progress.tick();
+ assert!(progress.is_finished());
+ }
+ });
+ sender
+ }
+
+ /// Print one line per test that gets run.
+ pub fn verbose() -> Self {
+ Self {
+ sender: Self::start_thread(),
+ progress: false,
+ }
+ }
+ /// Print a progress bar.
+ pub fn quiet() -> Self {
+ Self {
+ sender: Self::start_thread(),
+ progress: true,
+ }
+ }
+}
+
+struct TextTest {
+ text: Text,
+ path: PathBuf,
+ revision: String,
+ first: AtomicBool,
+}
+
+impl TextTest {
+ fn msg(&self) -> String {
+ if self.revision.is_empty() {
+ self.path.display().to_string()
+ } else {
+ format!("{} ({})", self.path.display(), self.revision)
+ }
+ }
+}
+
+impl TestStatus for TextTest {
+ fn done(&self, result: &TestResult) {
+ if self.text.progress {
+ self.text.sender.send(Msg::Inc).unwrap();
+ self.text.sender.send(Msg::Pop(self.msg(), None)).unwrap();
+ } else {
+ let result = match result {
+ Ok(TestOk::Ok) => "ok".green(),
+ Err(Errored { .. }) => "FAILED".red().bold(),
+ Ok(TestOk::Ignored) => "ignored (in-test comment)".yellow(),
+ Ok(TestOk::Filtered) => return,
+ };
+ let old_msg = self.msg();
+ let msg = format!("... {result}");
+ if ProgressDrawTarget::stdout().is_hidden() {
+ println!("{old_msg} {msg}");
+ std::io::stdout().flush().unwrap();
+ } else {
+ self.text.sender.send(Msg::Pop(old_msg, Some(msg))).unwrap();
+ }
+ }
+ }
+
+ fn update_status(&self, msg: String) {
+ self.text.sender.send(Msg::Status(self.msg(), msg)).unwrap();
+ }
+
+ fn failed_test<'a>(
+ &self,
+ cmd: &Command,
+ stderr: &'a [u8],
+ stdout: &'a [u8],
+ ) -> Box<dyn Debug + 'a> {
+ println!();
+ let path = self.path.display().to_string();
+ print!("{}", path.underline().bold());
+ let revision = if self.revision.is_empty() {
+ String::new()
+ } else {
+ format!(" (revision `{}`)", self.revision)
+ };
+ print!("{revision}");
+ print!(" {}", "FAILED:".red().bold());
+ println!();
+ println!("command: {cmd:?}");
+ println!();
+
+ #[derive(Debug)]
+ struct Guard<'a> {
+ stderr: &'a [u8],
+ stdout: &'a [u8],
+ }
+ impl<'a> Drop for Guard<'a> {
+ fn drop(&mut self) {
+ println!("full stderr:");
+ std::io::stdout().write_all(self.stderr).unwrap();
+ println!();
+ println!("full stdout:");
+ std::io::stdout().write_all(self.stdout).unwrap();
+ println!();
+ println!();
+ }
+ }
+ Box::new(Guard { stderr, stdout })
+ }
+
+ fn path(&self) -> &Path {
+ &self.path
+ }
+
+ fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> {
+ assert_eq!(self.revision, "");
+ if !self.first.swap(false, std::sync::atomic::Ordering::Relaxed) && self.text.progress {
+ self.text.sender.send(Msg::IncLength).unwrap();
+ }
+
+ let text = Self {
+ text: self.text.clone(),
+ path: self.path.clone(),
+ revision: revision.to_owned(),
+ first: AtomicBool::new(false),
+ };
+ self.text.sender.send(Msg::Push(text.msg())).unwrap();
+ Box::new(text)
+ }
+
+ fn revision(&self) -> &str {
+ &self.revision
+ }
+}
+
+impl StatusEmitter for Text {
+ fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> {
+ if self.progress {
+ self.sender.send(Msg::IncLength).unwrap();
+ }
+ Box::new(TextTest {
+ text: self.clone(),
+ path,
+ revision: String::new(),
+ first: AtomicBool::new(true),
+ })
+ }
+
+ fn finalize(
+ &self,
+ failures: usize,
+ succeeded: usize,
+ ignored: usize,
+ filtered: usize,
+ ) -> Box<dyn Summary> {
+ self.sender.send(Msg::Finish).unwrap();
+ while !self.sender.is_empty() {
+ std::thread::sleep(Duration::from_millis(10));
+ }
+ if !ProgressDrawTarget::stdout().is_hidden() {
+ // The progress bars do not have a trailing newline, so let's
+ // add it here.
+ println!();
+ }
+ // Print all errors in a single thread to show reliable output
+ if failures == 0 {
+ println!();
+ print!("test result: {}.", "ok".green());
+ if succeeded > 0 {
+ print!(" {} passed;", succeeded.to_string().green());
+ }
+ if ignored > 0 {
+ print!(" {} ignored;", ignored.to_string().yellow());
+ }
+ if filtered > 0 {
+ print!(" {} filtered out;", filtered.to_string().yellow());
+ }
+ println!();
+ println!();
+ Box::new(())
+ } else {
+ struct Summarizer {
+ failures: Vec<String>,
+ succeeded: usize,
+ ignored: usize,
+ filtered: usize,
+ }
+
+ impl Summary for Summarizer {
+ fn test_failure(&mut self, status: &dyn TestStatus, errors: &Errors) {
+ for error in errors {
+ print_error(error, status.path());
+ }
+
+ self.failures.push(if status.revision().is_empty() {
+ format!(" {}", status.path().display())
+ } else {
+ format!(
+ " {} (revision {})",
+ status.path().display(),
+ status.revision()
+ )
+ });
+ }
+ }
+
+ impl Drop for Summarizer {
+ fn drop(&mut self) {
+ println!("{}", "FAILURES:".red().underline().bold());
+ for line in &self.failures {
+ println!("{line}");
+ }
+ println!();
+ print!("test result: {}.", "FAIL".red());
+ print!(" {} failed;", self.failures.len().to_string().green());
+ if self.succeeded > 0 {
+ print!(" {} passed;", self.succeeded.to_string().green());
+ }
+ if self.ignored > 0 {
+ print!(" {} ignored;", self.ignored.to_string().yellow());
+ }
+ if self.filtered > 0 {
+ print!(" {} filtered out;", self.filtered.to_string().yellow());
+ }
+ println!();
+ println!();
+ }
+ }
+ Box::new(Summarizer {
+ failures: vec![],
+ succeeded,
+ ignored,
+ filtered,
+ })
+ }
+ }
+}
+
+fn print_error(error: &Error, path: &Path) {
+ match error {
+ Error::ExitStatus {
+ mode,
+ status,
+ expected,
+ } => {
+ println!("{mode} test got {status}, but expected {expected}")
+ }
+ Error::Command { kind, status } => {
+ println!("{kind} failed with {status}");
+ }
+ Error::PatternNotFound(pattern) => {
+ let msg = match &**pattern {
+ Pattern::SubString(s) => {
+ format!("substring `{s}` {} in stderr output", "not found")
+ }
+ Pattern::Regex(r) => {
+ format!("`/{r}/` does {} stderr output", "not match")
+ }
+ };
+ create_error(
+ msg,
+ &[(
+ &[("expected because of this pattern", Some(pattern.span()))],
+ pattern.line(),
+ )],
+ path,
+ );
+ }
+ Error::NoPatternsFound => {
+ println!("{}", "no error patterns found in fail test".red());
+ }
+ Error::PatternFoundInPassTest => {
+ println!("{}", "error pattern found in pass test".red())
+ }
+ Error::OutputDiffers {
+ path: output_path,
+ actual,
+ expected,
+ bless_command,
+ } => {
+ println!("{}", "actual output differed from expected".underline());
+ println!(
+ "Execute `{}` to update `{}` to the actual output",
+ bless_command,
+ output_path.display()
+ );
+ println!("{}", format!("--- {}", output_path.display()).red());
+ println!(
+ "{}",
+ format!(
+ "+++ <{} output>",
+ output_path.extension().unwrap().to_str().unwrap()
+ )
+ .green()
+ );
+ crate::diff::print_diff(expected, actual);
+ }
+ Error::ErrorsWithoutPattern { path, msgs } => {
+ if let Some(path) = path.as_ref() {
+ let line = path.line();
+ let msgs = msgs
+ .iter()
+ .map(|msg| (format!("{:?}: {}", msg.level, msg.message), msg.line_col))
+ .collect::<Vec<_>>();
+ create_error(
+ format!("There were {} unmatched diagnostics", msgs.len()),
+ &[(
+ &msgs
+ .iter()
+ .map(|(msg, lc)| (msg.as_ref(), *lc))
+ .collect::<Vec<_>>(),
+ line,
+ )],
+ path,
+ );
+ } else {
+ println!(
+ "There were {} unmatched diagnostics that occurred outside the testfile and had no pattern",
+ msgs.len(),
+ );
+ for Message {
+ level,
+ message,
+ line_col: _,
+ } in msgs
+ {
+ println!(" {level:?}: {message}")
+ }
+ }
+ }
+ Error::InvalidComment { msg, span } => {
+ create_error(msg, &[(&[("", Some(*span))], span.line_start)], path)
+ }
+ Error::MultipleRevisionsWithResults { kind, lines } => {
+ let title = format!("multiple {kind} found");
+ create_error(
+ title,
+ &lines
+ .iter()
+ .map(|&line| (&[] as &[_], line))
+ .collect::<Vec<_>>(),
+ path,
+ )
+ }
+ Error::Bug(msg) => {
+ println!("A bug in `ui_test` occurred: {msg}");
+ }
+ Error::Aux {
+ path: aux_path,
+ errors,
+ line,
+ } => {
+ println!("Aux build from {}:{line} failed", path.display());
+ for error in errors {
+ print_error(error, aux_path);
+ }
+ }
+ Error::Rustfix(error) => {
+ println!(
+ "failed to apply suggestions for {} with rustfix: {error}",
+ path.display()
+ );
+ println!("Add //@no-rustfix to the test file to ignore rustfix suggestions");
+ }
+ }
+ println!();
+}
+
+#[allow(clippy::type_complexity)]
+fn create_error(
+ s: impl AsRef<str>,
+ lines: &[(&[(&str, Option<Span>)], NonZeroUsize)],
+ file: &Path,
+) {
+ let source = std::fs::read_to_string(file).unwrap();
+ let source: Vec<_> = source.split_inclusive('\n').collect();
+ let file = file.display().to_string();
+ let msg = Snippet {
+ title: Some(Annotation {
+ id: None,
+ annotation_type: AnnotationType::Error,
+ label: Some(s.as_ref()),
+ }),
+ slices: lines
+ .iter()
+ .map(|(label, line)| {
+ let source = source[line.get() - 1];
+ let len = source.chars().count();
+ Slice {
+ source,
+ line_start: line.get(),
+ origin: Some(&file),
+ annotations: label
+ .iter()
+ .map(|(label, lc)| SourceAnnotation {
+ range: lc.map_or((0, len - 1), |lc| {
+ assert_eq!(lc.line_start, *line);
+ if lc.line_end > lc.line_start {
+ (lc.column_start.get() - 1, len - 1)
+ } else if lc.column_start == lc.column_end {
+ if lc.column_start.get() - 1 == len {
+ // rustc sometimes produces spans pointing *after* the `\n` at the end of the line,
+ // but we want to render an annotation at the end.
+ (lc.column_start.get() - 2, lc.column_start.get() - 1)
+ } else {
+ (lc.column_start.get() - 1, lc.column_start.get())
+ }
+ } else {
+ (lc.column_start.get() - 1, lc.column_end.get() - 1)
+ }
+ }),
+ label,
+ annotation_type: AnnotationType::Error,
+ })
+ .collect(),
+ fold: false,
+ }
+ })
+ .collect(),
+ footer: vec![],
+ opt: FormatOptions {
+ color: colored::control::SHOULD_COLORIZE.should_colorize(),
+ anonymized_line_numbers: false,
+ margin: None,
+ },
+ };
+ println!("{}", DisplayList::from(msg));
+}
+
+fn gha_error(error: &Error, test_path: &str, revision: &str) {
+ match error {
+ Error::ExitStatus {
+ mode,
+ status,
+ expected,
+ } => {
+ github_actions::error(
+ test_path,
+ format!("{mode} test{revision} got {status}, but expected {expected}"),
+ );
+ }
+ Error::Command { kind, status } => {
+ github_actions::error(test_path, format!("{kind}{revision} failed with {status}"));
+ }
+ Error::PatternNotFound(pattern) => {
+ github_actions::error(test_path, format!("Pattern not found{revision}"))
+ .line(pattern.line());
+ }
+ Error::NoPatternsFound => {
+ github_actions::error(
+ test_path,
+ format!("no error patterns found in fail test{revision}"),
+ );
+ }
+ Error::PatternFoundInPassTest => {
+ github_actions::error(
+ test_path,
+ format!("error pattern found in pass test{revision}"),
+ );
+ }
+ Error::OutputDiffers {
+ path: output_path,
+ actual,
+ expected,
+ bless_command,
+ } => {
+ if expected.is_empty() {
+ let mut err = github_actions::error(
+ test_path,
+ "test generated output, but there was no output file",
+ );
+ writeln!(
+ err,
+ "you likely need to bless the tests with `{bless_command}`"
+ )
+ .unwrap();
+ return;
+ }
+
+ let mut line = 1;
+ for r in
+ prettydiff::diff_lines(expected.to_str().unwrap(), actual.to_str().unwrap()).diff()
+ {
+ use prettydiff::basic::DiffOp::*;
+ match r {
+ Equal(s) => {
+ line += s.len();
+ continue;
+ }
+ Replace(l, r) => {
+ let mut err = github_actions::error(
+ output_path.display().to_string(),
+ "actual output differs from expected",
+ )
+ .line(NonZeroUsize::new(line + 1).unwrap());
+ writeln!(err, "this line was expected to be `{}`", r[0]).unwrap();
+ line += l.len();
+ }
+ Remove(l) => {
+ let mut err = github_actions::error(
+ output_path.display().to_string(),
+ "extraneous lines in output",
+ )
+ .line(NonZeroUsize::new(line + 1).unwrap());
+ writeln!(
+ err,
+ "remove this line and possibly later ones by blessing the test"
+ )
+ .unwrap();
+ line += l.len();
+ }
+ Insert(r) => {
+ let mut err = github_actions::error(
+ output_path.display().to_string(),
+ "missing line in output",
+ )
+ .line(NonZeroUsize::new(line + 1).unwrap());
+ writeln!(err, "bless the test to create a line containing `{}`", r[0])
+ .unwrap();
+ // Do not count these lines, they don't exist in the original file and
+ // would thus mess up the line number.
+ }
+ }
+ }
+ }
+ Error::ErrorsWithoutPattern { path, msgs } => {
+ if let Some(path) = path.as_ref() {
+ let line = path.line();
+ let path = path.display();
+ let mut err =
+ github_actions::error(&path, format!("Unmatched diagnostics{revision}"))
+ .line(line);
+ for Message {
+ level,
+ message,
+ line_col: _,
+ } in msgs
+ {
+ writeln!(err, "{level:?}: {message}").unwrap();
+ }
+ } else {
+ let mut err = github_actions::error(
+ test_path,
+ format!("Unmatched diagnostics outside the testfile{revision}"),
+ );
+ for Message {
+ level,
+ message,
+ line_col: _,
+ } in msgs
+ {
+ writeln!(err, "{level:?}: {message}").unwrap();
+ }
+ }
+ }
+ Error::InvalidComment { msg, span } => {
+ let mut err = github_actions::error(test_path, format!("Could not parse comment"))
+ .line(span.line_start);
+ writeln!(err, "{msg}").unwrap();
+ }
+ Error::MultipleRevisionsWithResults { kind, lines } => {
+ github_actions::error(test_path, format!("multiple {kind} found")).line(lines[0]);
+ }
+ Error::Bug(_) => {}
+ Error::Aux {
+ path: aux_path,
+ errors,
+ line,
+ } => {
+ github_actions::error(test_path, format!("Aux build failed")).line(*line);
+ for error in errors {
+ gha_error(error, &aux_path.display().to_string(), "")
+ }
+ }
+ Error::Rustfix(error) => {
+ github_actions::error(
+ test_path,
+ format!("failed to apply suggestions with rustfix: {error}"),
+ );
+ }
+ }
+}
+
+/// Emits Github Actions Workspace commands to show the failures directly in the github diff view.
+/// If the const generic `GROUP` boolean is `true`, also emit `::group` commands.
+pub struct Gha<const GROUP: bool> {
+ /// Show a specific name for the final summary.
+ pub name: String,
+}
+
+#[derive(Clone)]
+struct PathAndRev<const GROUP: bool> {
+ path: PathBuf,
+ revision: String,
+}
+
+impl<const GROUP: bool> TestStatus for PathAndRev<GROUP> {
+ fn path(&self) -> &Path {
+ &self.path
+ }
+
+ fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> {
+ assert_eq!(self.revision, "");
+ Box::new(Self {
+ path: self.path.clone(),
+ revision: revision.to_owned(),
+ })
+ }
+
+ fn failed_test(&self, _cmd: &Command, _stderr: &[u8], _stdout: &[u8]) -> Box<dyn Debug> {
+ if GROUP {
+ Box::new(github_actions::group(format_args!(
+ "{}:{}",
+ self.path.display(),
+ self.revision
+ )))
+ } else {
+ Box::new(())
+ }
+ }
+
+ fn revision(&self) -> &str {
+ &self.revision
+ }
+
+ fn update_status(&self, _msg: String) {}
+}
+
+impl<const GROUP: bool> StatusEmitter for Gha<GROUP> {
+ fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> {
+ Box::new(PathAndRev::<GROUP> {
+ path,
+ revision: String::new(),
+ })
+ }
+
+ fn finalize(
+ &self,
+ _failures: usize,
+ succeeded: usize,
+ ignored: usize,
+ filtered: usize,
+ ) -> Box<dyn Summary> {
+ struct Summarizer<const GROUP: bool> {
+ failures: Vec<String>,
+ succeeded: usize,
+ ignored: usize,
+ filtered: usize,
+ name: String,
+ }
+
+ impl<const GROUP: bool> Summary for Summarizer<GROUP> {
+ fn test_failure(&mut self, status: &dyn TestStatus, errors: &Errors) {
+ let revision = if status.revision().is_empty() {
+ "".to_string()
+ } else {
+ format!(" (revision: {})", status.revision())
+ };
+ for error in errors {
+ gha_error(error, &status.path().display().to_string(), &revision);
+ }
+ self.failures
+ .push(format!("{}{revision}", status.path().display()));
+ }
+ }
+ impl<const GROUP: bool> Drop for Summarizer<GROUP> {
+ fn drop(&mut self) {
+ if let Some(mut file) = github_actions::summary() {
+ writeln!(file, "### {}", self.name).unwrap();
+ for line in &self.failures {
+ writeln!(file, "* {line}").unwrap();
+ }
+ writeln!(file).unwrap();
+ writeln!(file, "| failed | passed | ignored | filtered out |").unwrap();
+ writeln!(file, "| --- | --- | --- | --- |").unwrap();
+ writeln!(
+ file,
+ "| {} | {} | {} | {} |",
+ self.failures.len(),
+ self.succeeded,
+ self.ignored,
+ self.filtered,
+ )
+ .unwrap();
+ }
+ }
+ }
+
+ Box::new(Summarizer::<GROUP> {
+ failures: vec![],
+ succeeded,
+ ignored,
+ filtered,
+ name: self.name.clone(),
+ })
+ }
+}
+
+impl<T: TestStatus, U: TestStatus> TestStatus for (T, U) {
+ fn done(&self, result: &TestResult) {
+ self.0.done(result);
+ self.1.done(result);
+ }
+
+ fn failed_test<'a>(
+ &'a self,
+ cmd: &'a Command,
+ stderr: &'a [u8],
+ stdout: &'a [u8],
+ ) -> Box<dyn Debug + 'a> {
+ Box::new((
+ self.0.failed_test(cmd, stderr, stdout),
+ self.1.failed_test(cmd, stderr, stdout),
+ ))
+ }
+
+ fn path(&self) -> &Path {
+ let path = self.0.path();
+ assert_eq!(path, self.1.path());
+ path
+ }
+
+ fn revision(&self) -> &str {
+ let rev = self.0.revision();
+ assert_eq!(rev, self.1.revision());
+ rev
+ }
+
+ fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> {
+ Box::new((self.0.for_revision(revision), self.1.for_revision(revision)))
+ }
+
+ fn update_status(&self, msg: String) {
+ self.0.update_status(msg.clone());
+ self.1.update_status(msg)
+ }
+}
+
+impl<T: StatusEmitter, U: StatusEmitter> StatusEmitter for (T, U) {
+ fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> {
+ Box::new((
+ self.0.register_test(path.clone()),
+ self.1.register_test(path),
+ ))
+ }
+
+ fn finalize(
+ &self,
+ failures: usize,
+ succeeded: usize,
+ ignored: usize,
+ filtered: usize,
+ ) -> Box<dyn Summary> {
+ Box::new((
+ self.1.finalize(failures, succeeded, ignored, filtered),
+ self.0.finalize(failures, succeeded, ignored, filtered),
+ ))
+ }
+}
+
+impl<T: TestStatus + ?Sized> TestStatus for Box<T> {
+ fn done(&self, result: &TestResult) {
+ (**self).done(result);
+ }
+
+ fn path(&self) -> &Path {
+ (**self).path()
+ }
+
+ fn revision(&self) -> &str {
+ (**self).revision()
+ }
+
+ fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> {
+ (**self).for_revision(revision)
+ }
+
+ fn failed_test<'a>(
+ &'a self,
+ cmd: &'a Command,
+ stderr: &'a [u8],
+ stdout: &'a [u8],
+ ) -> Box<dyn Debug + 'a> {
+ (**self).failed_test(cmd, stderr, stdout)
+ }
+
+ fn update_status(&self, msg: String) {
+ (**self).update_status(msg)
+ }
+}
+
+impl<T: StatusEmitter + ?Sized> StatusEmitter for Box<T> {
+ fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> {
+ (**self).register_test(path)
+ }
+
+ fn finalize(
+ &self,
+ failures: usize,
+ succeeded: usize,
+ ignored: usize,
+ filtered: usize,
+ ) -> Box<dyn Summary> {
+ (**self).finalize(failures, succeeded, ignored, filtered)
+ }
+}
+
+impl Summary for (Box<dyn Summary>, Box<dyn Summary>) {
+ fn test_failure(&mut self, status: &dyn TestStatus, errors: &Errors) {
+ self.0.test_failure(status, errors);
+ self.1.test_failure(status, errors);
+ }
+}