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