2 clippy::enum_variant_names,
3 clippy::useless_format,
4 clippy::too_many_arguments,
8 use std::collections::VecDeque;
9 use std::ffi::OsString;
11 use std::path::{Path, PathBuf};
12 use std::process::{Command, ExitStatus};
13 use std::sync::atomic::{AtomicUsize, Ordering};
17 use color_eyre::eyre::Result;
19 use parser::{ErrorMatch, Pattern};
21 use rustc_stderr::{Level, Message};
23 use crate::dependencies::build_dependencies;
24 use crate::parser::{Comments, Condition};
34 /// Arguments passed to the binary that is executed.
35 pub args: Vec<OsString>,
36 /// `None` to run on the host, otherwise a target triple
37 pub target: Option<String>,
38 /// Filters applied to stderr output before processing it
39 pub stderr_filters: Filter,
40 /// Filters applied to stdout output before processing it
41 pub stdout_filters: Filter,
42 /// The folder in which to start searching for .rs files
43 pub root_dir: PathBuf,
46 pub output_conflict_handling: OutputConflictHandling,
47 /// Only run tests with one of these strings in their path/name
48 pub path_filter: Vec<String>,
49 /// Path to a `Cargo.toml` that describes which dependencies the tests can access.
50 pub dependencies_crate_manifest_path: Option<PathBuf>,
51 /// Can be used to override what command to run instead of `cargo` to build the
52 /// dependencies in `manifest_path`
53 pub dependency_builder: Option<DependencyBuilder>,
54 /// Print one character per test instead of one line
59 pub struct DependencyBuilder {
61 pub args: Vec<String>,
62 pub envs: Vec<(String, OsString)>,
66 pub enum OutputConflictHandling {
67 /// The default: emit a diff of the expected/actual output.
69 /// Ignore mismatches in the stderr/stdout files.
71 /// Instead of erroring if the stderr/stdout differs from the expected
72 /// automatically replace it with the found output (after applying filters).
76 pub type Filter = Vec<(Regex, &'static str)>;
78 pub fn run_tests(mut config: Config) -> Result<()> {
79 eprintln!(" Compiler flags: {:?}", config.args);
81 // Get the triple with which to run the tests
82 let target = config.target.clone().unwrap_or_else(|| config.get_host());
84 eprintln!(" Building test dependencies...");
85 let dependencies = build_dependencies(&config)?;
86 for (name, dependency) in dependencies.dependencies {
87 config.args.push("--extern".into());
88 let mut dep = OsString::from(name);
91 config.args.push(dep);
93 for import_path in dependencies.import_paths {
94 config.args.push("-L".into());
95 config.args.push(import_path.into());
99 // A channel for files to process
100 let (submit, receive) = crossbeam::channel::unbounded();
102 // Some statistics and failure reports.
103 let failures = Mutex::new(vec![]);
104 let succeeded = AtomicUsize::default();
105 let ignored = AtomicUsize::default();
106 let filtered = AtomicUsize::default();
108 crossbeam::scope(|s| -> Result<()> {
109 // Create a thread that is in charge of walking the directory and submitting jobs.
110 // It closes the channel when it is done.
112 let mut todo = VecDeque::new();
113 todo.push_back(config.root_dir.clone());
114 while let Some(path) = todo.pop_front() {
116 // Enqueue everything inside this directory.
117 // We want it sorted, to have some control over scheduling of slow tests.
119 std::fs::read_dir(path).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
120 entries.sort_by_key(|e| e.file_name());
121 for entry in entries {
122 todo.push_back(entry.path());
124 } else if path.extension().map(|ext| ext == "rs").unwrap_or(false) {
125 // Forward .rs files to the test workers.
126 submit.send(path).unwrap();
129 // There will be no more jobs. This signals the workers to quit.
130 // (This also ensures `submit` is moved into this closure.)
134 // A channel for the messages emitted by the individual test threads.
135 let (finished_files_sender, finished_files_recv) = crossbeam::channel::unbounded();
144 for (i, (_, result)) in finished_files_recv.into_iter().enumerate() {
145 // Humans start counting at 1
148 TestResult::Ok => eprint!("{}", ".".green()),
149 TestResult::Failed => eprint!("{}", "F".red().bold()),
150 TestResult::Ignored => eprint!("{}", "i".yellow()),
157 for (msg, result) in finished_files_recv {
158 eprint!("{msg} ... ");
162 TestResult::Ok => "ok".green(),
163 TestResult::Failed => "FAILED".red().bold(),
164 TestResult::Ignored => "ignored (in-test comment)".yellow(),
171 let mut threads = vec![];
173 // Create N worker threads that receive files to test.
174 for _ in 0..std::thread::available_parallelism().unwrap().get() {
175 let finished_files_sender = finished_files_sender.clone();
176 threads.push(s.spawn(|_| -> Result<()> {
177 let finished_files_sender = finished_files_sender;
178 for path in &receive {
179 if !config.path_filter.is_empty() {
180 let path_display = path.display().to_string();
181 if !config.path_filter.iter().any(|filter| path_display.contains(filter)) {
182 filtered.fetch_add(1, Ordering::Relaxed);
186 let comments = Comments::parse_file(&path)?;
187 // Ignore file if only/ignore rules do (not) apply
188 if !test_file_conditions(&comments, &target, &config) {
189 ignored.fetch_add(1, Ordering::Relaxed);
190 finished_files_sender
191 .send((path.display().to_string(), TestResult::Ignored))?;
194 // Run the test for all revisions
196 comments.revisions.clone().unwrap_or_else(|| vec![String::new()])
198 let (m, errors, stderr) =
199 run_test(&path, &config, &target, &revision, &comments);
201 // Using a single `eprintln!` to prevent messages from threads from getting intermingled.
202 let mut msg = format!("{}", path.display());
203 if !revision.is_empty() {
204 write!(msg, " (revision `{revision}`) ").unwrap();
206 if errors.is_empty() {
207 finished_files_sender.send((msg, TestResult::Ok))?;
208 succeeded.fetch_add(1, Ordering::Relaxed);
210 finished_files_sender.send((msg, TestResult::Failed))?;
211 failures.lock().unwrap().push((
225 for thread in threads {
226 thread.join().unwrap()?;
232 // Print all errors in a single thread to show reliable output
233 let failures = failures.into_inner().unwrap();
234 let succeeded = succeeded.load(Ordering::Relaxed);
235 let ignored = ignored.load(Ordering::Relaxed);
236 let filtered = filtered.load(Ordering::Relaxed);
237 if !failures.is_empty() {
238 for (path, miri, revision, errors, stderr) in &failures {
240 eprint!("{}", path.display().to_string().underline().bold());
241 if !revision.is_empty() {
242 eprint!(" (revision `{}`)", revision);
244 eprint!(" {}", "FAILED:".red().bold());
246 eprintln!("command: {:?}", miri);
248 for error in errors {
250 Error::ExitStatus(mode, exit_status) => eprintln!("{mode:?} got {exit_status}"),
251 Error::PatternNotFound { pattern, definition_line } => {
253 Pattern::SubString(s) =>
254 eprintln!("substring `{s}` {} in stderr output", "not found".red()),
256 eprintln!("`/{r}/` does {} stderr output", "not match".red()),
259 "expected because of pattern here: {}:{definition_line}",
260 path.display().to_string().bold()
263 Error::NoPatternsFound => {
264 eprintln!("{}", "no error patterns found in failure test".red());
266 Error::PatternFoundInPassTest =>
267 eprintln!("{}", "error pattern found in success test".red()),
268 Error::OutputDiffers { path, actual, expected } => {
269 eprintln!("actual output differed from expected {}", path.display());
270 eprintln!("{}", pretty_assertions::StrComparison::new(expected, actual));
273 Error::ErrorsWithoutPattern { path: None, msgs } => {
275 "There were {} unmatched diagnostics that occurred outside the testfile and had not pattern",
278 for Message { level, message } in msgs {
279 eprintln!(" {level:?}: {message}")
282 Error::ErrorsWithoutPattern { path: Some((path, line)), msgs } => {
284 "There were {} unmatched diagnostics at {}:{line}",
288 for Message { level, message } in msgs {
289 eprintln!(" {level:?}: {message}")
295 eprintln!("full stderr:");
296 eprintln!("{}", stderr);
299 eprintln!("{}", "FAILURES:".red().underline().bold());
300 for (path, _miri, _revision, _errors, _stderr) in &failures {
301 eprintln!(" {}", path.display());
305 "test result: {}. {} tests failed, {} tests passed, {} ignored, {} filtered out",
307 failures.len().to_string().red().bold(),
308 succeeded.to_string().green(),
309 ignored.to_string().yellow(),
310 filtered.to_string().yellow(),
312 std::process::exit(1);
316 "test result: {}. {} tests passed, {} ignored, {} filtered out",
318 succeeded.to_string().green(),
319 ignored.to_string().yellow(),
320 filtered.to_string().yellow(),
328 /// Got an invalid exit status for the given mode.
329 ExitStatus(Mode, ExitStatus),
332 definition_line: usize,
334 /// A ui test checking for failure does not have any failure patterns
336 /// A ui test checking for success has failure patterns
337 PatternFoundInPassTest,
338 /// Stderr/Stdout differed from the `.stderr`/`.stdout` file present.
344 ErrorsWithoutPattern {
346 path: Option<(PathBuf, usize)>,
350 type Errors = Vec<Error>;
358 ) -> (Command, Errors, String) {
360 let mut miri = Command::new(&config.program);
361 miri.args(config.args.iter());
363 if !revision.is_empty() {
364 miri.arg(format!("--cfg={revision}"));
366 miri.arg("--error-format=json");
367 for arg in &comments.compile_flags {
370 miri.envs(comments.env_vars.iter().map(|(k, v)| (k, v)));
371 let output = miri.output().expect("could not execute miri");
372 let mut errors = config.mode.ok(output.status);
373 let stderr = check_test_result(
383 (miri, errors, stderr)
386 fn check_test_result(
396 // Always remove annotation comments from stderr.
397 let diagnostics = rustc_stderr::process(path, stderr);
398 let stdout = std::str::from_utf8(stdout).unwrap();
399 // Check output files (if any)
400 let revised = |extension: &str| {
401 if revision.is_empty() {
402 extension.to_string()
404 format!("{}.{}", revision, extension)
407 // Check output files against actual output
409 &diagnostics.rendered,
414 &config.stderr_filters,
424 &config.stdout_filters,
428 // Check error annotations in the source against output
430 diagnostics.messages,
431 diagnostics.messages_from_unknown_file_or_line,
441 fn check_annotations(
442 mut messages: Vec<Vec<Message>>,
443 mut messages_from_unknown_file_or_line: Vec<Message>,
450 if let Some((ref error_pattern, definition_line)) = comments.error_pattern {
451 // first check the diagnostics messages outside of our file. We check this first, so that
452 // you can mix in-file annotations with //@error-pattern annotations, even if there is overlap
454 if let Some(i) = messages_from_unknown_file_or_line
456 .position(|msg| error_pattern.matches(&msg.message))
458 messages_from_unknown_file_or_line.remove(i);
460 errors.push(Error::PatternNotFound { pattern: error_pattern.clone(), definition_line });
464 // The order on `Level` is such that `Error` is the highest level.
465 // We will ensure that *all* diagnostics of level at least `lowest_annotation_level`
467 let mut lowest_annotation_level = Level::Error;
468 for &ErrorMatch { ref pattern, revision: ref rev, definition_line, line, level } in
469 &comments.error_matches
471 if let Some(rev) = rev {
477 // If we found a diagnostic with a level annotation, make sure that all
478 // diagnostics of that level have annotations, even if we don't end up finding a matching diagnostic
480 lowest_annotation_level = std::cmp::min(lowest_annotation_level, level);
482 if let Some(msgs) = messages.get_mut(line) {
484 msgs.iter().position(|msg| pattern.matches(&msg.message) && msg.level == level);
485 if let Some(found) = found {
491 errors.push(Error::PatternNotFound { pattern: pattern.clone(), definition_line });
494 let filter = |msgs: Vec<Message>| -> Vec<_> {
498 >= comments.require_annotations_for_level.unwrap_or(lowest_annotation_level)
503 let messages_from_unknown_file_or_line = filter(messages_from_unknown_file_or_line);
504 if !messages_from_unknown_file_or_line.is_empty() {
505 errors.push(Error::ErrorsWithoutPattern {
507 msgs: messages_from_unknown_file_or_line,
511 for (line, msgs) in messages.into_iter().enumerate() {
512 let msgs = filter(msgs);
513 if !msgs.is_empty() {
515 .push(Error::ErrorsWithoutPattern { path: Some((path.to_path_buf(), line)), msgs });
519 match (config.mode, comments.error_pattern.is_some() || !comments.error_matches.is_empty()) {
520 (Mode::Pass, true) | (Mode::Panic, true) => errors.push(Error::PatternFoundInPassTest),
521 (Mode::Fail, false) => errors.push(Error::NoPatternsFound),
536 let output = normalize(path, output, filters, comments);
537 let path = output_path(path, comments, kind, target);
538 match config.output_conflict_handling {
539 OutputConflictHandling::Bless =>
540 if output.is_empty() {
541 let _ = std::fs::remove_file(path);
543 std::fs::write(path, &output).unwrap();
545 OutputConflictHandling::Error => {
546 let expected_output = std::fs::read_to_string(&path).unwrap_or_default();
547 if output != expected_output {
548 errors.push(Error::OutputDiffers {
551 expected: expected_output,
555 OutputConflictHandling::Ignore => {}
559 fn output_path(path: &Path, comments: &Comments, kind: String, target: &str) -> PathBuf {
560 if comments.stderr_per_bitwidth {
561 return path.with_extension(format!("{}bit.{kind}", get_pointer_width(target)));
563 path.with_extension(kind)
566 fn test_condition(condition: &Condition, target: &str, config: &Config) -> bool {
568 Condition::Bitwidth(bits) => get_pointer_width(target) == *bits,
569 Condition::Target(t) => target.contains(t),
570 Condition::OnHost => config.target.is_none(),
574 /// Returns whether according to the in-file conditions, this file should be run.
575 fn test_file_conditions(comments: &Comments, target: &str, config: &Config) -> bool {
576 if comments.ignore.iter().any(|c| test_condition(c, target, config)) {
579 comments.only.iter().all(|c| test_condition(c, target, config))
582 // Taken 1:1 from compiletest-rs
583 fn get_pointer_width(triple: &str) -> u8 {
584 if (triple.contains("64") && !triple.ends_with("gnux32") && !triple.ends_with("gnu_ilp32"))
585 || triple.starts_with("s390x")
588 } else if triple.starts_with("avr") {
595 fn normalize(path: &Path, text: &str, filters: &Filter, comments: &Comments) -> String {
597 let mut text = text.replace(&path.parent().unwrap().display().to_string(), "$DIR");
598 if let Some(lib_path) = option_env!("RUSTC_LIB_PATH") {
599 text = text.replace(lib_path, "RUSTLIB");
602 for (regex, replacement) in filters.iter() {
603 text = regex.replace_all(&text, *replacement).to_string();
606 for (from, to) in &comments.normalize_stderr {
607 text = from.replace_all(&text, to).to_string();
613 fn get_host(&self) -> String {
614 rustc_version::VersionMeta::for_command(std::process::Command::new(&self.program))
615 .expect("failed to parse rustc version info")
620 #[derive(Copy, Clone, Debug)]
622 // The test passes a full execution of the rustc driver
624 // The rustc driver panicked
626 // The rustc driver emitted an error
631 fn ok(self, status: ExitStatus) -> Errors {
632 match (status.code().unwrap(), self) {
633 (1, Mode::Fail) | (101, Mode::Panic) | (0, Mode::Pass) => vec![],
634 _ => vec![Error::ExitStatus(self, status)],