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