1use super::RevisionStyle;
2use super::StatusEmitter;
3use super::Summary;
4use super::TestStatus;
5use crate::diagnostics::Level;
6use crate::diagnostics::Message;
7use crate::display;
8use crate::parser::Pattern;
9use crate::test_result::Errored;
10use crate::test_result::TestOk;
11use crate::test_result::TestResult;
12use crate::Error;
13use crate::Errors;
14use crate::Format;
15use annotate_snippets::Renderer;
16use annotate_snippets::Snippet;
17use colored::Colorize;
18#[cfg(feature = "indicatif")]
19use crossbeam_channel::{Sender, TryRecvError};
20#[cfg(feature = "indicatif")]
21use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
22use spanned::Span;
23use std::fmt::{Debug, Display};
24use std::io::Write as _;
25use std::path::Path;
26use std::path::PathBuf;
27
28#[cfg(feature = "indicatif")]
29use std::{
30 sync::{atomic::AtomicUsize, atomic::Ordering, Arc, Mutex},
31 thread::JoinHandle,
32 time::Duration,
33};
34
35#[derive(Clone, Copy)]
36enum OutputVerbosity {
37 Progress,
38 DiffOnly,
39 Full,
40}
41
42/// A human readable output emitter.
43#[derive(Clone)]
44pub 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")]
55struct JoinOnDrop(Mutex<Option<JoinHandle<()>>>);
56#[cfg(feature = "indicatif")]
57impl From<JoinHandle<()>> for JoinOnDrop {
58 fn from(handle: JoinHandle<()>) -> Self {
59 Self(Mutex::new(Some(handle)))
60 }
61}
62#[cfg(feature = "indicatif")]
63impl Drop for JoinOnDrop {
64 fn drop(&mut self) {
65 self.join();
66 }
67}
68
69#[cfg(feature = "indicatif")]
70impl 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)]
81enum 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
95impl 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
344impl 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
353struct 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
364impl 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
511impl 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
631fn 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)]
847fn 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