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