#![allow( clippy::enum_variant_names, clippy::useless_format, clippy::too_many_arguments, rustc::internal )] #![deny(missing_docs)] //! A crate to run the Rust compiler (or other binaries) and test their command line output. use bstr::ByteSlice; pub use color_eyre; use color_eyre::eyre::{eyre, Result}; use crossbeam_channel::{unbounded, Receiver, Sender}; use dependencies::{Build, BuildManager}; use lazy_static::lazy_static; use parser::{ErrorMatch, MaybeSpanned, OptWithLine, Revisioned, Spanned}; use regex::bytes::{Captures, Regex}; use rustc_stderr::{Level, Message, Span}; use status_emitter::{StatusEmitter, TestStatus}; use std::borrow::Cow; use std::collections::{HashSet, VecDeque}; use std::ffi::OsString; use std::num::NonZeroUsize; use std::path::{Component, Path, PathBuf, Prefix}; use std::process::{Command, ExitStatus}; use std::thread; use crate::parser::{Comments, Condition}; mod cmd; mod config; mod dependencies; mod diff; mod error; pub mod github_actions; mod mode; mod parser; mod rustc_stderr; pub mod status_emitter; #[cfg(test)] mod tests; pub use cmd::*; pub use config::*; pub use error::*; pub use mode::*; /// A filter's match rule. #[derive(Clone, Debug)] pub enum Match { /// If the regex matches, the filter applies Regex(Regex), /// If the exact byte sequence is found, the filter applies Exact(Vec), /// Uses a heuristic to find backslashes in windows style paths PathBackslash, } impl Match { fn replace_all<'a>(&self, text: &'a [u8], replacement: &[u8]) -> Cow<'a, [u8]> { match self { Match::Regex(regex) => regex.replace_all(text, replacement), Match::Exact(needle) => text.replace(needle, replacement).into(), Match::PathBackslash => { lazy_static! { static ref PATH_RE: Regex = Regex::new( r"(?x) (?: # Match paths to files with extensions that don't include spaces \\(?:[\pL\pN.\-_']+[/\\])*[\pL\pN.\-_']+\.\pL+ | # Allow spaces in absolute paths [A-Z]:\\(?:[\pL\pN.\-_'\ ]+[/\\])+ )", ) .unwrap(); } PATH_RE.replace_all(text, |caps: &Captures<'_>| { caps[0].replace(r"\", replacement) }) } } } } impl From<&'_ Path> for Match { fn from(v: &Path) -> Self { let mut v = v.display().to_string(); // Normalize away windows canonicalized paths. if v.starts_with(r"\\?\") { v.drain(0..4); } let mut v = v.into_bytes(); // Normalize paths on windows to use slashes instead of backslashes, // So that paths are rendered the same on all systems. for c in &mut v { if *c == b'\\' { *c = b'/'; } } Self::Exact(v) } } impl From for Match { fn from(v: Regex) -> Self { Self::Regex(v) } } /// Replacements to apply to output files. pub type Filter = Vec<(Match, &'static [u8])>; /// Run all tests as described in the config argument. /// Will additionally process command line arguments. pub fn run_tests(mut config: Config) -> Result<()> { let args = Args::test()?; if !args.quiet { println!("Compiler: {}", config.program.display()); } let name = config.root_dir.display().to_string(); let text = if args.quiet { status_emitter::Text::quiet() } else { status_emitter::Text::verbose() }; config.with_args(&args, true); run_tests_generic( vec![config], default_file_filter, default_per_file_config, (text, status_emitter::Gha:: { name }), ) } /// The filter used by `run_tests` to only run on `.rs` files that are /// specified by [`Config::filter_files`] and [`Config::skip_files`]. pub fn default_file_filter(path: &Path, config: &Config) -> bool { path.extension().is_some_and(|ext| ext == "rs") && default_any_file_filter(path, config) } /// Run on all files that are specified by [`Config::filter_files`] and /// [`Config::skip_files`]. /// /// To only include rust files see [`default_file_filter`]. pub fn default_any_file_filter(path: &Path, config: &Config) -> bool { let path = path.display().to_string(); let contains_path = |files: &[String]| files.iter().any(|f| path.contains(f)); if contains_path(&config.skip_files) { return false; } config.filter_files.is_empty() || contains_path(&config.filter_files) } /// The default per-file config used by `run_tests`. pub fn default_per_file_config(config: &mut Config, _path: &Path, file_contents: &[u8]) { // Heuristic: // * if the file contains `#[test]`, automatically pass `--cfg test`. // * if the file does not contain `fn main()` or `#[start]`, automatically pass `--crate-type=lib`. // This avoids having to spam `fn main() {}` in almost every test. if file_contents.find(b"#[proc_macro").is_some() { config.program.args.push("--crate-type=proc-macro".into()) } else if file_contents.find(b"#[test]").is_some() { config.program.args.push("--test".into()); } else if file_contents.find(b"fn main()").is_none() && file_contents.find(b"#[start]").is_none() { config.program.args.push("--crate-type=lib".into()); } } /// Create a command for running a single file, with the settings from the `config` argument. /// Ignores various settings from `Config` that relate to finding test files. pub fn test_command(mut config: Config, path: &Path) -> Result { config.fill_host_and_target()?; let extra_args = config.build_dependencies()?; let comments = Comments::parse_file(path)?.map_err(|errors| color_eyre::eyre::eyre!("{errors:#?}"))?; let mut result = build_command(path, &config, "", &comments).unwrap(); result.args(extra_args); Ok(result) } /// The possible non-failure results a single test can have. pub enum TestOk { /// The test passed Ok, /// The test was ignored due to a rule (`//@only-*` or `//@ignore-*`) Ignored, /// The test was filtered with the `file_filter` argument. Filtered, } /// The possible results a single test can have. pub type TestResult = Result; /// Information about a test failure. #[derive(Debug)] pub struct Errored { /// Command that failed command: Command, /// The errors that were encountered. errors: Vec, /// The full stderr of the test run. stderr: Vec, /// The full stdout of the test run. stdout: Vec, } struct TestRun { result: TestResult, status: Box, } /// A version of `run_tests` that allows more fine-grained control over running tests. /// /// If multiple configs are provided only the first [`Config::threads`] value is used pub fn run_tests_generic( mut configs: Vec, file_filter: impl Fn(&Path, &Config) -> bool + Sync, per_file_config: impl Fn(&mut Config, &Path, &[u8]) + Sync, status_emitter: impl StatusEmitter + Send, ) -> Result<()> { for config in &mut configs { config.fill_host_and_target()?; } let build_manager = BuildManager::new(&status_emitter); let mut results = vec![]; let num_threads = match configs.first().and_then(|config| config.threads) { Some(threads) => threads, None => match std::env::var_os("RUST_TEST_THREADS") { Some(n) => n .to_str() .ok_or_else(|| eyre!("could not parse RUST_TEST_THREADS env var"))? .parse()?, None => std::thread::available_parallelism()?, }, }; run_and_collect( num_threads, |submit| { let mut todo = VecDeque::new(); for config in &configs { todo.push_back((config.root_dir.clone(), config)); } while let Some((path, config)) = todo.pop_front() { if path.is_dir() { if path.file_name().unwrap() == "auxiliary" { continue; } // 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() .map(|e| e.unwrap().path()) .collect::>(); entries.sort_by(|a, b| a.file_name().cmp(&b.file_name())); for entry in entries { todo.push_back((entry, config)); } } else if file_filter(&path, config) { let status = status_emitter.register_test(path); // Forward .rs files to the test workers. submit.send((status, config)).unwrap(); } } }, |receive, finished_files_sender| -> Result<()> { for (status, config) in receive { let path = status.path(); let file_contents = std::fs::read(path).unwrap(); let mut config = config.clone(); per_file_config(&mut config, path, &file_contents); let result = match std::panic::catch_unwind(|| { parse_and_test_file(&build_manager, &status, config, file_contents) }) { Ok(Ok(res)) => res, Ok(Err(err)) => { finished_files_sender.send(TestRun { result: Err(err), status, })?; continue; } Err(err) => { finished_files_sender.send(TestRun { result: Err(Errored { command: Command::new(""), errors: vec![Error::Bug( *Box::::downcast::( err, ) .unwrap(), )], stderr: vec![], stdout: vec![], }), status, })?; continue; } }; for result in result { finished_files_sender.send(result)?; } } Ok(()) }, |finished_files_recv| { for run in finished_files_recv { run.status.done(&run.result); results.push(run); } }, )?; let mut failures = vec![]; let mut succeeded = 0; let mut ignored = 0; let mut filtered = 0; for run in results { match run.result { Ok(TestOk::Ok) => succeeded += 1, Ok(TestOk::Ignored) => ignored += 1, Ok(TestOk::Filtered) => filtered += 1, Err(errored) => failures.push((run.status, errored)), } } let mut failure_emitter = status_emitter.finalize(failures.len(), succeeded, ignored, filtered); for ( status, Errored { command, errors, stderr, stdout, }, ) in &failures { let _guard = status.failed_test(command, stderr, stdout); failure_emitter.test_failure(status, errors); } if failures.is_empty() { Ok(()) } else { Err(eyre!("tests failed")) } } /// A generic multithreaded runner that has a thread for producing work, /// a thread for collecting work, and `num_threads` threads for doing the work. pub fn run_and_collect( num_threads: NonZeroUsize, submitter: impl FnOnce(Sender) + Send, runner: impl Sync + Fn(&Receiver, Sender) -> Result<()>, collector: impl FnOnce(Receiver) + Send, ) -> Result<()> { // A channel for files to process let (submit, receive) = unbounded(); thread::scope(|s| { // Create a thread that is in charge of walking the directory and submitting jobs. // It closes the channel when it is done. s.spawn(|| submitter(submit)); // A channel for the messages emitted by the individual test threads. // Used to produce live updates while running the tests. let (finished_files_sender, finished_files_recv) = unbounded(); s.spawn(|| collector(finished_files_recv)); let mut threads = vec![]; // Create N worker threads that receive files to test. for _ in 0..num_threads.get() { let finished_files_sender = finished_files_sender.clone(); threads.push(s.spawn(|| runner(&receive, finished_files_sender))); } for thread in threads { thread.join().unwrap()?; } Ok(()) }) } fn parse_and_test_file( build_manager: &BuildManager<'_>, status: &dyn TestStatus, mut config: Config, file_contents: Vec, ) -> Result, Errored> { let comments = parse_comments(&file_contents)?; const EMPTY: &[String] = &[String::new()]; // Run the test for all revisions let revisions = comments.revisions.as_deref().unwrap_or(EMPTY); let mut built_deps = false; Ok(revisions .iter() .map(|revision| { let status = status.for_revision(revision); // Ignore file if only/ignore rules do (not) apply if !status.test_file_conditions(&comments, &config) { return TestRun { result: Ok(TestOk::Ignored), status, }; } if !built_deps { status.update_status("waiting for dependencies to finish building".into()); match build_manager.build(Build::Dependencies, &config) { Ok(extra_args) => config.program.args.extend(extra_args), Err(err) => { return TestRun { result: Err(err), status, } } } status.update_status(String::new()); built_deps = true; } let result = status.run_test(build_manager, &config, &comments); TestRun { result, status } }) .collect()) } fn parse_comments(file_contents: &[u8]) -> Result { match Comments::parse(file_contents) { Ok(comments) => Ok(comments), Err(errors) => Err(Errored { command: Command::new("parse comments"), errors, stderr: vec![], stdout: vec![], }), } } fn build_command( path: &Path, config: &Config, revision: &str, comments: &Comments, ) -> Result { let mut cmd = config.program.build(&config.out_dir); cmd.arg(path); if !revision.is_empty() { cmd.arg(format!("--cfg={revision}")); } for arg in comments .for_revision(revision) .flat_map(|r| r.compile_flags.iter()) { cmd.arg(arg); } let edition = comments.edition(revision, config)?; if let Some(edition) = edition { cmd.arg("--edition").arg(&*edition); } cmd.envs( comments .for_revision(revision) .flat_map(|r| r.env_vars.iter()) .map(|(k, v)| (k, v)), ); Ok(cmd) } fn build_aux( aux_file: &Path, config: &Config, build_manager: &BuildManager<'_>, ) -> std::result::Result, Errored> { let file_contents = std::fs::read(aux_file).map_err(|err| Errored { command: Command::new(format!("reading aux file `{}`", aux_file.display())), errors: vec![], stderr: err.to_string().into_bytes(), stdout: vec![], })?; let comments = parse_comments(&file_contents)?; assert_eq!( comments.revisions, None, "aux builds cannot specify revisions" ); let mut config = config.clone(); // Strip any `crate-type` flags from the args, as we need to set our own, // and they may conflict (e.g. `lib` vs `proc-macro`); let mut prev_was_crate_type = false; config.program.args.retain(|arg| { if prev_was_crate_type { prev_was_crate_type = false; return false; } if arg == "--test" { false } else if arg == "--crate-type" { prev_was_crate_type = true; false } else if let Some(arg) = arg.to_str() { !arg.starts_with("--crate-type=") } else { true } }); default_per_file_config(&mut config, aux_file, &file_contents); // Put aux builds into a separate directory per path so that multiple aux files // from different directories (but with the same file name) don't collide. let relative = strip_path_prefix(aux_file.parent().unwrap(), &config.out_dir); config.out_dir.extend(relative); let mut aux_cmd = build_command(aux_file, &config, "", &comments)?; let mut extra_args = build_aux_files( aux_file.parent().unwrap(), &comments, "", &config, build_manager, )?; // Make sure we see our dependencies aux_cmd.args(extra_args.iter()); aux_cmd.arg("--emit=link"); let filename = aux_file.file_stem().unwrap().to_str().unwrap(); let output = aux_cmd.output().unwrap(); if !output.status.success() { let error = Error::Command { kind: "compilation of aux build failed".to_string(), status: output.status, }; return Err(Errored { command: aux_cmd, errors: vec![error], stderr: rustc_stderr::process(aux_file, &output.stderr).rendered, stdout: output.stdout, }); } // Now run the command again to fetch the output filenames aux_cmd.arg("--print").arg("file-names"); let output = aux_cmd.output().unwrap(); assert!(output.status.success()); for file in output.stdout.lines() { let file = std::str::from_utf8(file).unwrap(); let crate_name = filename.replace('-', "_"); let path = config.out_dir.join(file); extra_args.push("--extern".into()); let mut cname = OsString::from(&crate_name); cname.push("="); cname.push(path); extra_args.push(cname); // Help cargo find the crates added with `--extern`. extra_args.push("-L".into()); extra_args.push(config.out_dir.as_os_str().to_os_string()); } Ok(extra_args) } impl dyn TestStatus { fn run_test( &self, build_manager: &BuildManager<'_>, config: &Config, comments: &Comments, ) -> TestResult { let path = self.path(); let revision = self.revision(); let extra_args = build_aux_files( &path.parent().unwrap().join("auxiliary"), comments, revision, config, build_manager, )?; let mut cmd = build_command(path, config, revision, comments)?; cmd.args(&extra_args); let (status, stderr, stdout) = self.run_command(&mut cmd); let mode = config.mode.maybe_override(comments, revision)?; match *mode { Mode::Run { .. } if Mode::Pass.ok(status).is_ok() => { return run_test_binary(mode, path, revision, comments, cmd, config) } Mode::Panic | Mode::Yolo { .. } => {} Mode::Run { .. } | Mode::Pass | Mode::Fail { .. } => { if status.code() == Some(101) { let stderr = String::from_utf8_lossy(&stderr); let stdout = String::from_utf8_lossy(&stdout); return Err(Errored { command: cmd, errors: vec![Error::Bug(format!( "test panicked: stderr:\n{stderr}\nstdout:\n{stdout}", ))], stderr: vec![], stdout: vec![], }); } } } check_test_result( cmd, *mode, path, config, revision, comments, status, &stdout, &stderr, )?; run_rustfix( &stderr, &stdout, path, comments, revision, config, *mode, extra_args, )?; Ok(TestOk::Ok) } /// Run a command, and if it takes more than 100ms, start appending the last stderr/stdout /// line to the current status spinner. fn run_command(&self, cmd: &mut Command) -> (ExitStatus, Vec, Vec) { let output = cmd.output().unwrap_or_else(|err| { panic!( "could not spawn `{:?}` as a process: {err}", cmd.get_program() ) }); (output.status, output.stderr, output.stdout) } } fn build_aux_files( aux_dir: &Path, comments: &Comments, revision: &str, config: &Config, build_manager: &BuildManager<'_>, ) -> Result, Errored> { let mut extra_args = vec![]; for rev in comments.for_revision(revision) { for aux in &rev.aux_builds { let line = aux.line(); let aux = &**aux; let aux_file = if aux.starts_with("..") { aux_dir.parent().unwrap().join(aux) } else { aux_dir.join(aux) }; extra_args.extend( build_manager .build( Build::Aux { aux_file: strip_path_prefix( &aux_file.canonicalize().map_err(|err| Errored { command: Command::new(format!( "canonicalizing path `{}`", aux_file.display() )), errors: vec![], stderr: err.to_string().into_bytes(), stdout: vec![], })?, &std::env::current_dir().unwrap(), ) .collect(), }, config, ) .map_err( |Errored { command, errors, stderr, stdout, }| Errored { command, errors: vec![Error::Aux { path: aux_file, errors, line, }], stderr, stdout, }, )?, ); } } Ok(extra_args) } fn run_test_binary( mode: MaybeSpanned, path: &Path, revision: &str, comments: &Comments, mut cmd: Command, config: &Config, ) -> TestResult { cmd.arg("--print").arg("file-names"); let output = cmd.output().unwrap(); assert!(output.status.success()); let mut files = output.stdout.lines(); let file = files.next().unwrap(); assert_eq!(files.next(), None); let file = std::str::from_utf8(file).unwrap(); let exe = config.out_dir.join(file); let mut exe = Command::new(exe); let output = exe.output().unwrap(); let mut errors = vec![]; check_test_output( path, &mut errors, revision, config, comments, &output.stdout, &output.stderr, ); errors.extend(mode.ok(output.status).err()); if errors.is_empty() { Ok(TestOk::Ok) } else { Err(Errored { command: exe, errors, stderr: vec![], stdout: vec![], }) } } fn run_rustfix( stderr: &[u8], stdout: &[u8], path: &Path, comments: &Comments, revision: &str, config: &Config, mode: Mode, extra_args: Vec, ) -> Result<(), Errored> { let no_run_rustfix = comments.find_one_for_revision(revision, "`no-rustfix` annotations", |r| r.no_rustfix)?; let global_rustfix = match mode { Mode::Pass | Mode::Run { .. } | Mode::Panic => RustfixMode::Disabled, Mode::Fail { rustfix, .. } | Mode::Yolo { rustfix } => rustfix, }; let fixed_code = (no_run_rustfix.is_none() && global_rustfix.enabled()) .then_some(()) .and_then(|()| { let suggestions = std::str::from_utf8(stderr) .unwrap() .lines() .flat_map(|line| { if !line.starts_with('{') { return vec![]; } rustfix::get_suggestions_from_json( line, &HashSet::new(), if global_rustfix == RustfixMode::Everything { rustfix::Filter::Everything } else { rustfix::Filter::MachineApplicableOnly }, ) .unwrap_or_else(|err| { panic!("could not deserialize diagnostics json for rustfix {err}:{line}") }) }) .collect::>(); if suggestions.is_empty() { None } else { Some(rustfix::apply_suggestions( &std::fs::read_to_string(path).unwrap(), &suggestions, )) } }) .transpose() .map_err(|err| Errored { command: Command::new(format!("rustfix {}", path.display())), errors: vec![Error::Rustfix(err)], stderr: stderr.into(), stdout: stdout.into(), })?; let edition = comments.edition(revision, config)?; let edition = edition .map(|mwl| { let line = mwl.span().unwrap_or(Span::INVALID); Spanned::new(mwl.into_inner(), line) }) .into(); let rustfix_comments = Comments { revisions: None, revisioned: std::iter::once(( vec![], Revisioned { span: Span::INVALID, ignore: vec![], only: vec![], stderr_per_bitwidth: false, compile_flags: comments .for_revision(revision) .flat_map(|r| r.compile_flags.iter().cloned()) .collect(), env_vars: comments .for_revision(revision) .flat_map(|r| r.env_vars.iter().cloned()) .collect(), normalize_stderr: vec![], normalize_stdout: vec![], error_in_other_files: vec![], error_matches: vec![], require_annotations_for_level: Default::default(), aux_builds: comments .for_revision(revision) .flat_map(|r| r.aux_builds.iter().cloned()) .collect(), edition, mode: OptWithLine::new(Mode::Pass, Span::INVALID), no_rustfix: OptWithLine::new((), Span::INVALID), needs_asm_support: false, }, )) .collect(), }; let run = fixed_code.is_some(); let mut errors = vec![]; let rustfix_path = check_output( // Always check for `.fixed` files, even if there were reasons not to run rustfix. // We don't want to leave around stray `.fixed` files fixed_code.unwrap_or_default().as_bytes(), path, &mut errors, "fixed", &Filter::default(), config, &rustfix_comments, revision, ); if !errors.is_empty() { return Err(Errored { command: Command::new(format!("checking {}", path.display())), errors, stderr: vec![], stdout: vec![], }); } if !run { return Ok(()); } let mut cmd = build_command(&rustfix_path, config, revision, &rustfix_comments)?; cmd.args(extra_args); // picking the crate name from the file name is problematic when `.revision_name` is inserted cmd.arg("--crate-name").arg( path.file_stem() .unwrap() .to_str() .unwrap() .replace('-', "_"), ); let output = cmd.output().unwrap(); if output.status.success() { Ok(()) } else { Err(Errored { command: cmd, errors: vec![Error::Command { kind: "rustfix".into(), status: output.status, }], stderr: rustc_stderr::process(&rustfix_path, &output.stderr).rendered, stdout: output.stdout, }) } } fn revised(revision: &str, extension: &str) -> String { if revision.is_empty() { extension.to_string() } else { format!("{revision}.{extension}") } } fn check_test_result( command: Command, mode: Mode, path: &Path, config: &Config, revision: &str, comments: &Comments, status: ExitStatus, stdout: &[u8], stderr: &[u8], ) -> Result<(), Errored> { let mut errors = vec![]; errors.extend(mode.ok(status).err()); // Always remove annotation comments from stderr. let diagnostics = rustc_stderr::process(path, stderr); check_test_output( path, &mut errors, revision, config, comments, stdout, &diagnostics.rendered, ); // Check error annotations in the source against output check_annotations( diagnostics.messages, diagnostics.messages_from_unknown_file_or_line, path, &mut errors, config, revision, comments, )?; if errors.is_empty() { Ok(()) } else { Err(Errored { command, errors, stderr: diagnostics.rendered, stdout: stdout.into(), }) } } fn check_test_output( path: &Path, errors: &mut Vec, revision: &str, config: &Config, comments: &Comments, stdout: &[u8], stderr: &[u8], ) { // Check output files (if any) // Check output files against actual output check_output( stderr, path, errors, "stderr", &config.stderr_filters, config, comments, revision, ); check_output( stdout, path, errors, "stdout", &config.stdout_filters, config, comments, revision, ); } fn check_annotations( mut messages: Vec>, mut messages_from_unknown_file_or_line: Vec, path: &Path, errors: &mut Errors, config: &Config, revision: &str, comments: &Comments, ) -> Result<(), Errored> { let error_patterns = comments .for_revision(revision) .flat_map(|r| r.error_in_other_files.iter()); let mut seen_error_match = false; for error_pattern in error_patterns { seen_error_match = true; // first check the diagnostics messages outside of our file. We check this first, so that // you can mix in-file annotations with //@error-in-other-file annotations, even if there is overlap // in the messages. if let Some(i) = messages_from_unknown_file_or_line .iter() .position(|msg| error_pattern.matches(&msg.message)) { messages_from_unknown_file_or_line.remove(i); } else { errors.push(Error::PatternNotFound(error_pattern.clone())); } } // The order on `Level` is such that `Error` is the highest level. // 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 pattern, level, line, } in comments .for_revision(revision) .flat_map(|r| r.error_matches.iter()) { seen_error_match = true; // 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. if lowest_annotation_level > level { lowest_annotation_level = level; } if let Some(msgs) = messages.get_mut(line.get()) { let found = msgs .iter() .position(|msg| pattern.matches(&msg.message) && msg.level == level); if let Some(found) = found { msgs.remove(found); continue; } } errors.push(Error::PatternNotFound(pattern.clone())); } let required_annotation_level = comments.find_one_for_revision( revision, "`require_annotations_for_level` annotations", |r| r.require_annotations_for_level, )?; let required_annotation_level = required_annotation_level.map_or(lowest_annotation_level, |l| *l); let filter = |mut msgs: Vec| -> Vec<_> { msgs.retain(|msg| msg.level >= required_annotation_level); msgs }; let mode = config.mode.maybe_override(comments, revision)?; if !matches!(config.mode, Mode::Yolo { .. }) { let messages_from_unknown_file_or_line = filter(messages_from_unknown_file_or_line); if !messages_from_unknown_file_or_line.is_empty() { errors.push(Error::ErrorsWithoutPattern { path: None, msgs: messages_from_unknown_file_or_line, }); } for (line, msgs) in messages.into_iter().enumerate() { let msgs = filter(msgs); if !msgs.is_empty() { let line = NonZeroUsize::new(line).expect("line 0 is always empty"); errors.push(Error::ErrorsWithoutPattern { path: Some(Spanned::new( path.to_path_buf(), Span { line_start: line, ..Span::INVALID }, )), msgs, }); } } } match (*mode, seen_error_match) { (Mode::Pass, true) | (Mode::Panic, true) => errors.push(Error::PatternFoundInPassTest), ( Mode::Fail { require_patterns: true, .. }, false, ) => errors.push(Error::NoPatternsFound), _ => {} } Ok(()) } fn check_output( output: &[u8], path: &Path, errors: &mut Errors, kind: &'static str, filters: &Filter, config: &Config, comments: &Comments, revision: &str, ) -> PathBuf { let target = config.target.as_ref().unwrap(); let output = normalize(path, output, filters, comments, revision, kind); let path = output_path(path, comments, revised(revision, kind), target, revision); match &config.output_conflict_handling { OutputConflictHandling::Error(bless_command) => { let expected_output = std::fs::read(&path).unwrap_or_default(); if output != expected_output { errors.push(Error::OutputDiffers { path: path.clone(), actual: output.clone(), expected: expected_output, bless_command: bless_command.clone(), }); } } OutputConflictHandling::Bless => { if output.is_empty() { let _ = std::fs::remove_file(&path); } else { std::fs::write(&path, &output).unwrap(); } } OutputConflictHandling::Ignore => {} } path } fn output_path( path: &Path, comments: &Comments, kind: String, target: &str, revision: &str, ) -> PathBuf { if comments .for_revision(revision) .any(|r| r.stderr_per_bitwidth) { return path.with_extension(format!("{}bit.{kind}", get_pointer_width(target))); } path.with_extension(kind) } fn test_condition(condition: &Condition, config: &Config) -> bool { let target = config.target.as_ref().unwrap(); match condition { Condition::Bitwidth(bits) => get_pointer_width(target) == *bits, Condition::Target(t) => target.contains(t), Condition::Host(t) => config.host.as_ref().unwrap().contains(t), Condition::OnHost => target == config.host.as_ref().unwrap(), } } impl dyn TestStatus { /// Returns whether according to the in-file conditions, this file should be run. fn test_file_conditions(&self, comments: &Comments, config: &Config) -> bool { let revision = self.revision(); if comments .for_revision(revision) .flat_map(|r| r.ignore.iter()) .any(|c| test_condition(c, config)) { return false; } if comments .for_revision(revision) .any(|r| r.needs_asm_support && !config.has_asm_support()) { return false; } comments .for_revision(revision) .flat_map(|r| r.only.iter()) .all(|c| test_condition(c, config)) } } // Taken 1:1 from compiletest-rs fn get_pointer_width(triple: &str) -> u8 { if (triple.contains("64") && !triple.ends_with("gnux32") && !triple.ends_with("gnu_ilp32")) || triple.starts_with("s390x") { 64 } else if triple.starts_with("avr") { 16 } else { 32 } } fn normalize( path: &Path, text: &[u8], filters: &Filter, comments: &Comments, revision: &str, kind: &'static str, ) -> Vec { // Useless paths let path_filter = (Match::from(path.parent().unwrap()), b"$DIR" as &[u8]); let filters = filters.iter().chain(std::iter::once(&path_filter)); let mut text = text.to_owned(); if let Some(lib_path) = option_env!("RUSTC_LIB_PATH") { text = text.replace(lib_path, "RUSTLIB"); } for (rule, replacement) in filters { text = rule.replace_all(&text, replacement).into_owned(); } for (from, to) in comments.for_revision(revision).flat_map(|r| match kind { "fixed" => &[] as &[_], "stderr" => &r.normalize_stderr, "stdout" => &r.normalize_stdout, _ => unreachable!(), }) { text = from.replace_all(&text, to).into_owned(); } text } /// Remove the common prefix of this path and the `root_dir`. fn strip_path_prefix<'a>(path: &'a Path, prefix: &Path) -> impl Iterator> { let mut components = path.components(); for c in prefix.components() { // Windows has some funky paths. This is probably wrong, but works well in practice. let deverbatimize = |c| match c { Component::Prefix(prefix) => Err(match prefix.kind() { Prefix::VerbatimUNC(a, b) => Prefix::UNC(a, b), Prefix::VerbatimDisk(d) => Prefix::Disk(d), other => other, }), c => Ok(c), }; let c2 = components.next(); if Some(deverbatimize(c)) == c2.map(deverbatimize) { continue; } return c2.into_iter().chain(components); } None.into_iter().chain(components) }