]> git.lizzy.rs Git - rust.git/blobdiff - ui_test/src/lib.rs
tweak failure output a little
[rust.git] / ui_test / src / lib.rs
index 4a9cdd386ac27a68e8754a7d209c8689225f80d0..917e382379abe34443d2dba3ed6713eff12020c4 100644 (file)
@@ -1,18 +1,22 @@
+#![allow(clippy::enum_variant_names, clippy::useless_format, clippy::too_many_arguments)]
+
+use std::collections::VecDeque;
 use std::fmt::Write;
 use std::path::{Path, PathBuf};
 use std::process::{Command, ExitStatus};
 use std::sync::atomic::{AtomicUsize, Ordering};
 use std::sync::Mutex;
 
+pub use color_eyre;
+use color_eyre::eyre::Result;
 use colored::*;
-use comments::ErrorMatch;
-use crossbeam::queue::SegQueue;
+use parser::{ErrorMatch, Pattern};
 use regex::Regex;
 use rustc_stderr::{Level, Message};
 
-use crate::comments::Comments;
+use crate::parser::{Comments, Condition};
 
-mod comments;
+mod parser;
 mod rustc_stderr;
 #[cfg(test)]
 mod tests;
@@ -49,15 +53,14 @@ pub enum OutputConflictHandling {
 
 pub type Filter = Vec<(Regex, &'static str)>;
 
-pub fn run_tests(config: Config) {
+pub fn run_tests(config: Config) -> Result<()> {
     eprintln!("   Compiler flags: {:?}", config.args);
 
     // Get the triple with which to run the tests
     let target = config.target.clone().unwrap_or_else(|| config.get_host());
 
-    // A queue for files or folders to process
-    let todo = SegQueue::new();
-    todo.push(config.root_dir.clone());
+    // A channel for files to process
+    let (submit, receive) = crossbeam::channel::unbounded();
 
     // Some statistics and failure reports.
     let failures = Mutex::new(vec![]);
@@ -65,21 +68,38 @@ pub fn run_tests(config: Config) {
     let ignored = AtomicUsize::default();
     let filtered = AtomicUsize::default();
 
-    crossbeam::scope(|s| {
-        for _ in 0..std::thread::available_parallelism().unwrap().get() {
-            s.spawn(|_| {
-                while let Some(path) = todo.pop() {
-                    // Collect everything inside directories
-                    if path.is_dir() {
-                        for entry in std::fs::read_dir(path).unwrap() {
-                            todo.push(entry.unwrap().path());
-                        }
-                        continue;
-                    }
-                    // Only look at .rs files
-                    if !path.extension().map(|ext| ext == "rs").unwrap_or(false) {
-                        continue;
+    crossbeam::scope(|s| -> Result<()> {
+        // Create a thread that is in charge of walking the directory and submitting jobs.
+        // It closes the channel when it is done.
+        s.spawn(|_| {
+            let mut todo = VecDeque::new();
+            todo.push_back(config.root_dir.clone());
+            while let Some(path) = todo.pop_front() {
+                if path.is_dir() {
+                    // Enqueue everything inside this directory.
+                    // We want it sorted, to have some control over scheduling of slow tests.
+                    let mut entries =
+                        std::fs::read_dir(path).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
+                    entries.sort_by_key(|e| e.file_name());
+                    for entry in entries {
+                        todo.push_back(entry.path());
                     }
+                } else if path.extension().map(|ext| ext == "rs").unwrap_or(false) {
+                    // Forward .rs files to the test workers.
+                    submit.send(path).unwrap();
+                }
+            }
+            // There will be no more jobs. This signals the workers to quit.
+            // (This also ensures `submit` is moved into this closure.)
+            drop(submit);
+        });
+
+        let mut threads = vec![];
+
+        // Create N worker threads that receive files to test.
+        for _ in 0..std::thread::available_parallelism().unwrap().get() {
+            threads.push(s.spawn(|_| -> Result<()> {
+                for path in &receive {
                     if !config.path_filter.is_empty() {
                         let path_display = path.display().to_string();
                         if !config.path_filter.iter().any(|filter| path_display.contains(filter)) {
@@ -87,9 +107,9 @@ pub fn run_tests(config: Config) {
                             continue;
                         }
                     }
-                    let comments = Comments::parse_file(&path);
+                    let comments = Comments::parse_file(&path)?;
                     // Ignore file if only/ignore rules do (not) apply
-                    if ignore_file(&comments, &target) {
+                    if !test_file_conditions(&comments, &target, &config) {
                         ignored.fetch_add(1, Ordering::Relaxed);
                         eprintln!(
                             "{} ... {}",
@@ -126,10 +146,15 @@ pub fn run_tests(config: Config) {
                         }
                     }
                 }
-            });
+                Ok(())
+            }));
+        }
+        for thread in threads {
+            thread.join().unwrap()?;
         }
+        Ok(())
     })
-    .unwrap();
+    .unwrap()?;
 
     // Print all errors in a single thread to show reliable output
     let failures = failures.into_inner().unwrap();
@@ -139,28 +164,28 @@ pub fn run_tests(config: Config) {
     if !failures.is_empty() {
         for (path, miri, revision, errors, stderr) in &failures {
             eprintln!();
-            eprint!("{}", path.display().to_string().underline());
+            eprint!("{}", path.display().to_string().underline().bold());
             if !revision.is_empty() {
                 eprint!(" (revision `{}`)", revision);
             }
-            eprint!(" {}", "FAILED".red());
+            eprint!(" {}", "FAILED:".red().bold());
             eprintln!();
             eprintln!("command: {:?}", miri);
             eprintln!();
-            // `None` means never dump, as we already dumped it for an `OutputDiffers`
-            // `Some(false)` means there's no reason to dump, as all errors are independent of the stderr
-            // `Some(true)` means that there was a pattern in the .rs file that was not found in the output.
-            let mut dump_stderr = Some(false);
             for error in errors {
                 match error {
                     Error::ExitStatus(mode, exit_status) => eprintln!("{mode:?} got {exit_status}"),
                     Error::PatternNotFound { pattern, definition_line } => {
-                        eprintln!("`{pattern}` {} in stderr output", "not found".red());
+                        match pattern {
+                            Pattern::SubString(s) =>
+                                eprintln!("substring `{s}` {} in stderr output", "not found".red()),
+                            Pattern::Regex(r) =>
+                                eprintln!("`/{r}/` does {} stderr output", "not match".red()),
+                        }
                         eprintln!(
                             "expected because of pattern here: {}:{definition_line}",
                             path.display().to_string().bold()
                         );
-                        dump_stderr = dump_stderr.map(|_| true);
                     }
                     Error::NoPatternsFound => {
                         eprintln!("{}", "no error patterns found in failure test".red());
@@ -168,9 +193,6 @@ pub fn run_tests(config: Config) {
                     Error::PatternFoundInPassTest =>
                         eprintln!("{}", "error pattern found in success test".red()),
                     Error::OutputDiffers { path, actual, expected } => {
-                        if path.extension().unwrap() == "stderr" {
-                            dump_stderr = None;
-                        }
                         eprintln!("actual output differed from expected {}", path.display());
                         eprintln!("{}", pretty_assertions::StrComparison::new(expected, actual));
                         eprintln!()
@@ -194,21 +216,18 @@ pub fn run_tests(config: Config) {
                             eprintln!("    {level:?}: {message}")
                         }
                     }
-                    Error::ErrorPatternWithoutErrorAnnotation(path, line) => {
-                        eprintln!(
-                            "Annotation at {}:{line} matched an error diagnostic but did not have `ERROR` before its message",
-                            path.display()
-                        );
-                    }
                 }
                 eprintln!();
             }
-            if let Some(true) = dump_stderr {
-                eprintln!("actual stderr:");
-                eprintln!("{}", stderr);
-                eprintln!();
-            }
+            eprintln!("full stderr:");
+            eprintln!("{}", stderr);
+            eprintln!();
+        }
+        eprintln!("{}", "FAILURES:".red().underline().bold());
+        for (path, _miri, _revision, _errors, _stderr) in &failures {
+            eprintln!("    {}", path.display());
         }
+        eprintln!();
         eprintln!(
             "test result: {}. {} tests failed, {} tests passed, {} ignored, {} filtered out",
             "FAIL".red(),
@@ -228,6 +247,7 @@ pub fn run_tests(config: Config) {
         filtered.to_string().yellow(),
     );
     eprintln!();
+    Ok(())
 }
 
 #[derive(Debug)]
@@ -235,7 +255,7 @@ enum Error {
     /// Got an invalid exit status for the given mode.
     ExitStatus(Mode, ExitStatus),
     PatternNotFound {
-        pattern: String,
+        pattern: Pattern,
         definition_line: usize,
     },
     /// A ui test checking for failure does not have any failure patterns
@@ -252,7 +272,6 @@ enum Error {
         msgs: Vec<Message>,
         path: Option<(PathBuf, usize)>,
     },
-    ErrorPatternWithoutErrorAnnotation(PathBuf, usize),
 }
 
 type Errors = Vec<Error>;
@@ -322,17 +341,17 @@ fn check_test_result(
         revised("stderr"),
         target,
         &config.stderr_filters,
-        &config,
+        config,
         comments,
     );
     check_output(
-        &stdout,
+        stdout,
         path,
         errors,
         revised("stdout"),
         target,
         &config.stdout_filters,
-        &config,
+        config,
         comments,
     );
     // Check error annotations in the source against output
@@ -358,36 +377,16 @@ fn check_annotations(
     comments: &Comments,
 ) {
     if let Some((ref error_pattern, definition_line)) = comments.error_pattern {
-        let mut found = false;
-
         // first check the diagnostics messages outside of our file. We check this first, so that
-        // you can mix in-file annotations with // error-pattern annotations, even if there is overlap
+        // you can mix in-file annotations with //@error-pattern annotations, even if there is overlap
         // in the messages.
         if let Some(i) = messages_from_unknown_file_or_line
             .iter()
-            .position(|msg| msg.message.contains(error_pattern))
+            .position(|msg| error_pattern.matches(&msg.message))
         {
             messages_from_unknown_file_or_line.remove(i);
-            found = true;
-        }
-
-        // if nothing was found, check the ones inside our file. We permit this because some tests may have
-        // flaky line numbers for their messages.
-        if !found {
-            for line in &mut messages {
-                if let Some(i) = line.iter().position(|msg| msg.message.contains(error_pattern)) {
-                    line.remove(i);
-                    found = true;
-                    break;
-                }
-            }
-        }
-
-        if !found {
-            errors.push(Error::PatternNotFound {
-                pattern: error_pattern.to_string(),
-                definition_line,
-            });
+        } else {
+            errors.push(Error::PatternNotFound { pattern: error_pattern.clone(), definition_line });
         }
     }
 
@@ -395,7 +394,7 @@ fn check_annotations(
     // We will ensure that *all* diagnostics of level at least `lowest_annotation_level`
     // are matched.
     let mut lowest_annotation_level = Level::Error;
-    for &ErrorMatch { ref matched, revision: ref rev, definition_line, line, level } in
+    for &ErrorMatch { ref pattern, revision: ref rev, definition_line, line, level } in
         &comments.error_matches
     {
         if let Some(rev) = rev {
@@ -403,36 +402,31 @@ fn check_annotations(
                 continue;
             }
         }
-        if let Some(level) = level {
-            // If we found a diagnostic with a level annotation, make sure that all
-            // diagnostics of that level have annotations, even if we don't end up finding a matching diagnostic
-            // for this pattern.
-            lowest_annotation_level = std::cmp::min(lowest_annotation_level, level);
-        }
+
+        // If we found a diagnostic with a level annotation, make sure that all
+        // diagnostics of that level have annotations, even if we don't end up finding a matching diagnostic
+        // for this pattern.
+        lowest_annotation_level = std::cmp::min(lowest_annotation_level, level);
 
         if let Some(msgs) = messages.get_mut(line) {
-            let found = msgs.iter().position(|msg| {
-                msg.message.contains(matched)
-                    // in case there is no level on the annotation, match any level.
-                    && level.map_or(true, |level| {
-                        msg.level == level
-                    })
-            });
+            let found =
+                msgs.iter().position(|msg| pattern.matches(&msg.message) && msg.level == level);
             if let Some(found) = found {
-                let msg = msgs.remove(found);
-                if msg.level == Level::Error && level.is_none() {
-                    errors
-                        .push(Error::ErrorPatternWithoutErrorAnnotation(path.to_path_buf(), line));
-                }
+                msgs.remove(found);
                 continue;
             }
         }
 
-        errors.push(Error::PatternNotFound { pattern: matched.to_string(), definition_line });
+        errors.push(Error::PatternNotFound { pattern: pattern.clone(), definition_line });
     }
 
     let filter = |msgs: Vec<Message>| -> Vec<_> {
-        msgs.into_iter().filter(|msg| msg.level >= lowest_annotation_level).collect()
+        msgs.into_iter()
+            .filter(|msg| {
+                msg.level
+                    >= comments.require_annotations_for_level.unwrap_or(lowest_annotation_level)
+            })
+            .collect()
     };
 
     let messages_from_unknown_file_or_line = filter(messages_from_unknown_file_or_line);
@@ -493,41 +487,37 @@ fn check_output(
 
 fn output_path(path: &Path, comments: &Comments, kind: String, target: &str) -> PathBuf {
     if comments.stderr_per_bitwidth {
-        return path.with_extension(format!("{}.{kind}", get_pointer_width(target)));
+        return path.with_extension(format!("{}bit.{kind}", get_pointer_width(target)));
     }
     path.with_extension(kind)
 }
 
-fn ignore_file(comments: &Comments, target: &str) -> bool {
-    for s in &comments.ignore {
-        if target.contains(s) {
-            return true;
-        }
-        if get_pointer_width(target) == s {
-            return true;
-        }
+fn test_condition(condition: &Condition, target: &str, config: &Config) -> bool {
+    match condition {
+        Condition::Bitwidth(bits) => get_pointer_width(target) == *bits,
+        Condition::Target(t) => target.contains(t),
+        Condition::OnHost => config.target.is_none(),
     }
-    for s in &comments.only {
-        if !target.contains(s) {
-            return true;
-        }
-        if get_pointer_width(target) != s {
-            return true;
-        }
+}
+
+/// Returns whether according to the in-file conditions, this file should be run.
+fn test_file_conditions(comments: &Comments, target: &str, config: &Config) -> bool {
+    if comments.ignore.iter().any(|c| test_condition(c, target, config)) {
+        return false;
     }
-    false
+    comments.only.iter().all(|c| test_condition(c, target, config))
 }
 
 // Taken 1:1 from compiletest-rs
-fn get_pointer_width(triple: &str) -> &'static str {
+fn get_pointer_width(triple: &str) -> u8 {
     if (triple.contains("64") && !triple.ends_with("gnux32") && !triple.ends_with("gnu_ilp32"))
         || triple.starts_with("s390x")
     {
-        "64bit"
+        64
     } else if triple.starts_with("avr") {
-        "16bit"
+        16
     } else {
-        "32bit"
+        32
     }
 }