]> git.lizzy.rs Git - rust.git/blob - ui_test/src/lib.rs
Auto merge of #2376 - RalfJung:rustup, 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 pub use color_eyre;
11 use color_eyre::eyre::Result;
12 use colored::*;
13 use parser::{ErrorMatch, Pattern};
14 use regex::Regex;
15 use rustc_stderr::{Level, Message};
16
17 use crate::parser::{Comments, Condition};
18
19 mod parser;
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| -> Result<()> {
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         let mut threads = vec![];
98
99         // Create N worker threads that receive files to test.
100         for _ in 0..std::thread::available_parallelism().unwrap().get() {
101             threads.push(s.spawn(|_| -> Result<()> {
102                 for path in &receive {
103                     if !config.path_filter.is_empty() {
104                         let path_display = path.display().to_string();
105                         if !config.path_filter.iter().any(|filter| path_display.contains(filter)) {
106                             filtered.fetch_add(1, Ordering::Relaxed);
107                             continue;
108                         }
109                     }
110                     let comments = Comments::parse_file(&path)?;
111                     // Ignore file if only/ignore rules do (not) apply
112                     if !test_file_conditions(&comments, &target, &config) {
113                         ignored.fetch_add(1, Ordering::Relaxed);
114                         eprintln!(
115                             "{} ... {}",
116                             path.display(),
117                             "ignored (in-test comment)".yellow()
118                         );
119                         continue;
120                     }
121                     // Run the test for all revisions
122                     for revision in
123                         comments.revisions.clone().unwrap_or_else(|| vec![String::new()])
124                     {
125                         let (m, errors, stderr) =
126                             run_test(&path, &config, &target, &revision, &comments);
127
128                         // Using a single `eprintln!` to prevent messages from threads from getting intermingled.
129                         let mut msg = format!("{} ", path.display());
130                         if !revision.is_empty() {
131                             write!(msg, "(revision `{revision}`) ").unwrap();
132                         }
133                         write!(msg, "... ").unwrap();
134                         if errors.is_empty() {
135                             eprintln!("{msg}{}", "ok".green());
136                             succeeded.fetch_add(1, Ordering::Relaxed);
137                         } else {
138                             eprintln!("{msg}{}", "FAILED".red().bold());
139                             failures.lock().unwrap().push((
140                                 path.clone(),
141                                 m,
142                                 revision,
143                                 errors,
144                                 stderr,
145                             ));
146                         }
147                     }
148                 }
149                 Ok(())
150             }));
151         }
152         for thread in threads {
153             thread.join().unwrap()?;
154         }
155         Ok(())
156     })
157     .unwrap()?;
158
159     // Print all errors in a single thread to show reliable output
160     let failures = failures.into_inner().unwrap();
161     let succeeded = succeeded.load(Ordering::Relaxed);
162     let ignored = ignored.load(Ordering::Relaxed);
163     let filtered = filtered.load(Ordering::Relaxed);
164     if !failures.is_empty() {
165         for (path, miri, revision, errors, stderr) in &failures {
166             eprintln!();
167             eprint!("{}", path.display().to_string().underline());
168             if !revision.is_empty() {
169                 eprint!(" (revision `{}`)", revision);
170             }
171             eprint!(" {}", "FAILED".red());
172             eprintln!();
173             eprintln!("command: {:?}", miri);
174             eprintln!();
175             let mut dump_stderr = true;
176             for error in errors {
177                 match error {
178                     Error::ExitStatus(mode, exit_status) => eprintln!("{mode:?} got {exit_status}"),
179                     Error::PatternNotFound { pattern, definition_line } => {
180                         match pattern {
181                             Pattern::SubString(s) =>
182                                 eprintln!("substring `{s}` {} in stderr output", "not found".red()),
183                             Pattern::Regex(r) =>
184                                 eprintln!("`/{r}/` does {} stderr output", "not match".red()),
185                         }
186                         eprintln!(
187                             "expected because of pattern here: {}:{definition_line}",
188                             path.display().to_string().bold()
189                         );
190                     }
191                     Error::NoPatternsFound => {
192                         eprintln!("{}", "no error patterns found in failure test".red());
193                     }
194                     Error::PatternFoundInPassTest =>
195                         eprintln!("{}", "error pattern found in success test".red()),
196                     Error::OutputDiffers { path, actual, expected } => {
197                         if path.extension().unwrap() == "stderr" {
198                             dump_stderr = false;
199                         }
200                         eprintln!("actual output differed from expected {}", path.display());
201                         eprintln!("{}", pretty_assertions::StrComparison::new(expected, actual));
202                         eprintln!()
203                     }
204                     Error::ErrorsWithoutPattern { path: None, msgs } => {
205                         eprintln!(
206                             "There were {} unmatched diagnostics that occurred outside the testfile and had not pattern",
207                             msgs.len(),
208                         );
209                         for Message { level, message } in msgs {
210                             eprintln!("    {level:?}: {message}")
211                         }
212                     }
213                     Error::ErrorsWithoutPattern { path: Some((path, line)), msgs } => {
214                         eprintln!(
215                             "There were {} unmatched diagnostics at {}:{line}",
216                             msgs.len(),
217                             path.display()
218                         );
219                         for Message { level, message } in msgs {
220                             eprintln!("    {level:?}: {message}")
221                         }
222                     }
223                 }
224                 eprintln!();
225             }
226             // Unless we already dumped the stderr via an OutputDiffers diff, let's dump it here.
227             if dump_stderr {
228                 eprintln!("actual stderr:");
229                 eprintln!("{}", stderr);
230                 eprintln!();
231             }
232         }
233         eprintln!("{}", "failures:".red().underline());
234         for (path, _miri, _revision, _errors, _stderr) in &failures {
235             eprintln!("    {}", path.display());
236         }
237         eprintln!();
238         eprintln!(
239             "test result: {}. {} tests failed, {} tests passed, {} ignored, {} filtered out",
240             "FAIL".red(),
241             failures.len().to_string().red().bold(),
242             succeeded.to_string().green(),
243             ignored.to_string().yellow(),
244             filtered.to_string().yellow(),
245         );
246         std::process::exit(1);
247     }
248     eprintln!();
249     eprintln!(
250         "test result: {}. {} tests passed, {} ignored, {} filtered out",
251         "ok".green(),
252         succeeded.to_string().green(),
253         ignored.to_string().yellow(),
254         filtered.to_string().yellow(),
255     );
256     eprintln!();
257     Ok(())
258 }
259
260 #[derive(Debug)]
261 enum Error {
262     /// Got an invalid exit status for the given mode.
263     ExitStatus(Mode, ExitStatus),
264     PatternNotFound {
265         pattern: Pattern,
266         definition_line: usize,
267     },
268     /// A ui test checking for failure does not have any failure patterns
269     NoPatternsFound,
270     /// A ui test checking for success has failure patterns
271     PatternFoundInPassTest,
272     /// Stderr/Stdout differed from the `.stderr`/`.stdout` file present.
273     OutputDiffers {
274         path: PathBuf,
275         actual: String,
276         expected: String,
277     },
278     ErrorsWithoutPattern {
279         msgs: Vec<Message>,
280         path: Option<(PathBuf, usize)>,
281     },
282 }
283
284 type Errors = Vec<Error>;
285
286 fn run_test(
287     path: &Path,
288     config: &Config,
289     target: &str,
290     revision: &str,
291     comments: &Comments,
292 ) -> (Command, Errors, String) {
293     // Run miri
294     let mut miri = Command::new(&config.program);
295     miri.args(config.args.iter());
296     miri.arg(path);
297     if !revision.is_empty() {
298         miri.arg(format!("--cfg={revision}"));
299     }
300     miri.arg("--error-format=json");
301     for arg in &comments.compile_flags {
302         miri.arg(arg);
303     }
304     for (k, v) in &comments.env_vars {
305         miri.env(k, v);
306     }
307     let output = miri.output().expect("could not execute miri");
308     let mut errors = config.mode.ok(output.status);
309     let stderr = check_test_result(
310         path,
311         config,
312         target,
313         revision,
314         comments,
315         &mut errors,
316         &output.stdout,
317         &output.stderr,
318     );
319     (miri, errors, stderr)
320 }
321
322 fn check_test_result(
323     path: &Path,
324     config: &Config,
325     target: &str,
326     revision: &str,
327     comments: &Comments,
328     errors: &mut Errors,
329     stdout: &[u8],
330     stderr: &[u8],
331 ) -> String {
332     // Always remove annotation comments from stderr.
333     let diagnostics = rustc_stderr::process(path, stderr);
334     let stdout = std::str::from_utf8(stdout).unwrap();
335     // Check output files (if any)
336     let revised = |extension: &str| {
337         if revision.is_empty() {
338             extension.to_string()
339         } else {
340             format!("{}.{}", revision, extension)
341         }
342     };
343     // Check output files against actual output
344     check_output(
345         &diagnostics.rendered,
346         path,
347         errors,
348         revised("stderr"),
349         target,
350         &config.stderr_filters,
351         config,
352         comments,
353     );
354     check_output(
355         stdout,
356         path,
357         errors,
358         revised("stdout"),
359         target,
360         &config.stdout_filters,
361         config,
362         comments,
363     );
364     // Check error annotations in the source against output
365     check_annotations(
366         diagnostics.messages,
367         diagnostics.messages_from_unknown_file_or_line,
368         path,
369         errors,
370         config,
371         revision,
372         comments,
373     );
374     diagnostics.rendered
375 }
376
377 fn check_annotations(
378     mut messages: Vec<Vec<Message>>,
379     mut messages_from_unknown_file_or_line: Vec<Message>,
380     path: &Path,
381     errors: &mut Errors,
382     config: &Config,
383     revision: &str,
384     comments: &Comments,
385 ) {
386     if let Some((ref error_pattern, definition_line)) = comments.error_pattern {
387         // first check the diagnostics messages outside of our file. We check this first, so that
388         // you can mix in-file annotations with //@error-pattern annotations, even if there is overlap
389         // in the messages.
390         if let Some(i) = messages_from_unknown_file_or_line
391             .iter()
392             .position(|msg| error_pattern.matches(&msg.message))
393         {
394             messages_from_unknown_file_or_line.remove(i);
395         } else {
396             errors.push(Error::PatternNotFound { pattern: error_pattern.clone(), definition_line });
397         }
398     }
399
400     // The order on `Level` is such that `Error` is the highest level.
401     // We will ensure that *all* diagnostics of level at least `lowest_annotation_level`
402     // are matched.
403     let mut lowest_annotation_level = Level::Error;
404     for &ErrorMatch { ref pattern, revision: ref rev, definition_line, line, level } in
405         &comments.error_matches
406     {
407         if let Some(rev) = rev {
408             if rev != revision {
409                 continue;
410             }
411         }
412
413         // If we found a diagnostic with a level annotation, make sure that all
414         // diagnostics of that level have annotations, even if we don't end up finding a matching diagnostic
415         // for this pattern.
416         lowest_annotation_level = std::cmp::min(lowest_annotation_level, level);
417
418         if let Some(msgs) = messages.get_mut(line) {
419             let found =
420                 msgs.iter().position(|msg| pattern.matches(&msg.message) && msg.level == level);
421             if let Some(found) = found {
422                 msgs.remove(found);
423                 continue;
424             }
425         }
426
427         errors.push(Error::PatternNotFound { pattern: pattern.clone(), definition_line });
428     }
429
430     let filter = |msgs: Vec<Message>| -> Vec<_> {
431         msgs.into_iter()
432             .filter(|msg| {
433                 msg.level
434                     >= comments.require_annotations_for_level.unwrap_or(lowest_annotation_level)
435             })
436             .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, config: &Config) -> bool {
503     match condition {
504         Condition::Bitwidth(bits) => get_pointer_width(target) == *bits,
505         Condition::Target(t) => target.contains(t),
506         Condition::OnHost => config.target.is_none(),
507     }
508 }
509
510 /// Returns whether according to the in-file conditions, this file should be run.
511 fn test_file_conditions(comments: &Comments, target: &str, config: &Config) -> bool {
512     if comments.ignore.iter().any(|c| test_condition(c, target, config)) {
513         return false;
514     }
515     comments.only.iter().all(|c| test_condition(c, target, config))
516 }
517
518 // Taken 1:1 from compiletest-rs
519 fn get_pointer_width(triple: &str) -> u8 {
520     if (triple.contains("64") && !triple.ends_with("gnux32") && !triple.ends_with("gnu_ilp32"))
521         || triple.starts_with("s390x")
522     {
523         64
524     } else if triple.starts_with("avr") {
525         16
526     } else {
527         32
528     }
529 }
530
531 fn normalize(path: &Path, text: &str, filters: &Filter, comments: &Comments) -> String {
532     // Useless paths
533     let mut text = text.replace(&path.parent().unwrap().display().to_string(), "$DIR");
534     if let Some(lib_path) = option_env!("RUSTC_LIB_PATH") {
535         text = text.replace(lib_path, "RUSTLIB");
536     }
537
538     for (regex, replacement) in filters.iter() {
539         text = regex.replace_all(&text, *replacement).to_string();
540     }
541
542     for (from, to) in &comments.normalize_stderr {
543         text = from.replace_all(&text, to).to_string();
544     }
545     text
546 }
547
548 impl Config {
549     fn get_host(&self) -> String {
550         rustc_version::VersionMeta::for_command(std::process::Command::new(&self.program))
551             .expect("failed to parse rustc version info")
552             .host
553     }
554 }
555
556 #[derive(Copy, Clone, Debug)]
557 pub enum Mode {
558     // The test passes a full execution of the rustc driver
559     Pass,
560     // The rustc driver panicked
561     Panic,
562     // The rustc driver emitted an error
563     Fail,
564 }
565
566 impl Mode {
567     fn ok(self, status: ExitStatus) -> Errors {
568         match (status.code().unwrap(), self) {
569             (1, Mode::Fail) | (101, Mode::Panic) | (0, Mode::Pass) => vec![],
570             _ => vec![Error::ExitStatus(self, status)],
571         }
572     }
573 }