]> git.lizzy.rs Git - rust.git/blob - ui_test/src/lib.rs
fmt
[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     // Always remove annotation comments from stderr.
239     let annotations = Regex::new(r"\s*//~.*").unwrap();
240     let stderr = std::str::from_utf8(&output.stderr).unwrap();
241     let stderr = annotations.replace_all(stderr, "");
242     let stdout = std::str::from_utf8(&output.stdout).unwrap();
243     // Check output files (if any)
244     let revised = |extension: &str| {
245         if revision.is_empty() {
246             extension.to_string()
247         } else {
248             format!("{}.{}", revision, extension)
249         }
250     };
251     // Check output files against actual output
252     check_output(
253         &stderr,
254         path,
255         &mut errors,
256         revised("stderr"),
257         target,
258         &config.stderr_filters,
259         &config,
260         comments,
261     );
262     check_output(
263         &stdout,
264         path,
265         &mut errors,
266         revised("stdout"),
267         target,
268         &config.stdout_filters,
269         &config,
270         comments,
271     );
272     // Check error annotations in the source against output
273     check_annotations(&stderr, &mut errors, config, revision, comments);
274     (miri, errors)
275 }
276
277 fn check_annotations(
278     unnormalized_stderr: &str,
279     errors: &mut Errors,
280     config: &Config,
281     revision: &str,
282     comments: &Comments,
283 ) {
284     let mut found_annotation = false;
285     if let Some((ref error_pattern, definition_line)) = comments.error_pattern {
286         if !unnormalized_stderr.contains(error_pattern) {
287             errors.push(Error::PatternNotFound {
288                 stderr: unnormalized_stderr.to_string(),
289                 pattern: error_pattern.to_string(),
290                 definition_line,
291             });
292         }
293         found_annotation = true;
294     }
295     for &ErrorMatch { ref matched, revision: ref rev, definition_line } in &comments.error_matches {
296         // FIXME: check that the error happens on the marked line
297
298         if let Some(rev) = rev {
299             if rev != revision {
300                 continue;
301             }
302         }
303
304         if !unnormalized_stderr.contains(matched) {
305             errors.push(Error::PatternNotFound {
306                 stderr: unnormalized_stderr.to_string(),
307                 pattern: matched.to_string(),
308                 definition_line,
309             });
310         }
311         found_annotation = true;
312     }
313     match (config.mode, found_annotation) {
314         (Mode::Pass, true) | (Mode::Panic, true) => errors.push(Error::PatternFoundInPassTest),
315         (Mode::Fail, false) => errors.push(Error::NoPatternsFound),
316         _ => {}
317     };
318 }
319
320 fn check_output(
321     output: &str,
322     path: &Path,
323     errors: &mut Errors,
324     kind: String,
325     target: &str,
326     filters: &Filter,
327     config: &Config,
328     comments: &Comments,
329 ) {
330     let output = normalize(path, output, filters, comments);
331     let path = output_path(path, comments, kind, target);
332     match config.output_conflict_handling {
333         OutputConflictHandling::Bless =>
334             if output.is_empty() {
335                 let _ = std::fs::remove_file(path);
336             } else {
337                 std::fs::write(path, &output).unwrap();
338             },
339         OutputConflictHandling::Error => {
340             let expected_output = std::fs::read_to_string(&path).unwrap_or_default();
341             if output != expected_output {
342                 errors.push(Error::OutputDiffers {
343                     path,
344                     actual: output,
345                     expected: expected_output,
346                 });
347             }
348         }
349         OutputConflictHandling::Ignore => {}
350     }
351 }
352
353 fn output_path(path: &Path, comments: &Comments, kind: String, target: &str) -> PathBuf {
354     if comments.stderr_per_bitwidth {
355         return path.with_extension(format!("{}.{kind}", get_pointer_width(target)));
356     }
357     path.with_extension(kind)
358 }
359
360 fn ignore_file(comments: &Comments, target: &str) -> bool {
361     for s in &comments.ignore {
362         if target.contains(s) {
363             return true;
364         }
365         if get_pointer_width(target) == s {
366             return true;
367         }
368     }
369     for s in &comments.only {
370         if !target.contains(s) {
371             return true;
372         }
373         if get_pointer_width(target) != s {
374             return true;
375         }
376     }
377     false
378 }
379
380 // Taken 1:1 from compiletest-rs
381 fn get_pointer_width(triple: &str) -> &'static str {
382     if (triple.contains("64") && !triple.ends_with("gnux32") && !triple.ends_with("gnu_ilp32"))
383         || triple.starts_with("s390x")
384     {
385         "64bit"
386     } else if triple.starts_with("avr") {
387         "16bit"
388     } else {
389         "32bit"
390     }
391 }
392
393 fn normalize(path: &Path, text: &str, filters: &Filter, comments: &Comments) -> String {
394     // Useless paths
395     let mut text = text.replace(&path.parent().unwrap().display().to_string(), "$DIR");
396     if let Some(lib_path) = option_env!("RUSTC_LIB_PATH") {
397         text = text.replace(lib_path, "RUSTLIB");
398     }
399
400     for (regex, replacement) in filters.iter() {
401         text = regex.replace_all(&text, *replacement).to_string();
402     }
403
404     for (from, to) in &comments.normalize_stderr {
405         text = from.replace_all(&text, to).to_string();
406     }
407     text
408 }
409
410 impl Config {
411     fn get_host(&self) -> String {
412         rustc_version::VersionMeta::for_command(std::process::Command::new(&self.program))
413             .expect("failed to parse rustc version info")
414             .host
415     }
416 }
417
418 #[derive(Copy, Clone, Debug)]
419 pub enum Mode {
420     // The test passes a full execution of the rustc driver
421     Pass,
422     // The rustc driver panicked
423     Panic,
424     // The rustc driver emitted an error
425     Fail,
426 }
427
428 impl Mode {
429     fn ok(self, status: ExitStatus) -> Errors {
430         match (status.code().unwrap(), self) {
431             (1, Mode::Fail) | (101, Mode::Panic) | (0, Mode::Pass) => vec![],
432             _ => vec![Error::ExitStatus(self, status)],
433         }
434     }
435 }