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