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