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