| 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 | |