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