]> git.lizzy.rs Git - rust.git/blob - ui_test/src/lib.rs
hotfix for incorrect only- logic
[rust.git] / ui_test / src / lib.rs
1 use std::fmt::Write;
2 use std::path::{Path, PathBuf};
3 use std::process::{Command, ExitStatus};
4 use std::sync::atomic::{AtomicUsize, Ordering};
5 use std::sync::Mutex;
6
7 use colored::*;
8 use comments::ErrorMatch;
9 use crossbeam::queue::SegQueue;
10 use regex::Regex;
11 use rustc_stderr::{Level, Message};
12
13 use crate::comments::Comments;
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 queue for files or folders to process
59     let todo = SegQueue::new();
60     todo.push(config.root_dir.clone());
61
62     // Some statistics and failure reports.
63     let failures = Mutex::new(vec![]);
64     let succeeded = AtomicUsize::default();
65     let ignored = AtomicUsize::default();
66     let filtered = AtomicUsize::default();
67
68     crossbeam::scope(|s| {
69         for _ in 0..std::thread::available_parallelism().unwrap().get() {
70             s.spawn(|_| {
71                 while let Some(path) = todo.pop() {
72                     // Collect everything inside directories
73                     if path.is_dir() {
74                         for entry in std::fs::read_dir(path).unwrap() {
75                             todo.push(entry.unwrap().path());
76                         }
77                         continue;
78                     }
79                     // Only look at .rs files
80                     if !path.extension().map(|ext| ext == "rs").unwrap_or(false) {
81                         continue;
82                     }
83                     if !config.path_filter.is_empty() {
84                         let path_display = path.display().to_string();
85                         if !config.path_filter.iter().any(|filter| path_display.contains(filter)) {
86                             filtered.fetch_add(1, Ordering::Relaxed);
87                             continue;
88                         }
89                     }
90                     let comments = Comments::parse_file(&path);
91                     // Ignore file if only/ignore rules do (not) apply
92                     if ignore_file(&comments, &target) {
93                         ignored.fetch_add(1, Ordering::Relaxed);
94                         eprintln!(
95                             "{} ... {}",
96                             path.display(),
97                             "ignored (in-test comment)".yellow()
98                         );
99                         continue;
100                     }
101                     // Run the test for all revisions
102                     for revision in
103                         comments.revisions.clone().unwrap_or_else(|| vec![String::new()])
104                     {
105                         let (m, errors, stderr) =
106                             run_test(&path, &config, &target, &revision, &comments);
107
108                         // Using a single `eprintln!` to prevent messages from threads from getting intermingled.
109                         let mut msg = format!("{} ", path.display());
110                         if !revision.is_empty() {
111                             write!(msg, "(revision `{revision}`) ").unwrap();
112                         }
113                         write!(msg, "... ").unwrap();
114                         if errors.is_empty() {
115                             eprintln!("{msg}{}", "ok".green());
116                             succeeded.fetch_add(1, Ordering::Relaxed);
117                         } else {
118                             eprintln!("{msg}{}", "FAILED".red().bold());
119                             failures.lock().unwrap().push((
120                                 path.clone(),
121                                 m,
122                                 revision,
123                                 errors,
124                                 stderr,
125                             ));
126                         }
127                     }
128                 }
129             });
130         }
131     })
132     .unwrap();
133
134     // Print all errors in a single thread to show reliable output
135     let failures = failures.into_inner().unwrap();
136     let succeeded = succeeded.load(Ordering::Relaxed);
137     let ignored = ignored.load(Ordering::Relaxed);
138     let filtered = filtered.load(Ordering::Relaxed);
139     if !failures.is_empty() {
140         for (path, miri, revision, errors, stderr) in &failures {
141             eprintln!();
142             eprint!("{}", path.display().to_string().underline());
143             if !revision.is_empty() {
144                 eprint!(" (revision `{}`)", revision);
145             }
146             eprint!(" {}", "FAILED".red());
147             eprintln!();
148             eprintln!("command: {:?}", miri);
149             eprintln!();
150             // `None` means never dump, as we already dumped it for an `OutputDiffers`
151             // `Some(false)` means there's no reason to dump, as all errors are independent of the stderr
152             // `Some(true)` means that there was a pattern in the .rs file that was not found in the output.
153             let mut dump_stderr = Some(false);
154             for error in errors {
155                 match error {
156                     Error::ExitStatus(mode, exit_status) => eprintln!("{mode:?} got {exit_status}"),
157                     Error::PatternNotFound { pattern, definition_line } => {
158                         eprintln!("`{pattern}` {} in stderr output", "not found".red());
159                         eprintln!(
160                             "expected because of pattern here: {}:{definition_line}",
161                             path.display().to_string().bold()
162                         );
163                         dump_stderr = dump_stderr.map(|_| true);
164                     }
165                     Error::NoPatternsFound => {
166                         eprintln!("{}", "no error patterns found in failure test".red());
167                     }
168                     Error::PatternFoundInPassTest =>
169                         eprintln!("{}", "error pattern found in success test".red()),
170                     Error::OutputDiffers { path, actual, expected } => {
171                         if path.extension().unwrap() == "stderr" {
172                             dump_stderr = None;
173                         }
174                         eprintln!("actual output differed from expected {}", path.display());
175                         eprintln!("{}", pretty_assertions::StrComparison::new(expected, actual));
176                         eprintln!()
177                     }
178                     Error::ErrorsWithoutPattern { path: None, msgs } => {
179                         eprintln!(
180                             "There were {} unmatched diagnostics that occurred outside the testfile and had not pattern",
181                             msgs.len(),
182                         );
183                         for Message { level, message } in msgs {
184                             eprintln!("    {level:?}: {message}")
185                         }
186                     }
187                     Error::ErrorsWithoutPattern { path: Some((path, line)), msgs } => {
188                         eprintln!(
189                             "There were {} unmatched diagnostics at {}:{line}",
190                             msgs.len(),
191                             path.display()
192                         );
193                         for Message { level, message } in msgs {
194                             eprintln!("    {level:?}: {message}")
195                         }
196                     }
197                     Error::ErrorPatternWithoutErrorAnnotation(path, line) => {
198                         eprintln!(
199                             "Annotation at {}:{line} matched an error diagnostic but did not have `ERROR` before its message",
200                             path.display()
201                         );
202                     }
203                 }
204                 eprintln!();
205             }
206             if let Some(true) = dump_stderr {
207                 eprintln!("actual stderr:");
208                 eprintln!("{}", stderr);
209                 eprintln!();
210             }
211         }
212         eprintln!("{}", "failures:".red().underline());
213         for (path, _miri, _revision, _errors, _stderr) in &failures {
214             eprintln!("    {}", path.display());
215         }
216         eprintln!();
217         eprintln!(
218             "test result: {}. {} tests failed, {} tests passed, {} ignored, {} filtered out",
219             "FAIL".red(),
220             failures.len().to_string().red().bold(),
221             succeeded.to_string().green(),
222             ignored.to_string().yellow(),
223             filtered.to_string().yellow(),
224         );
225         std::process::exit(1);
226     }
227     eprintln!();
228     eprintln!(
229         "test result: {}. {} tests passed, {} ignored, {} filtered out",
230         "ok".green(),
231         succeeded.to_string().green(),
232         ignored.to_string().yellow(),
233         filtered.to_string().yellow(),
234     );
235     eprintln!();
236 }
237
238 #[derive(Debug)]
239 enum Error {
240     /// Got an invalid exit status for the given mode.
241     ExitStatus(Mode, ExitStatus),
242     PatternNotFound {
243         pattern: String,
244         definition_line: usize,
245     },
246     /// A ui test checking for failure does not have any failure patterns
247     NoPatternsFound,
248     /// A ui test checking for success has failure patterns
249     PatternFoundInPassTest,
250     /// Stderr/Stdout differed from the `.stderr`/`.stdout` file present.
251     OutputDiffers {
252         path: PathBuf,
253         actual: String,
254         expected: String,
255     },
256     ErrorsWithoutPattern {
257         msgs: Vec<Message>,
258         path: Option<(PathBuf, usize)>,
259     },
260     ErrorPatternWithoutErrorAnnotation(PathBuf, usize),
261 }
262
263 type Errors = Vec<Error>;
264
265 fn run_test(
266     path: &Path,
267     config: &Config,
268     target: &str,
269     revision: &str,
270     comments: &Comments,
271 ) -> (Command, Errors, String) {
272     // Run miri
273     let mut miri = Command::new(&config.program);
274     miri.args(config.args.iter());
275     miri.arg(path);
276     if !revision.is_empty() {
277         miri.arg(format!("--cfg={revision}"));
278     }
279     miri.arg("--error-format=json");
280     for arg in &comments.compile_flags {
281         miri.arg(arg);
282     }
283     for (k, v) in &comments.env_vars {
284         miri.env(k, v);
285     }
286     let output = miri.output().expect("could not execute miri");
287     let mut errors = config.mode.ok(output.status);
288     let stderr = check_test_result(
289         path,
290         config,
291         target,
292         revision,
293         comments,
294         &mut errors,
295         &output.stdout,
296         &output.stderr,
297     );
298     (miri, errors, stderr)
299 }
300
301 fn check_test_result(
302     path: &Path,
303     config: &Config,
304     target: &str,
305     revision: &str,
306     comments: &Comments,
307     errors: &mut Errors,
308     stdout: &[u8],
309     stderr: &[u8],
310 ) -> String {
311     // Always remove annotation comments from stderr.
312     let diagnostics = rustc_stderr::process(path, stderr);
313     let stdout = std::str::from_utf8(stdout).unwrap();
314     // Check output files (if any)
315     let revised = |extension: &str| {
316         if revision.is_empty() {
317             extension.to_string()
318         } else {
319             format!("{}.{}", revision, extension)
320         }
321     };
322     // Check output files against actual output
323     check_output(
324         &diagnostics.rendered,
325         path,
326         errors,
327         revised("stderr"),
328         target,
329         &config.stderr_filters,
330         &config,
331         comments,
332     );
333     check_output(
334         &stdout,
335         path,
336         errors,
337         revised("stdout"),
338         target,
339         &config.stdout_filters,
340         &config,
341         comments,
342     );
343     // Check error annotations in the source against output
344     check_annotations(
345         diagnostics.messages,
346         diagnostics.messages_from_unknown_file_or_line,
347         path,
348         errors,
349         config,
350         revision,
351         comments,
352     );
353     diagnostics.rendered
354 }
355
356 fn check_annotations(
357     mut messages: Vec<Vec<Message>>,
358     mut messages_from_unknown_file_or_line: Vec<Message>,
359     path: &Path,
360     errors: &mut Errors,
361     config: &Config,
362     revision: &str,
363     comments: &Comments,
364 ) {
365     if let Some((ref error_pattern, definition_line)) = comments.error_pattern {
366         let mut found = false;
367
368         // first check the diagnostics messages outside of our file. We check this first, so that
369         // you can mix in-file annotations with // error-pattern annotations, even if there is overlap
370         // in the messages.
371         if let Some(i) = messages_from_unknown_file_or_line
372             .iter()
373             .position(|msg| msg.message.contains(error_pattern))
374         {
375             messages_from_unknown_file_or_line.remove(i);
376             found = true;
377         }
378
379         // if nothing was found, check the ones inside our file. We permit this because some tests may have
380         // flaky line numbers for their messages.
381         if !found {
382             for line in &mut messages {
383                 if let Some(i) = line.iter().position(|msg| msg.message.contains(error_pattern)) {
384                     line.remove(i);
385                     found = true;
386                     break;
387                 }
388             }
389         }
390
391         if !found {
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!("{}.{kind}", get_pointer_width(target)));
502     }
503     path.with_extension(kind)
504 }
505
506 fn ignore_file(comments: &Comments, target: &str) -> bool {
507     for s in &comments.ignore {
508         if target.contains(s) {
509             return true;
510         }
511         if get_pointer_width(target) == s {
512             return true;
513         }
514     }
515     for s in &comments.only {
516         if !target.contains(s) {
517             return true;
518         }
519         /* FIXME(https://github.com/rust-lang/miri/issues/2206)
520         if get_pointer_width(target) != s {
521             return true;
522         } */
523     }
524     false
525 }
526
527 // Taken 1:1 from compiletest-rs
528 fn get_pointer_width(triple: &str) -> &'static str {
529     if (triple.contains("64") && !triple.ends_with("gnux32") && !triple.ends_with("gnu_ilp32"))
530         || triple.starts_with("s390x")
531     {
532         "64bit"
533     } else if triple.starts_with("avr") {
534         "16bit"
535     } else {
536         "32bit"
537     }
538 }
539
540 fn normalize(path: &Path, text: &str, filters: &Filter, comments: &Comments) -> String {
541     // Useless paths
542     let mut text = text.replace(&path.parent().unwrap().display().to_string(), "$DIR");
543     if let Some(lib_path) = option_env!("RUSTC_LIB_PATH") {
544         text = text.replace(lib_path, "RUSTLIB");
545     }
546
547     for (regex, replacement) in filters.iter() {
548         text = regex.replace_all(&text, *replacement).to_string();
549     }
550
551     for (from, to) in &comments.normalize_stderr {
552         text = from.replace_all(&text, to).to_string();
553     }
554     text
555 }
556
557 impl Config {
558     fn get_host(&self) -> String {
559         rustc_version::VersionMeta::for_command(std::process::Command::new(&self.program))
560             .expect("failed to parse rustc version info")
561             .host
562     }
563 }
564
565 #[derive(Copy, Clone, Debug)]
566 pub enum Mode {
567     // The test passes a full execution of the rustc driver
568     Pass,
569     // The rustc driver panicked
570     Panic,
571     // The rustc driver emitted an error
572     Fail,
573 }
574
575 impl Mode {
576     fn ok(self, status: ExitStatus) -> Errors {
577         match (status.code().unwrap(), self) {
578             (1, Mode::Fail) | (101, Mode::Panic) | (0, Mode::Pass) => vec![],
579             _ => vec![Error::ExitStatus(self, status)],
580         }
581     }
582 }