diff options
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.rs | 972 |
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); + } +} |