]> git.lizzy.rs Git - rust.git/blob - ui_test/src/lib.rs
Introduce a proper error handling framework
[rust.git] / ui_test / src / lib.rs
1 #![allow(clippy::enum_variant_names, clippy::useless_format, clippy::too_many_arguments)]
2
3 use std::collections::VecDeque;
4 use std::fmt::Write;
5 use std::path::{Path, PathBuf};
6 use std::process::{Command, ExitStatus};
7 use std::sync::atomic::{AtomicUsize, Ordering};
8 use std::sync::Mutex;
9
10 use colored::*;
11 use comments::ErrorMatch;
12 use regex::Regex;
13 use rustc_stderr::{Level, Message};
14 use color_eyre::eyre::Result;
15 pub use color_eyre;
16
17 use crate::comments::{Comments, Condition};
18
19 mod comments;
20 mod rustc_stderr;
21 #[cfg(test)]
22 mod tests;
23
24 #[derive(Debug)]
25 pub struct Config {
26     /// Arguments passed to the binary that is executed.
27     pub args: Vec<String>,
28     /// `None` to run on the host, otherwise a target triple
29     pub target: Option<String>,
30     /// Filters applied to stderr output before processing it
31     pub stderr_filters: Filter,
32     /// Filters applied to stdout output before processing it
33     pub stdout_filters: Filter,
34     /// The folder in which to start searching for .rs files
35     pub root_dir: PathBuf,
36     pub mode: Mode,
37     pub program: PathBuf,
38     pub output_conflict_handling: OutputConflictHandling,
39     /// Only run tests with one of these strings in their path/name
40     pub path_filter: Vec<String>,
41 }
42
43 #[derive(Debug)]
44 pub enum OutputConflictHandling {
45     /// The default: emit a diff of the expected/actual output.
46     Error,
47     /// Ignore mismatches in the stderr/stdout files.
48     Ignore,
49     /// Instead of erroring if the stderr/stdout differs from the expected
50     /// automatically replace it with the found output (after applying filters).
51     Bless,
52 }
53
54 pub type Filter = Vec<(Regex, &'static str)>;
55
56 pub fn run_tests(config: Config) -> Result<()> {
57     eprintln!("   Compiler flags: {:?}", config.args);
58
59     // Get the triple with which to run the tests
60     let target = config.target.clone().unwrap_or_else(|| config.get_host());
61
62     // A channel for files to process
63     let (submit, receive) = crossbeam::channel::unbounded();
64
65     // Some statistics and failure reports.
66     let failures = Mutex::new(vec![]);
67     let succeeded = AtomicUsize::default();
68     let ignored = AtomicUsize::default();
69     let filtered = AtomicUsize::default();
70
71     crossbeam::scope(|s| {
72         // Create a thread that is in charge of walking the directory and submitting jobs.
73         // It closes the channel when it is done.
74         s.spawn(|_| {
75             let mut todo = VecDeque::new();
76             todo.push_back(config.root_dir.clone());
77             while let Some(path) = todo.pop_front() {
78                 if path.is_dir() {
79                     // Enqueue everything inside this directory.
80                     // We want it sorted, to have some control over scheduling of slow tests.
81                     let mut entries =
82                         std::fs::read_dir(path).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
83                     entries.sort_by_key(|e| e.file_name());
84                     for entry in entries {
85                         todo.push_back(entry.path());
86                     }
87                 } else if path.extension().map(|ext| ext == "rs").unwrap_or(false) {
88                     // Forward .rs files to the test workers.
89                     submit.send(path).unwrap();
90                 }
91             }
92             // There will be no more jobs. This signals the workers to quit.
93             // (This also ensures `submit` is moved into this closure.)
94             drop(submit);
95         });
96
97         // Create N worker threads that receive files to test.
98         for _ in 0..std::thread::available_parallelism().unwrap().get() {
99             s.spawn(|_| -> Result<()> {
100                 for path in &receive {
101                     if !config.path_filter.is_empty() {
102                         let path_display = path.display().to_string();
103                         if !config.path_filter.iter().any(|filter| path_display.contains(filter)) {
104                             filtered.fetch_add(1, Ordering::Relaxed);
105                             continue;
106                         }
107                     }
108                     let comments = Comments::parse_file(&path)?;
109                     // Ignore file if only/ignore rules do (not) apply
110                     if !test_file_conditions(&comments, &target) {
111                         ignored.fetch_add(1, Ordering::Relaxed);
112                         eprintln!(
113                             "{} ... {}",
114                             path.display(),
115                             "ignored (in-test comment)".yellow()
116                         );
117                         continue;
118                     }
119                     // Run the test for all revisions
120                     for revision in
121                         comments.revisions.clone().unwrap_or_else(|| vec![String::new()])
122                     {
123                         let (m, errors, stderr) =
124                             run_test(&path, &config, &target, &revision, &comments);
125
126                         // Using a single `eprintln!` to prevent messages from threads from getting intermingled.
127                         let mut msg = format!("{} ", path.display());
128                         if !revision.is_empty() {
129                             write!(msg, "(revision `{revision}`) ").unwrap();
130                         }
131                         write!(msg, "... ").unwrap();
132                         if errors.is_empty() {
133                             eprintln!("{msg}{}", "ok".green());
134                             succeeded.fetch_add(1, Ordering::Relaxed);
135                         } else {
136                             eprintln!("{msg}{}", "FAILED".red().bold());
137                             failures.lock().unwrap().push((
138                                 path.clone(),
139                                 m,
140                                 revision,
141                                 errors,
142                                 stderr,
143                             ));
144                         }
145                     }
146                 }
147                 Ok(())
148             });
149         }
150     })
151     .unwrap();
152
153     // Print all errors in a single thread to show reliable output
154     let failures = failures.into_inner().unwrap();
155     let succeeded = succeeded.load(Ordering::Relaxed);
156     let ignored = ignored.load(Ordering::Relaxed);
157     let filtered = filtered.load(Ordering::Relaxed);
158     if !failures.is_empty() {
159         for (path, miri, revision, errors, stderr) in &failures {
160             eprintln!();
161             eprint!("{}", path.display().to_string().underline());
162             if !revision.is_empty() {
163                 eprint!(" (revision `{}`)", revision);
164             }
165             eprint!(" {}", "FAILED".red());
166             eprintln!();
167             eprintln!("command: {:?}", miri);
168             eprintln!();
169             let mut dump_stderr = true;
170             for error in errors {
171                 match error {
172                     Error::ExitStatus(mode, exit_status) => eprintln!("{mode:?} got {exit_status}"),
173                     Error::PatternNotFound { pattern, definition_line } => {
174                         eprintln!("`{pattern}` {} in stderr output", "not found".red());
175                         eprintln!(
176                             "expected because of pattern here: {}:{definition_line}",
177                             path.display().to_string().bold()
178                         );
179                     }
180                     Error::NoPatternsFound => {
181                         eprintln!("{}", "no error patterns found in failure test".red());
182                     }
183                     Error::PatternFoundInPassTest =>
184                         eprintln!("{}", "error pattern found in success test".red()),
185                     Error::OutputDiffers { path, actual, expected } => {
186                         if path.extension().unwrap() == "stderr" {
187                             dump_stderr = false;
188                         }
189                         eprintln!("actual output differed from expected {}", path.display());
190                         eprintln!("{}", pretty_assertions::StrComparison::new(expected, actual));
191                         eprintln!()
192                     }
193                     Error::ErrorsWithoutPattern { path: None, msgs } => {
194                         eprintln!(
195                             "There were {} unmatched diagnostics that occurred outside the testfile and had not pattern",
196                             msgs.len(),
197                         );
198                         for Message { level, message } in msgs {
199                             eprintln!("    {level:?}: {message}")
200                         }
201                     }
202                     Error::ErrorsWithoutPattern { path: Some((path, line)), msgs } => {
203                         eprintln!(
204                             "There were {} unmatched diagnostics at {}:{line}",
205                             msgs.len(),
206                             path.display()
207                         );
208                         for Message { level, message } in msgs {
209                             eprintln!("    {level:?}: {message}")
210                         }
211                     }
212                     Error::ErrorPatternWithoutErrorAnnotation(path, line) => {
213                         eprintln!(
214                             "Annotation at {}:{line} matched an error diagnostic but did not have `ERROR` before its message",
215                             path.display()
216                         );
217                     }
218                 }
219                 eprintln!();
220             }
221             // Unless we already dumped the stderr via an OutputDiffers diff, let's dump it here.
222             if dump_stderr {
223                 eprintln!("actual stderr:");
224                 eprintln!("{}", stderr);
225                 eprintln!();
226             }
227         }
228         eprintln!("{}", "failures:".red().underline());
229         for (path, _miri, _revision, _errors, _stderr) in &failures {
230             eprintln!("    {}", path.display());
231         }
232         eprintln!();
233         eprintln!(
234             "test result: {}. {} tests failed, {} tests passed, {} ignored, {} filtered out",
235             "FAIL".red(),
236             failures.len().to_string().red().bold(),
237             succeeded.to_string().green(),
238             ignored.to_string().yellow(),
239             filtered.to_string().yellow(),
240         );
241         std::process::exit(1);
242     }
243     eprintln!();
244     eprintln!(
245         "test result: {}. {} tests passed, {} ignored, {} filtered out",
246         "ok".green(),
247         succeeded.to_string().green(),
248         ignored.to_string().yellow(),
249         filtered.to_string().yellow(),
250     );
251     eprintln!();
252     Ok(())
253 }
254
255 #[derive(Debug)]
256 enum Error {
257     /// Got an invalid exit status for the given mode.
258     ExitStatus(Mode, ExitStatus),
259     PatternNotFound {
260         pattern: String,
261         definition_line: usize,
262     },
263     /// A ui test checking for failure does not have any failure patterns
264     NoPatternsFound,
265     /// A ui test checking for success has failure patterns
266     PatternFoundInPassTest,
267     /// Stderr/Stdout differed from the `.stderr`/`.stdout` file present.
268     OutputDiffers {
269         path: PathBuf,
270         actual: String,
271         expected: String,
272     },
273     ErrorsWithoutPattern {
274         msgs: Vec<Message>,
275         path: Option<(PathBuf, usize)>,
276     },
277     ErrorPatternWithoutErrorAnnotation(PathBuf, usize),
278 }
279
280 type Errors = Vec<Error>;
281
282 fn run_test(
283     path: &Path,
284     config: &Config,
285     target: &str,
286     revision: &str,
287     comments: &Comments,
288 ) -> (Command, Errors, String) {
289     // Run miri
290     let mut miri = Command::new(&config.program);
291     miri.args(config.args.iter());
292     miri.arg(path);
293     if !revision.is_empty() {
294         miri.arg(format!("--cfg={revision}"));
295     }
296     miri.arg("--error-format=json");
297     for arg in &comments.compile_flags {
298         miri.arg(arg);
299     }
300     for (k, v) in &comments.env_vars {
301         miri.env(k, v);
302     }
303     let output = miri.output().expect("could not execute miri");
304     let mut errors = config.mode.ok(output.status);
305     let stderr = check_test_result(
306         path,
307         config,
308         target,
309         revision,
310         comments,
311         &mut errors,
312         &output.stdout,
313         &output.stderr,
314     );
315     (miri, errors, stderr)
316 }
317
318 fn check_test_result(
319     path: &Path,
320     config: &Config,
321     target: &str,
322     revision: &str,
323     comments: &Comments,
324     errors: &mut Errors,
325     stdout: &[u8],
326     stderr: &[u8],
327 ) -> String {
328     // Always remove annotation comments from stderr.
329     let diagnostics = rustc_stderr::process(path, stderr);
330     let stdout = std::str::from_utf8(stdout).unwrap();
331     // Check output files (if any)
332     let revised = |extension: &str| {
333         if revision.is_empty() {
334             extension.to_string()
335         } else {
336             format!("{}.{}", revision, extension)
337         }
338     };
339     // Check output files against actual output
340     check_output(
341         &diagnostics.rendered,
342         path,
343         errors,
344         revised("stderr"),
345         target,
346         &config.stderr_filters,
347         config,
348         comments,
349     );
350     check_output(
351         stdout,
352         path,
353         errors,
354         revised("stdout"),
355         target,
356         &config.stdout_filters,
357         config,
358         comments,
359     );
360     // Check error annotations in the source against output
361     check_annotations(
362         diagnostics.messages,
363         diagnostics.messages_from_unknown_file_or_line,
364         path,
365         errors,
366         config,
367         revision,
368         comments,
369     );
370     diagnostics.rendered
371 }
372
373 fn check_annotations(
374     mut messages: Vec<Vec<Message>>,
375     mut messages_from_unknown_file_or_line: Vec<Message>,
376     path: &Path,
377     errors: &mut Errors,
378     config: &Config,
379     revision: &str,
380     comments: &Comments,
381 ) {
382     if let Some((ref error_pattern, definition_line)) = comments.error_pattern {
383         // first check the diagnostics messages outside of our file. We check this first, so that
384         // you can mix in-file annotations with //@error-pattern annotations, even if there is overlap
385         // in the messages.
386         if let Some(i) = messages_from_unknown_file_or_line
387             .iter()
388             .position(|msg| msg.message.contains(error_pattern))
389         {
390             messages_from_unknown_file_or_line.remove(i);
391         } else {
392             errors.push(Error::PatternNotFound {
393                 pattern: error_pattern.to_string(),
394                 definition_line,
395             });
396         }
397     }
398
399     // The order on `Level` is such that `Error` is the highest level.
400     // We will ensure that *all* diagnostics of level at least `lowest_annotation_level`
401     // are matched.
402     let mut lowest_annotation_level = Level::Error;
403     for &ErrorMatch { ref matched, revision: ref rev, definition_line, line, level } in
404         &comments.error_matches
405     {
406         if let Some(rev) = rev {
407             if rev != revision {
408                 continue;
409             }
410         }
411         if let Some(level) = level {
412             // If we found a diagnostic with a level annotation, make sure that all
413             // diagnostics of that level have annotations, even if we don't end up finding a matching diagnostic
414             // for this pattern.
415             lowest_annotation_level = std::cmp::min(lowest_annotation_level, level);
416         }
417
418         if let Some(msgs) = messages.get_mut(line) {
419             let found = msgs.iter().position(|msg| {
420                 msg.message.contains(matched)
421                     // in case there is no level on the annotation, match any level.
422                     && level.map_or(true, |level| {
423                         msg.level == level
424                     })
425             });
426             if let Some(found) = found {
427                 let msg = msgs.remove(found);
428                 if msg.level == Level::Error && level.is_none() {
429                     errors
430                         .push(Error::ErrorPatternWithoutErrorAnnotation(path.to_path_buf(), line));
431                 }
432                 continue;
433             }
434         }
435
436         errors.push(Error::PatternNotFound { pattern: matched.to_string(), definition_line });
437     }
438
439     let filter = |msgs: Vec<Message>| -> Vec<_> {
440         msgs.into_iter().filter(|msg| msg.level >= lowest_annotation_level).collect()
441     };
442
443     let messages_from_unknown_file_or_line = filter(messages_from_unknown_file_or_line);
444     if !messages_from_unknown_file_or_line.is_empty() {
445         errors.push(Error::ErrorsWithoutPattern {
446             path: None,
447             msgs: messages_from_unknown_file_or_line,
448         });
449     }
450
451     for (line, msgs) in messages.into_iter().enumerate() {
452         let msgs = filter(msgs);
453         if !msgs.is_empty() {
454             errors
455                 .push(Error::ErrorsWithoutPattern { path: Some((path.to_path_buf(), line)), msgs });
456         }
457     }
458
459     match (config.mode, comments.error_pattern.is_some() || !comments.error_matches.is_empty()) {
460         (Mode::Pass, true) | (Mode::Panic, true) => errors.push(Error::PatternFoundInPassTest),
461         (Mode::Fail, false) => errors.push(Error::NoPatternsFound),
462         _ => {}
463     }
464 }
465
466 fn check_output(
467     output: &str,
468     path: &Path,
469     errors: &mut Errors,
470     kind: String,
471     target: &str,
472     filters: &Filter,
473     config: &Config,
474     comments: &Comments,
475 ) {
476     let output = normalize(path, output, filters, comments);
477     let path = output_path(path, comments, kind, target);
478     match config.output_conflict_handling {
479         OutputConflictHandling::Bless =>
480             if output.is_empty() {
481                 let _ = std::fs::remove_file(path);
482             } else {
483                 std::fs::write(path, &output).unwrap();
484             },
485         OutputConflictHandling::Error => {
486             let expected_output = std::fs::read_to_string(&path).unwrap_or_default();
487             if output != expected_output {
488                 errors.push(Error::OutputDiffers {
489                     path,
490                     actual: output,
491                     expected: expected_output,
492                 });
493             }
494         }
495         OutputConflictHandling::Ignore => {}
496     }
497 }
498
499 fn output_path(path: &Path, comments: &Comments, kind: String, target: &str) -> PathBuf {
500     if comments.stderr_per_bitwidth {
501         return path.with_extension(format!("{}bit.{kind}", get_pointer_width(target)));
502     }
503     path.with_extension(kind)
504 }
505
506 fn test_condition(condition: &Condition, target: &str) -> bool {
507     match condition {
508         Condition::Bitwidth(bits) => get_pointer_width(target) == *bits,
509         Condition::Target(t) => target.contains(t),
510     }
511 }
512
513 /// Returns whether according to the in-file conditions, this file should be run.
514 fn test_file_conditions(comments: &Comments, target: &str) -> bool {
515     if comments.ignore.iter().any(|c| test_condition(c, target)) {
516         return false;
517     }
518     comments.only.iter().all(|c| test_condition(c, target))
519 }
520
521 // Taken 1:1 from compiletest-rs
522 fn get_pointer_width(triple: &str) -> u8 {
523     if (triple.contains("64") && !triple.ends_with("gnux32") && !triple.ends_with("gnu_ilp32"))
524         || triple.starts_with("s390x")
525     {
526         64
527     } else if triple.starts_with("avr") {
528         16
529     } else {
530         32
531     }
532 }
533
534 fn normalize(path: &Path, text: &str, filters: &Filter, comments: &Comments) -> String {
535     // Useless paths
536     let mut text = text.replace(&path.parent().unwrap().display().to_string(), "$DIR");
537     if let Some(lib_path) = option_env!("RUSTC_LIB_PATH") {
538         text = text.replace(lib_path, "RUSTLIB");
539     }
540
541     for (regex, replacement) in filters.iter() {
542         text = regex.replace_all(&text, *replacement).to_string();
543     }
544
545     for (from, to) in &comments.normalize_stderr {
546         text = from.replace_all(&text, to).to_string();
547     }
548     text
549 }
550
551 impl Config {
552     fn get_host(&self) -> String {
553         rustc_version::VersionMeta::for_command(std::process::Command::new(&self.program))
554             .expect("failed to parse rustc version info")
555             .host
556     }
557 }
558
559 #[derive(Copy, Clone, Debug)]
560 pub enum Mode {
561     // The test passes a full execution of the rustc driver
562     Pass,
563     // The rustc driver panicked
564     Panic,
565     // The rustc driver emitted an error
566     Fail,
567 }
568
569 impl Mode {
570     fn ok(self, status: ExitStatus) -> Errors {
571         match (status.code().unwrap(), self) {
572             (1, Mode::Fail) | (101, Mode::Panic) | (0, Mode::Pass) => vec![],
573             _ => vec![Error::ExitStatus(self, status)],
574         }
575     }
576 }