1 | use super::RevisionStyle; |
2 | use super::StatusEmitter; |
3 | use super::Summary; |
4 | use super::TestStatus; |
5 | use crate::diagnostics::Level; |
6 | use crate::diagnostics::Message; |
7 | use crate::display; |
8 | use crate::parser::Pattern; |
9 | use crate::test_result::Errored; |
10 | use crate::test_result::TestOk; |
11 | use crate::test_result::TestResult; |
12 | use crate::Error; |
13 | use crate::Errors; |
14 | use crate::Format; |
15 | use annotate_snippets::Renderer; |
16 | use annotate_snippets::Snippet; |
17 | use colored::Colorize; |
18 | #[cfg (feature = "indicatif" )] |
19 | use crossbeam_channel::{Sender, TryRecvError}; |
20 | #[cfg (feature = "indicatif" )] |
21 | use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; |
22 | use spanned::Span; |
23 | use std::fmt::{Debug, Display}; |
24 | use std::io::Write as _; |
25 | use std::path::Path; |
26 | use std::path::PathBuf; |
27 | |
28 | #[cfg (feature = "indicatif" )] |
29 | use std::{ |
30 | sync::{atomic::AtomicUsize, atomic::Ordering, Arc, Mutex}, |
31 | thread::JoinHandle, |
32 | time::Duration, |
33 | }; |
34 | |
35 | #[derive (Clone, Copy)] |
36 | enum OutputVerbosity { |
37 | Progress, |
38 | DiffOnly, |
39 | Full, |
40 | } |
41 | |
42 | /// A human readable output emitter. |
43 | #[derive (Clone)] |
44 | pub struct Text { |
45 | #[cfg (feature = "indicatif" )] |
46 | sender: Sender<Msg>, |
47 | progress: OutputVerbosity, |
48 | #[cfg (feature = "indicatif" )] |
49 | handle: Arc<JoinOnDrop>, |
50 | #[cfg (feature = "indicatif" )] |
51 | ids: Arc<AtomicUsize>, |
52 | } |
53 | |
54 | #[cfg (feature = "indicatif" )] |
55 | struct JoinOnDrop(Mutex<Option<JoinHandle<()>>>); |
56 | #[cfg (feature = "indicatif" )] |
57 | impl From<JoinHandle<()>> for JoinOnDrop { |
58 | fn from(handle: JoinHandle<()>) -> Self { |
59 | Self(Mutex::new(Some(handle))) |
60 | } |
61 | } |
62 | #[cfg (feature = "indicatif" )] |
63 | impl Drop for JoinOnDrop { |
64 | fn drop(&mut self) { |
65 | self.join(); |
66 | } |
67 | } |
68 | |
69 | #[cfg (feature = "indicatif" )] |
70 | impl JoinOnDrop { |
71 | fn join(&self) { |
72 | let Ok(Some(handle: JoinHandle<()>)) = self.0.try_lock().map(|mut g: MutexGuard<'_, Option>>| g.take()) else { |
73 | return; |
74 | }; |
75 | let _ = handle.join(); |
76 | } |
77 | } |
78 | |
79 | #[cfg (feature = "indicatif" )] |
80 | #[derive (Debug)] |
81 | enum Msg { |
82 | Pop { |
83 | new_leftover_msg: String, |
84 | id: usize, |
85 | }, |
86 | Push { |
87 | id: usize, |
88 | parent: usize, |
89 | msg: String, |
90 | }, |
91 | Finish, |
92 | Abort, |
93 | } |
94 | |
95 | impl Text { |
96 | fn start_thread(progress: OutputVerbosity) -> Self { |
97 | #[cfg (feature = "indicatif" )] |
98 | let (sender, receiver) = crossbeam_channel::unbounded(); |
99 | #[cfg (feature = "indicatif" )] |
100 | let handle = std::thread::spawn(move || { |
101 | let bars = MultiProgress::new(); |
102 | let progress = match progress { |
103 | OutputVerbosity::Progress => bars.add(ProgressBar::new(0)), |
104 | OutputVerbosity::DiffOnly | OutputVerbosity::Full => { |
105 | ProgressBar::with_draw_target(Some(0), ProgressDrawTarget::hidden()) |
106 | } |
107 | }; |
108 | |
109 | struct Thread { |
110 | parent: usize, |
111 | spinner: ProgressBar, |
112 | /// Used for sanity assertions only |
113 | done: bool, |
114 | } |
115 | |
116 | impl Debug for Thread { |
117 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
118 | f.debug_struct("Thread" ) |
119 | .field("parent" , &self.parent) |
120 | .field( |
121 | "spinner" , |
122 | &format_args!(" {}: {}" , self.spinner.prefix(), self.spinner.message()), |
123 | ) |
124 | .field("done" , &self.done) |
125 | .finish() |
126 | } |
127 | } |
128 | |
129 | struct ProgressHandler { |
130 | threads: Vec<Option<Thread>>, |
131 | aborted: bool, |
132 | bars: MultiProgress, |
133 | } |
134 | |
135 | impl ProgressHandler { |
136 | fn parents(&self, mut id: usize) -> impl Iterator<Item = usize> + '_ { |
137 | std::iter::from_fn(move || { |
138 | let parent = self.threads[id].as_ref().unwrap().parent; |
139 | if parent == 0 { |
140 | None |
141 | } else { |
142 | id = parent; |
143 | Some(parent) |
144 | } |
145 | }) |
146 | } |
147 | |
148 | fn root(&self, id: usize) -> usize { |
149 | self.parents(id).last().unwrap_or(id) |
150 | } |
151 | |
152 | fn tree(&self, id: usize) -> impl Iterator<Item = (usize, &Thread)> { |
153 | let root = self.root(id); |
154 | // No need to look at the entries before `root`, as child nodes |
155 | // are always after parent nodes. |
156 | self.threads |
157 | .iter() |
158 | .filter_map(|t| t.as_ref()) |
159 | .enumerate() |
160 | .skip(root - 1) |
161 | .filter(move |&(i, t)| { |
162 | root == if t.parent == 0 { |
163 | i |
164 | } else { |
165 | self.root(t.parent) |
166 | } |
167 | }) |
168 | } |
169 | |
170 | fn tree_done(&self, id: usize) -> bool { |
171 | self.tree(id).all(|(_, t)| t.done) |
172 | } |
173 | |
174 | fn pop(&mut self, new_leftover_msg: String, id: usize) { |
175 | assert_ne!(id, 0); |
176 | let Some(Some(thread)) = self.threads.get_mut(id) else { |
177 | // This can happen when a test was not run at all, because it failed directly during |
178 | // comment parsing. |
179 | return; |
180 | }; |
181 | thread.done = true; |
182 | let spinner = thread.spinner.clone(); |
183 | spinner.finish_with_message(new_leftover_msg); |
184 | let progress = &self.threads[0].as_ref().unwrap().spinner; |
185 | progress.inc(1); |
186 | if self.tree_done(id) { |
187 | for (_, thread) in self.tree(id) { |
188 | self.bars.remove(&thread.spinner); |
189 | if progress.is_hidden() { |
190 | self.bars |
191 | .println(format!( |
192 | " {} {}" , |
193 | thread.spinner.prefix(), |
194 | thread.spinner.message() |
195 | )) |
196 | .unwrap(); |
197 | } |
198 | } |
199 | } |
200 | } |
201 | |
202 | fn push(&mut self, parent: usize, id: usize, mut msg: String) { |
203 | assert!(parent < id); |
204 | self.threads[0].as_mut().unwrap().spinner.inc_length(1); |
205 | if self.threads.len() <= id { |
206 | self.threads.resize_with(id + 1, || None); |
207 | } |
208 | let parents = if parent == 0 { |
209 | 0 |
210 | } else { |
211 | self.parents(parent).count() + 1 |
212 | }; |
213 | for _ in 0..parents { |
214 | msg.insert_str(0, " " ); |
215 | } |
216 | let spinner = ProgressBar::new_spinner().with_prefix(msg); |
217 | let spinner = if parent == 0 { |
218 | self.bars.add(spinner) |
219 | } else { |
220 | let last = self |
221 | .threads |
222 | .iter() |
223 | .enumerate() |
224 | .rev() |
225 | .filter_map(|(i, t)| Some((i, t.as_ref()?))) |
226 | .find(|&(i, _)| self.parents(i).any(|p| p == parent)) |
227 | .map(|(_, thread)| thread) |
228 | .unwrap_or_else(|| self.threads[parent].as_ref().unwrap()); |
229 | self.bars.insert_after(&last.spinner, spinner) |
230 | }; |
231 | spinner.set_style( |
232 | ProgressStyle::with_template("{prefix} {spinner}{msg}" ).unwrap(), |
233 | ); |
234 | let thread = &mut self.threads[id]; |
235 | assert!(thread.is_none()); |
236 | let _ = thread.insert(Thread { |
237 | parent, |
238 | spinner, |
239 | done: false, |
240 | }); |
241 | } |
242 | |
243 | fn tick(&self) { |
244 | for thread in self.threads.iter().flatten() { |
245 | if !thread.done { |
246 | thread.spinner.tick(); |
247 | } |
248 | } |
249 | } |
250 | } |
251 | |
252 | impl Drop for ProgressHandler { |
253 | fn drop(&mut self) { |
254 | let progress = self.threads[0].as_ref().unwrap(); |
255 | for (key, thread) in self.threads.iter().skip(1).enumerate() { |
256 | if let Some(thread) = thread { |
257 | assert!( |
258 | thread.done, |
259 | " {key} ( {}: {}) not finished" , |
260 | thread.spinner.prefix(), |
261 | thread.spinner.message() |
262 | ); |
263 | } |
264 | } |
265 | if self.aborted { |
266 | progress.spinner.abandon(); |
267 | } else { |
268 | assert_eq!( |
269 | Some(progress.spinner.position()), |
270 | progress.spinner.length(), |
271 | " {:?}" , |
272 | self.threads |
273 | ); |
274 | progress.spinner.finish(); |
275 | } |
276 | } |
277 | } |
278 | |
279 | let mut handler = ProgressHandler { |
280 | threads: vec![Some(Thread { |
281 | parent: 0, |
282 | spinner: progress, |
283 | done: false, |
284 | })], |
285 | aborted: false, |
286 | bars, |
287 | }; |
288 | |
289 | 'outer: loop { |
290 | std::thread::sleep(Duration::from_millis(100)); |
291 | loop { |
292 | match receiver.try_recv() { |
293 | Ok(val) => match val { |
294 | Msg::Pop { |
295 | id, |
296 | new_leftover_msg, |
297 | } => { |
298 | handler.pop(new_leftover_msg, id); |
299 | } |
300 | |
301 | Msg::Push { parent, msg, id } => { |
302 | handler.push(parent, id, msg); |
303 | } |
304 | Msg::Finish => break 'outer, |
305 | Msg::Abort => handler.aborted = true, |
306 | }, |
307 | // Sender panicked, skip asserts |
308 | Err(TryRecvError::Disconnected) => return, |
309 | Err(TryRecvError::Empty) => break, |
310 | } |
311 | } |
312 | handler.tick() |
313 | } |
314 | }); |
315 | Self { |
316 | #[cfg (feature = "indicatif" )] |
317 | sender, |
318 | progress, |
319 | #[cfg (feature = "indicatif" )] |
320 | handle: Arc::new(handle.into()), |
321 | #[cfg (feature = "indicatif" )] |
322 | ids: Arc::new(AtomicUsize::new(1)), |
323 | } |
324 | } |
325 | |
326 | /// Print one line per test that gets run. |
327 | pub fn verbose() -> Self { |
328 | Self::start_thread(OutputVerbosity::Full) |
329 | } |
330 | /// Print one line per test that gets run. |
331 | pub fn diff() -> Self { |
332 | Self::start_thread(OutputVerbosity::DiffOnly) |
333 | } |
334 | /// Print a progress bar. |
335 | pub fn quiet() -> Self { |
336 | Self::start_thread(OutputVerbosity::Progress) |
337 | } |
338 | |
339 | fn is_full_output(&self) -> bool { |
340 | matches!(self.progress, OutputVerbosity::Full) |
341 | } |
342 | } |
343 | |
344 | impl From<Format> for Text { |
345 | fn from(format: Format) -> Self { |
346 | match format { |
347 | Format::Terse => Text::quiet(), |
348 | Format::Pretty => Text::verbose(), |
349 | } |
350 | } |
351 | } |
352 | |
353 | struct TextTest { |
354 | text: Text, |
355 | #[cfg (feature = "indicatif" )] |
356 | parent: usize, |
357 | #[cfg (feature = "indicatif" )] |
358 | id: usize, |
359 | path: PathBuf, |
360 | revision: String, |
361 | style: RevisionStyle, |
362 | } |
363 | |
364 | impl TestStatus for TextTest { |
365 | fn done(&self, result: &TestResult, aborted: bool) { |
366 | #[cfg (feature = "indicatif" )] |
367 | if aborted { |
368 | self.text.sender.send(Msg::Abort).unwrap(); |
369 | } |
370 | let result = match result { |
371 | _ if aborted => "aborted" .white(), |
372 | Ok(TestOk::Ok) => "ok" .green(), |
373 | Err(Errored { .. }) => "FAILED" .bright_red().bold(), |
374 | Ok(TestOk::Ignored) => "ignored (in-test comment)" .yellow(), |
375 | }; |
376 | let new_leftover_msg = format!("... {result}" ); |
377 | #[cfg (feature = "indicatif" )] |
378 | let print_immediately = ProgressDrawTarget::stdout().is_hidden(); |
379 | #[cfg (not(feature = "indicatif" ))] |
380 | let print_immediately = true; |
381 | if print_immediately { |
382 | match self.style { |
383 | RevisionStyle::Separate => println!(" {} {new_leftover_msg}" , self.revision), |
384 | RevisionStyle::Show => { |
385 | let revision = if self.revision.is_empty() { |
386 | String::new() |
387 | } else { |
388 | format!(" (revision ` {}`)" , self.revision) |
389 | }; |
390 | println!(" {}{revision} {new_leftover_msg}" , display(&self.path)); |
391 | } |
392 | } |
393 | std::io::stdout().flush().unwrap(); |
394 | } |
395 | #[cfg (feature = "indicatif" )] |
396 | self.text |
397 | .sender |
398 | .send(Msg::Pop { |
399 | id: self.id, |
400 | new_leftover_msg, |
401 | }) |
402 | .unwrap(); |
403 | } |
404 | |
405 | fn failed_test<'a>( |
406 | &self, |
407 | cmd: &str, |
408 | stderr: &'a [u8], |
409 | stdout: &'a [u8], |
410 | ) -> Box<dyn Debug + 'a> { |
411 | let maybe_revision = if self.revision.is_empty() { |
412 | String::new() |
413 | } else { |
414 | format!(" (revision ` {}`)" , self.revision) |
415 | }; |
416 | let text = format!( |
417 | " {} {}{}" , |
418 | "FAILED TEST:" .bright_red(), |
419 | display(&self.path), |
420 | maybe_revision |
421 | ); |
422 | |
423 | println!(); |
424 | println!(" {}" , text.bold().underline()); |
425 | println!("command: {cmd}" ); |
426 | println!(); |
427 | |
428 | if self.text.is_full_output() { |
429 | #[derive (Debug)] |
430 | struct Guard<'a> { |
431 | stderr: &'a [u8], |
432 | stdout: &'a [u8], |
433 | } |
434 | impl Drop for Guard<'_> { |
435 | fn drop(&mut self) { |
436 | println!(" {}" , "full stderr:" .bold()); |
437 | std::io::stdout().write_all(self.stderr).unwrap(); |
438 | println!(); |
439 | println!(" {}" , "full stdout:" .bold()); |
440 | std::io::stdout().write_all(self.stdout).unwrap(); |
441 | println!(); |
442 | println!(); |
443 | } |
444 | } |
445 | Box::new(Guard { stderr, stdout }) |
446 | } else { |
447 | Box::new(()) |
448 | } |
449 | } |
450 | |
451 | fn path(&self) -> &Path { |
452 | &self.path |
453 | } |
454 | |
455 | fn for_revision(&self, revision: &str, style: RevisionStyle) -> Box<dyn TestStatus> { |
456 | let text = Self { |
457 | text: self.text.clone(), |
458 | path: self.path.clone(), |
459 | #[cfg (feature = "indicatif" )] |
460 | parent: self.id, |
461 | #[cfg (feature = "indicatif" )] |
462 | id: self.text.ids.fetch_add(1, Ordering::Relaxed), |
463 | revision: revision.to_owned(), |
464 | style, |
465 | }; |
466 | // We already created the base entry |
467 | #[cfg (feature = "indicatif" )] |
468 | if !revision.is_empty() { |
469 | self.text |
470 | .sender |
471 | .send(Msg::Push { |
472 | parent: text.parent, |
473 | id: text.id, |
474 | msg: text.revision.clone(), |
475 | }) |
476 | .unwrap(); |
477 | } |
478 | |
479 | Box::new(text) |
480 | } |
481 | |
482 | fn for_path(&self, path: &Path) -> Box<dyn TestStatus> { |
483 | let text = Self { |
484 | text: self.text.clone(), |
485 | path: path.to_path_buf(), |
486 | #[cfg (feature = "indicatif" )] |
487 | parent: self.id, |
488 | #[cfg (feature = "indicatif" )] |
489 | id: self.text.ids.fetch_add(1, Ordering::Relaxed), |
490 | revision: String::new(), |
491 | style: RevisionStyle::Show, |
492 | }; |
493 | |
494 | #[cfg (feature = "indicatif" )] |
495 | self.text |
496 | .sender |
497 | .send(Msg::Push { |
498 | id: text.id, |
499 | parent: text.parent, |
500 | msg: display(path), |
501 | }) |
502 | .unwrap(); |
503 | Box::new(text) |
504 | } |
505 | |
506 | fn revision(&self) -> &str { |
507 | &self.revision |
508 | } |
509 | } |
510 | |
511 | impl StatusEmitter for Text { |
512 | fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> { |
513 | #[cfg (feature = "indicatif" )] |
514 | let id = self.ids.fetch_add(1, Ordering::Relaxed); |
515 | #[cfg (feature = "indicatif" )] |
516 | self.sender |
517 | .send(Msg::Push { |
518 | id, |
519 | parent: 0, |
520 | msg: display(&path), |
521 | }) |
522 | .unwrap(); |
523 | Box::new(TextTest { |
524 | text: self.clone(), |
525 | #[cfg (feature = "indicatif" )] |
526 | parent: 0, |
527 | #[cfg (feature = "indicatif" )] |
528 | id, |
529 | path, |
530 | revision: String::new(), |
531 | style: RevisionStyle::Show, |
532 | }) |
533 | } |
534 | |
535 | fn finalize( |
536 | &self, |
537 | _failures: usize, |
538 | succeeded: usize, |
539 | ignored: usize, |
540 | filtered: usize, |
541 | aborted: bool, |
542 | ) -> Box<dyn Summary> { |
543 | #[cfg (feature = "indicatif" )] |
544 | self.sender.send(Msg::Finish).unwrap(); |
545 | |
546 | #[cfg (feature = "indicatif" )] |
547 | self.handle.join(); |
548 | #[cfg (feature = "indicatif" )] |
549 | if !ProgressDrawTarget::stdout().is_hidden() { |
550 | // The progress bars do not have a trailing newline, so let's |
551 | // add it here. |
552 | println!(); |
553 | } |
554 | // Print all errors in a single thread to show reliable output |
555 | struct Summarizer { |
556 | failures: Vec<String>, |
557 | succeeded: usize, |
558 | ignored: usize, |
559 | filtered: usize, |
560 | aborted: bool, |
561 | } |
562 | |
563 | impl Summary for Summarizer { |
564 | fn test_failure(&mut self, status: &dyn TestStatus, errors: &Errors) { |
565 | for error in errors { |
566 | print_error(error, status.path()); |
567 | } |
568 | |
569 | self.failures.push(if status.revision().is_empty() { |
570 | format!(" {}" , display(status.path())) |
571 | } else { |
572 | format!( |
573 | " {} (revision {})" , |
574 | display(status.path()), |
575 | status.revision() |
576 | ) |
577 | }); |
578 | } |
579 | } |
580 | |
581 | impl Drop for Summarizer { |
582 | fn drop(&mut self) { |
583 | if self.failures.is_empty() { |
584 | println!(); |
585 | if self.aborted { |
586 | print!("test result: cancelled." ); |
587 | } else { |
588 | print!("test result: {}." , "ok" .green()); |
589 | } |
590 | } else { |
591 | println!(" {}" , "FAILURES:" .bright_red().underline().bold()); |
592 | for line in &self.failures { |
593 | println!(" {line}" ); |
594 | } |
595 | println!(); |
596 | print!("test result: {}." , "FAIL" .bright_red()); |
597 | print!(" {} failed" , self.failures.len().to_string().green()); |
598 | if self.succeeded > 0 || self.ignored > 0 || self.filtered > 0 { |
599 | print!(";" ); |
600 | } |
601 | } |
602 | if self.succeeded > 0 { |
603 | print!(" {} passed" , self.succeeded.to_string().green()); |
604 | if self.ignored > 0 || self.filtered > 0 { |
605 | print!(";" ); |
606 | } |
607 | } |
608 | if self.ignored > 0 { |
609 | print!(" {} ignored" , self.ignored.to_string().yellow()); |
610 | if self.filtered > 0 { |
611 | print!(";" ); |
612 | } |
613 | } |
614 | if self.filtered > 0 { |
615 | print!(" {} filtered out" , self.filtered.to_string().yellow()); |
616 | } |
617 | println!(); |
618 | println!(); |
619 | } |
620 | } |
621 | Box::new(Summarizer { |
622 | failures: vec![], |
623 | succeeded, |
624 | ignored, |
625 | filtered, |
626 | aborted, |
627 | }) |
628 | } |
629 | } |
630 | |
631 | fn print_error(error: &Error, path: &Path) { |
632 | /// Every error starts with a header like that, to make them all easy to find. |
633 | /// It is made to look like the headers printed for spanned errors. |
634 | fn print_error_header(msg: impl Display) { |
635 | let text = format!(" {} {msg}" , "error:" .bright_red()); |
636 | println!(" {}" , text.bold()); |
637 | } |
638 | |
639 | match error { |
640 | Error::ExitStatus { |
641 | status, |
642 | expected, |
643 | reason, |
644 | } => { |
645 | // `status` prints as `exit status: N`. |
646 | create_error( |
647 | format!("test got {status}, but expected {expected}" ), |
648 | &[&[(reason, reason.span.clone())]], |
649 | path, |
650 | ) |
651 | } |
652 | Error::Command { kind, status } => { |
653 | // `status` prints as `exit status: N`. |
654 | print_error_header(format_args!(" {kind} failed with {status}" )); |
655 | } |
656 | Error::PatternNotFound { |
657 | pattern, |
658 | expected_line, |
659 | } => { |
660 | let line = match expected_line { |
661 | Some(line) => format!("on line {line}" ), |
662 | None => format!("outside the testfile" ), |
663 | }; |
664 | let msg = match &**pattern { |
665 | Pattern::SubString(s) => { |
666 | format!("` {s}` not found in diagnostics {line}" ) |
667 | } |
668 | Pattern::Regex(r) => { |
669 | format!("`/ {r}/` does not match diagnostics {line}" ,) |
670 | } |
671 | }; |
672 | // This will print a suitable error header. |
673 | create_error( |
674 | msg, |
675 | &[&[("expected because of this pattern" , pattern.span())]], |
676 | path, |
677 | ); |
678 | } |
679 | Error::CodeNotFound { |
680 | code, |
681 | expected_line, |
682 | } => { |
683 | let line = match expected_line { |
684 | Some(line) => format!("on line {line}" ), |
685 | None => format!("outside the testfile" ), |
686 | }; |
687 | create_error( |
688 | format!("diagnostic code ` {}` not found {line}" , &**code), |
689 | &[&[("expected because of this pattern" , code.span())]], |
690 | path, |
691 | ); |
692 | } |
693 | Error::NoPatternsFound => { |
694 | print_error_header("expected error patterns, but found none" ); |
695 | } |
696 | Error::PatternFoundInPassTest { mode, span } => { |
697 | let annot = [("expected because of this annotation" , span.clone())]; |
698 | let mut lines: Vec<&[_]> = vec![&annot]; |
699 | let annot = [("expected because of this mode change" , mode.clone())]; |
700 | if !mode.is_dummy() { |
701 | lines.push(&annot) |
702 | } |
703 | // This will print a suitable error header. |
704 | create_error("error pattern found in pass test" , &lines, path); |
705 | } |
706 | Error::OutputDiffers { |
707 | path: output_path, |
708 | actual, |
709 | output, |
710 | expected, |
711 | bless_command, |
712 | } => { |
713 | let bless = || { |
714 | if let Some(bless_command) = bless_command { |
715 | println!( |
716 | "Execute ` {}` to update ` {}` to the actual output" , |
717 | bless_command, |
718 | display(output_path) |
719 | ); |
720 | } |
721 | }; |
722 | if expected.is_empty() { |
723 | print_error_header("no output was expected" ); |
724 | bless(); |
725 | println!( |
726 | " {}" , |
727 | format!( |
728 | "+++ < {} output>" , |
729 | output_path.extension().unwrap().to_str().unwrap() |
730 | ) |
731 | .green() |
732 | ); |
733 | println!(" {}" , String::from_utf8_lossy(output)); |
734 | } else if output.is_empty() { |
735 | print_error_header("no output was emitted" ); |
736 | if let Some(bless_command) = bless_command { |
737 | println!( |
738 | "Execute ` {}` to remove ` {}`" , |
739 | bless_command, |
740 | display(output_path) |
741 | ); |
742 | } |
743 | } else { |
744 | print_error_header("actual output differed from expected" ); |
745 | bless(); |
746 | println!(" {}" , format!("--- {}" , display(output_path)).red()); |
747 | println!( |
748 | " {}" , |
749 | format!( |
750 | "+++ < {} output>" , |
751 | output_path.extension().unwrap().to_str().unwrap() |
752 | ) |
753 | .green() |
754 | ); |
755 | crate::diff::print_diff(expected, actual); |
756 | |
757 | println!( |
758 | "Full unnormalized output: \n{}" , |
759 | String::from_utf8_lossy(output) |
760 | ); |
761 | } |
762 | } |
763 | Error::ErrorsWithoutPattern { path, msgs } => { |
764 | if let Some((path, _)) = path.as_ref() { |
765 | let msgs = msgs |
766 | .iter() |
767 | .map(|msg| { |
768 | let text = match (&msg.code, msg.level) { |
769 | (Some(code), Level::Error) => { |
770 | format!("Error[ {code}]: {}" , msg.message) |
771 | } |
772 | _ => format!(" {:?}: {}" , msg.level, msg.message), |
773 | }; |
774 | (text, msg.span.clone().unwrap_or_default()) |
775 | }) |
776 | .collect::<Vec<_>>(); |
777 | // This will print a suitable error header. |
778 | create_error( |
779 | format!("there were {} unmatched diagnostics" , msgs.len()), |
780 | &[&msgs |
781 | .iter() |
782 | .map(|(msg, lc)| (msg.as_ref(), lc.clone())) |
783 | .collect::<Vec<_>>()], |
784 | path, |
785 | ); |
786 | } else { |
787 | print_error_header(format_args!( |
788 | "there were {} unmatched diagnostics that occurred outside the testfile and had no pattern" , |
789 | msgs.len(), |
790 | )); |
791 | for Message { |
792 | level, |
793 | message, |
794 | line: _, |
795 | code: _, |
796 | span: _, |
797 | } in msgs |
798 | { |
799 | println!(" {level:?}: {message}" ) |
800 | } |
801 | } |
802 | } |
803 | Error::InvalidComment { msg, span } => { |
804 | // This will print a suitable error header. |
805 | create_error(msg, &[&[("" , span.clone())]], path) |
806 | } |
807 | Error::MultipleRevisionsWithResults { kind, lines } => { |
808 | let title = format!("multiple {kind} found" ); |
809 | // This will print a suitable error header. |
810 | create_error( |
811 | title, |
812 | &lines.iter().map(|_line| &[] as &[_]).collect::<Vec<_>>(), |
813 | path, |
814 | ) |
815 | } |
816 | Error::Bug(msg) => { |
817 | print_error_header("a bug in `ui_test` occurred" ); |
818 | println!(" {msg}" ); |
819 | } |
820 | Error::Aux { |
821 | path: aux_path, |
822 | errors, |
823 | } => { |
824 | create_error( |
825 | "aux build failed" , |
826 | &[&[(&path.display().to_string(), aux_path.span.clone())]], |
827 | &aux_path.span.file, |
828 | ); |
829 | for error in errors { |
830 | print_error(error, aux_path); |
831 | } |
832 | } |
833 | Error::Rustfix(error) => { |
834 | print_error_header(format_args!( |
835 | "failed to apply suggestions for {} with rustfix" , |
836 | display(path) |
837 | )); |
838 | println!(" {error}" ); |
839 | println!("Add //@no-rustfix to the test file to ignore rustfix suggestions" ); |
840 | } |
841 | Error::ConfigError(msg) => println!(" {msg}" ), |
842 | } |
843 | println!(); |
844 | } |
845 | |
846 | #[allow (clippy::type_complexity)] |
847 | fn create_error(s: impl AsRef<str>, lines: &[&[(&str, Span)]], file: &Path) { |
848 | let source = std::fs::read_to_string(file).unwrap(); |
849 | let file = display(file); |
850 | let mut msg = annotate_snippets::Level::Error.title(s.as_ref()); |
851 | for &label in lines { |
852 | let annotations = label |
853 | .iter() |
854 | .filter(|(_, span)| !span.is_dummy()) |
855 | .map(|(label, span)| { |
856 | annotate_snippets::Level::Error |
857 | .span(span.bytes.clone()) |
858 | .label(label) |
859 | }) |
860 | .collect::<Vec<_>>(); |
861 | if !annotations.is_empty() { |
862 | let snippet = Snippet::source(&source) |
863 | .fold(true) |
864 | .origin(&file) |
865 | .annotations(annotations); |
866 | msg = msg.snippet(snippet); |
867 | } |
868 | let footer = label |
869 | .iter() |
870 | .filter(|(_, span)| span.is_dummy()) |
871 | .map(|(label, _)| annotate_snippets::Level::Note.title(label)); |
872 | msg = msg.footers(footer); |
873 | } |
874 | let renderer = if colored::control::SHOULD_COLORIZE.should_colorize() { |
875 | Renderer::styled() |
876 | } else { |
877 | Renderer::plain() |
878 | }; |
879 | println!(" {}" , renderer.render(msg)); |
880 | } |
881 | |