]> git.lizzy.rs Git - rust.git/blob - ui_test/src/lib.rs
Auto merge of #2166 - RalfJung:tests, r=oli-obk
[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
12 use crate::comments::Comments;
13
14 mod comments;
15 #[cfg(test)]
16 mod tests;
17
18 #[derive(Debug)]
19 pub struct Config {
20     /// Arguments passed to the binary that is executed.
21     pub args: Vec<String>,
22     /// `None` to run on the host, otherwise a target triple
23     pub target: Option<String>,
24     /// Filters applied to stderr output before processing it
25     pub stderr_filters: Filter,
26     /// Filters applied to stdout output before processing it
27     pub stdout_filters: Filter,
28     /// The folder in which to start searching for .rs files
29     pub root_dir: PathBuf,
30     pub mode: Mode,
31     pub program: PathBuf,
32     pub output_conflict_handling: OutputConflictHandling,
33     /// Only run tests with one of these strings in their path/name
34     pub path_filter: Vec<String>,
35 }
36
37 #[derive(Debug)]
38 pub enum OutputConflictHandling {
39     /// The default: emit a diff of the expected/actual output.
40     Error,
41     /// Ignore mismatches in the stderr/stdout files.
42     Ignore,
43     /// Instead of erroring if the stderr/stdout differs from the expected
44     /// automatically replace it with the found output (after applying filters).
45     Bless,
46 }
47
48 pub type Filter = Vec<(Regex, &'static str)>;
49
50 pub fn run_tests(config: Config) {
51     eprintln!("   Compiler flags: {:?}", config.args);
52
53     // Get the triple with which to run the tests
54     let target = config.target.clone().unwrap_or_else(|| config.get_host());
55
56     // A queue for files or folders to process
57     let todo = SegQueue::new();
58     todo.push(config.root_dir.clone());
59
60     // Some statistics and failure reports.
61     let failures = Mutex::new(vec![]);
62     let succeeded = AtomicUsize::default();
63     let ignored = AtomicUsize::default();
64     let filtered = AtomicUsize::default();
65
66     crossbeam::scope(|s| {
67         for _ in 0..std::thread::available_parallelism().unwrap().get() {
68             s.spawn(|_| {
69                 while let Some(path) = todo.pop() {
70                     // Collect everything inside directories
71                     if path.is_dir() {
72                         for entry in std::fs::read_dir(path).unwrap() {
73                             todo.push(entry.unwrap().path());
74                         }
75                         continue;
76                     }
77                     // Only look at .rs files
78                     if !path.extension().map(|ext| ext == "rs").unwrap_or(false) {
79                         continue;
80                     }
81                     if !config.path_filter.is_empty() {
82                         let path_display = path.display().to_string();
83                         if !config.path_filter.iter().any(|filter| path_display.contains(filter)) {
84                             filtered.fetch_add(1, Ordering::Relaxed);
85                             continue;
86                         }
87                     }
88                     let comments = Comments::parse_file(&path);
89                     // Ignore file if only/ignore rules do (not) apply
90                     if ignore_file(&comments, &target) {
91                         ignored.fetch_add(1, Ordering::Relaxed);
92                         eprintln!(
93                             "{} ... {}",
94                             path.display(),
95                             "ignored (in-test comment)".yellow()
96                         );
97                         continue;
98                     }
99                     // Run the test for all revisions
100                     for revision in
101                         comments.revisions.clone().unwrap_or_else(|| vec![String::new()])
102                     {
103                         let (m, errors) = run_test(&path, &config, &target, &revision, &comments);
104
105                         // Using a single `eprintln!` to prevent messages from threads from getting intermingled.
106                         let mut msg = format!("{} ", path.display());
107                         if !revision.is_empty() {
108                             write!(msg, "(revision `{revision}`) ").unwrap();
109                         }
110                         write!(msg, "... ").unwrap();
111                         if errors.is_empty() {
112                             eprintln!("{msg}{}", "ok".green());
113                             succeeded.fetch_add(1, Ordering::Relaxed);
114                         } else {
115                             eprintln!("{msg}{}", "FAILED".red().bold());
116                             failures.lock().unwrap().push((path.clone(), m, revision, errors));
117                         }
118                     }
119                 }
120             });
121         }
122     })
123     .unwrap();
124
125     // Print all errors in a single thread to show reliable output
126     let failures = failures.into_inner().unwrap();
127     let succeeded = succeeded.load(Ordering::Relaxed);
128     let ignored = ignored.load(Ordering::Relaxed);
129     let filtered = filtered.load(Ordering::Relaxed);
130     if !failures.is_empty() {
131         for (path, miri, revision, errors) in &failures {
132             eprintln!();
133             eprint!("{}", path.display().to_string().underline());
134             if !revision.is_empty() {
135                 eprint!(" (revision `{}`)", revision);
136             }
137             eprint!(" {}", "FAILED".red());
138             eprintln!();
139             eprintln!("command: {:?}", miri);
140             eprintln!();
141             let mut dump_stderr = None;
142             for error in errors {
143                 match error {
144                     Error::ExitStatus(mode, exit_status) => eprintln!("{mode:?} got {exit_status}"),
145                     Error::PatternNotFound { stderr, pattern, definition_line } => {
146                         eprintln!("`{pattern}` {} in stderr output", "not found".red());
147                         eprintln!(
148                             "expected because of pattern here: {}:{definition_line}",
149                             path.display().to_string().bold()
150                         );
151                         dump_stderr = Some(stderr.clone())
152                     }
153                     Error::NoPatternsFound =>
154                         eprintln!("{}", "no error patterns found in failure test".red()),
155                     Error::PatternFoundInPassTest =>
156                         eprintln!("{}", "error pattern found in success test".red()),
157                     Error::OutputDiffers { path, actual, expected } => {
158                         dump_stderr = None;
159                         eprintln!("actual output differed from expected {}", path.display());
160                         eprintln!("{}", pretty_assertions::StrComparison::new(expected, actual));
161                         eprintln!()
162                     }
163                 }
164                 eprintln!();
165             }
166             if let Some(stderr) = dump_stderr {
167                 eprintln!("actual stderr:");
168                 eprintln!("{}", stderr);
169                 eprintln!();
170             }
171         }
172         eprintln!(
173             "test result: {}. {} tests failed, {} tests passed, {} ignored, {} filtered out",
174             "FAIL".red(),
175             failures.len().to_string().red().bold(),
176             succeeded.to_string().green(),
177             ignored.to_string().yellow(),
178             filtered.to_string().yellow(),
179         );
180         std::process::exit(1);
181     }
182     eprintln!();
183     eprintln!(
184         "test result: {}. {} tests passed, {} ignored, {} filtered out",
185         "ok".green(),
186         succeeded.to_string().green(),
187         ignored.to_string().yellow(),
188         filtered.to_string().yellow(),
189     );
190     eprintln!();
191 }
192
193 #[derive(Debug)]
194 enum Error {
195     /// Got an invalid exit status for the given mode.
196     ExitStatus(Mode, ExitStatus),
197     PatternNotFound {
198         stderr: String,
199         pattern: String,
200         definition_line: usize,
201     },
202     /// A ui test checking for failure does not have any failure patterns
203     NoPatternsFound,
204     /// A ui test checking for success has failure patterns
205     PatternFoundInPassTest,
206     /// Stderr/Stdout differed from the `.stderr`/`.stdout` file present.
207     OutputDiffers {
208         path: PathBuf,
209         actual: String,
210         expected: String,
211     },
212 }
213
214 type Errors = Vec<Error>;
215
216 fn run_test(
217     path: &Path,
218     config: &Config,
219     target: &str,
220     revision: &str,
221     comments: &Comments,
222 ) -> (Command, Errors) {
223     // Run miri
224     let mut miri = Command::new(&config.program);
225     miri.args(config.args.iter());
226     miri.arg(path);
227     if !revision.is_empty() {
228         miri.arg(format!("--cfg={revision}"));
229     }
230     for arg in &comments.compile_flags {
231         miri.arg(arg);
232     }
233     for (k, v) in &comments.env_vars {
234         miri.env(k, v);
235     }
236     let output = miri.output().expect("could not execute miri");
237     let mut errors = config.mode.ok(output.status);
238     check_test_result(
239         path,
240         config,
241         target,
242         revision,
243         comments,
244         &mut errors,
245         &output.stdout,
246         &output.stderr,
247     );
248     (miri, errors)
249 }
250
251 fn check_test_result(
252     path: &Path,
253     config: &Config,
254     target: &str,
255     revision: &str,
256     comments: &Comments,
257     errors: &mut Errors,
258     stdout: &[u8],
259     stderr: &[u8],
260 ) {
261     // Always remove annotation comments from stderr.
262     let annotations = Regex::new(r"\s*//~.*").unwrap();
263     let stderr = std::str::from_utf8(stderr).unwrap();
264     let stderr = annotations.replace_all(stderr, "");
265     let stdout = std::str::from_utf8(stdout).unwrap();
266     // Check output files (if any)
267     let revised = |extension: &str| {
268         if revision.is_empty() {
269             extension.to_string()
270         } else {
271             format!("{}.{}", revision, extension)
272         }
273     };
274     // Check output files against actual output
275     check_output(
276         &stderr,
277         path,
278         errors,
279         revised("stderr"),
280         target,
281         &config.stderr_filters,
282         &config,
283         comments,
284     );
285     check_output(
286         &stdout,
287         path,
288         errors,
289         revised("stdout"),
290         target,
291         &config.stdout_filters,
292         &config,
293         comments,
294     );
295     // Check error annotations in the source against output
296     check_annotations(&stderr, errors, config, revision, comments);
297 }
298
299 fn check_annotations(
300     unnormalized_stderr: &str,
301     errors: &mut Errors,
302     config: &Config,
303     revision: &str,
304     comments: &Comments,
305 ) {
306     let mut found_annotation = false;
307     if let Some((ref error_pattern, definition_line)) = comments.error_pattern {
308         if !unnormalized_stderr.contains(error_pattern) {
309             errors.push(Error::PatternNotFound {
310                 stderr: unnormalized_stderr.to_string(),
311                 pattern: error_pattern.to_string(),
312                 definition_line,
313             });
314         }
315         found_annotation = true;
316     }
317     for &ErrorMatch { ref matched, revision: ref rev, definition_line } in &comments.error_matches {
318         // FIXME: check that the error happens on the marked line
319
320         if let Some(rev) = rev {
321             if rev != revision {
322                 continue;
323             }
324         }
325
326         if !unnormalized_stderr.contains(matched) {
327             errors.push(Error::PatternNotFound {
328                 stderr: unnormalized_stderr.to_string(),
329                 pattern: matched.to_string(),
330                 definition_line,
331             });
332         }
333         found_annotation = true;
334     }
335     match (config.mode, found_annotation) {
336         (Mode::Pass, true) | (Mode::Panic, true) => errors.push(Error::PatternFoundInPassTest),
337         (Mode::Fail, false) => errors.push(Error::NoPatternsFound),
338         _ => {}
339     };
340 }
341
342 fn check_output(
343     output: &str,
344     path: &Path,
345     errors: &mut Errors,
346     kind: String,
347     target: &str,
348     filters: &Filter,
349     config: &Config,
350     comments: &Comments,
351 ) {
352     let output = normalize(path, output, filters, comments);
353     let path = output_path(path, comments, kind, target);
354     match config.output_conflict_handling {
355         OutputConflictHandling::Bless =>
356             if output.is_empty() {
357                 let _ = std::fs::remove_file(path);
358             } else {
359                 std::fs::write(path, &output).unwrap();
360             },
361         OutputConflictHandling::Error => {
362             let expected_output = std::fs::read_to_string(&path).unwrap_or_default();
363             if output != expected_output {
364                 errors.push(Error::OutputDiffers {
365                     path,
366                     actual: output,
367                     expected: expected_output,
368                 });
369             }
370         }
371         OutputConflictHandling::Ignore => {}
372     }
373 }
374
375 fn output_path(path: &Path, comments: &Comments, kind: String, target: &str) -> PathBuf {
376     if comments.stderr_per_bitwidth {
377         return path.with_extension(format!("{}.{kind}", get_pointer_width(target)));
378     }
379     path.with_extension(kind)
380 }
381
382 fn ignore_file(comments: &Comments, target: &str) -> bool {
383     for s in &comments.ignore {
384         if target.contains(s) {
385             return true;
386         }
387         if get_pointer_width(target) == s {
388             return true;
389         }
390     }
391     for s in &comments.only {
392         if !target.contains(s) {
393             return true;
394         }
395         if get_pointer_width(target) != s {
396             return true;
397         }
398     }
399     false
400 }
401
402 // Taken 1:1 from compiletest-rs
403 fn get_pointer_width(triple: &str) -> &'static str {
404     if (triple.contains("64") && !triple.ends_with("gnux32") && !triple.ends_with("gnu_ilp32"))
405         || triple.starts_with("s390x")
406     {
407         "64bit"
408     } else if triple.starts_with("avr") {
409         "16bit"
410     } else {
411         "32bit"
412     }
413 }
414
415 fn normalize(path: &Path, text: &str, filters: &Filter, comments: &Comments) -> String {
416     // Useless paths
417     let mut text = text.replace(&path.parent().unwrap().display().to_string(), "$DIR");
418     if let Some(lib_path) = option_env!("RUSTC_LIB_PATH") {
419         text = text.replace(lib_path, "RUSTLIB");
420     }
421
422     for (regex, replacement) in filters.iter() {
423         text = regex.replace_all(&text, *replacement).to_string();
424     }
425
426     for (from, to) in &comments.normalize_stderr {
427         text = from.replace_all(&text, to).to_string();
428     }
429     text
430 }
431
432 impl Config {
433     fn get_host(&self) -> String {
434         rustc_version::VersionMeta::for_command(std::process::Command::new(&self.program))
435             .expect("failed to parse rustc version info")
436             .host
437     }
438 }
439
440 #[derive(Copy, Clone, Debug)]
441 pub enum Mode {
442     // The test passes a full execution of the rustc driver
443     Pass,
444     // The rustc driver panicked
445     Panic,
446     // The rustc driver emitted an error
447     Fail,
448 }
449
450 impl Mode {
451     fn ok(self, status: ExitStatus) -> Errors {
452         match (status.code().unwrap(), self) {
453             (1, Mode::Fail) | (101, Mode::Panic) | (0, Mode::Pass) => vec![],
454             _ => vec![Error::ExitStatus(self, status)],
455         }
456     }
457 }