diff --git a/Cargo.lock b/Cargo.lock index 1c1607e4c1ac4..c03b75ec136be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -822,6 +822,7 @@ dependencies = [ "rustfix 0.8.1", "serde", "serde_json", + "strsim", "tracing", "tracing-subscriber", "unified-diff", diff --git a/src/tools/compiletest/Cargo.toml b/src/tools/compiletest/Cargo.toml index 52beb4c8b3dec..cca49e1d1c88c 100644 --- a/src/tools/compiletest/Cargo.toml +++ b/src/tools/compiletest/Cargo.toml @@ -25,6 +25,7 @@ walkdir = "2" glob = "0.3.0" anyhow = "1" home = "0.5.5" +strsim = "0.11.1" [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/src/tools/compiletest/src/lib.rs b/src/tools/compiletest/src/lib.rs index 0cf05b32e9681..527813ec4ae9c 100644 --- a/src/tools/compiletest/src/lib.rs +++ b/src/tools/compiletest/src/lib.rs @@ -13,6 +13,7 @@ pub mod compute_diff; pub mod errors; pub mod header; mod json; +mod load_cfg; mod raise_fd_limit; mod read2; pub mod runtest; diff --git a/src/tools/compiletest/src/load_cfg.rs b/src/tools/compiletest/src/load_cfg.rs new file mode 100644 index 0000000000000..5c0d48ca3d533 --- /dev/null +++ b/src/tools/compiletest/src/load_cfg.rs @@ -0,0 +1,169 @@ +#![allow(unused)] + +mod itemlist; +mod prepare; + +use error::{Error, ErrorExt, Result}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct LineCol { + pub line: usize, + pub col: usize, +} + +impl LineCol { + const fn new(line: usize, col: usize) -> Self { + Self { line, col } + } +} + +/// When we need to e.g. build regexes that include a pattern, we need to know what kind of +/// comments to use. Usually we just build a regex for all expressions, even though we don't +/// use them. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum CommentTy { + Slashes, + Hash, + Semi, +} + +impl CommentTy { + const fn as_str(self) -> &'static str { + match self { + CommentTy::Slashes => "//", + CommentTy::Hash => "#", + CommentTy::Semi => ";", + } + } + + const fn directive(self) -> &'static str { + match self { + CommentTy::Slashes => "//@", + CommentTy::Hash => "#@", + CommentTy::Semi => ";@", + } + } + + const fn all() -> &'static [Self] { + &[Self::Slashes, Self::Hash, Self::Semi] + } +} + +/// Errors used within the `load_cfg` module +mod error { + use std::fmt; + use std::path::{Path, PathBuf}; + + use super::LineCol; + + pub type Error = Box; + pub type Result = std::result::Result; + + #[derive(Debug)] + struct FullError { + msg: Box, + fname: Option, + pos: Option, + context: Vec>, + } + + impl fmt::Display for FullError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "error: {}", self.msg)?; + + let path = self.fname.as_ref().map_or(Path::new("unknown").display(), |p| p.display()); + write!(f, "\n parsing '{path}'",)?; + + let pos = self.pos.unwrap_or_default(); + write!(f, " at line {}, column {}", pos.line, pos.col)?; + + if !self.context.is_empty() { + write!(f, "\ncontext: {:#?}", self.context)?; + } + + Ok(()) + } + } + + impl std::error::Error for FullError {} + + /// Give us an easy way to tack context onto an error. + pub trait ErrorExt { + fn pos(self, pos: LineCol) -> Self; + fn line(self, line: usize) -> Self; + fn col(self, col: usize) -> Self; + fn fname(self, fname: impl Into) -> Self; + fn context(self, ctx: impl Into>) -> Self; + } + + impl ErrorExt for Error { + fn pos(self, pos: LineCol) -> Self { + let mut fe = to_fullerr(self); + fe.pos = Some(pos); + fe + } + + fn line(self, line: usize) -> Self { + let mut fe = to_fullerr(self); + match fe.pos.as_mut() { + Some(v) => v.line = line, + None => fe.pos = Some(LineCol::new(line, 0)), + }; + fe + } + + fn col(self, col: usize) -> Self { + let mut fe = to_fullerr(self); + match fe.pos.as_mut() { + Some(v) => v.col = col, + None => fe.pos = Some(LineCol::new(0, col)), + }; + fe + } + + fn fname(self, fname: impl Into) -> Self { + let mut fe = to_fullerr(self); + fe.fname = Some(fname.into()); + fe + } + + fn context(self, ctx: impl Into>) -> Self { + let mut fe = to_fullerr(self); + fe.context.push(ctx.into()); + fe + } + } + + impl ErrorExt for Result { + fn pos(self, pos: LineCol) -> Self { + self.map_err(|e| e.pos(pos)) + } + + fn line(self, line: usize) -> Self { + self.map_err(|e| e.line(line)) + } + + fn col(self, col: usize) -> Self { + self.map_err(|e| e.col(col)) + } + + fn fname(self, fname: impl Into) -> Self { + self.map_err(|e| e.fname(fname)) + } + + fn context(self, ctx: impl Into>) -> Self { + self.map_err(|e| e.context(ctx)) + } + } + + fn to_fullerr(e: Error) -> Box { + e.downcast().unwrap_or_else(|e| { + Box::new(FullError { + msg: e.to_string().into(), + fname: None, + pos: None, + context: Vec::new(), + }) + }) + } +} diff --git a/src/tools/compiletest/src/load_cfg/itemlist.rs b/src/tools/compiletest/src/load_cfg/itemlist.rs new file mode 100644 index 0000000000000..bb95484432ad6 --- /dev/null +++ b/src/tools/compiletest/src/load_cfg/itemlist.rs @@ -0,0 +1,733 @@ +//! Extract metadata from a test file to a basic structure +//! +//! This module handles only the most basic collection of directives and other metadata; +//! [`ItemTest`] just acts as a non-recursive `TokenTree`. We try to keep everything in this file +//! as minimal as possible. That is, this just parses the approximate shape of data but leaves it +//! up to another module to validate the data's content (which helps to raise errors on typos that +//! would otherwise just not get picked up). + +use std::{ + cell::{Cell, LazyCell}, + ops::ControlFlow, + rc::Rc, + sync::LazyLock, +}; + +use super::{CommentTy, Error, ErrorExt, LineCol, Result}; +use regex::Regex; + +const STRSIM_CONFIDENCE: f64 = 0.7; + +pub fn parse(s: &str, cty: CommentTy) -> Result, Vec> { + let mut items = Vec::new(); + let mut errors = Vec::new(); + + for (idx, line) in s.lines().enumerate() { + let line_no = idx + 1; + match try_match_line(line, cty).line(line_no) { + Ok(Some(item)) => items.push(Item::new(item, line_no, 1)), + Ok(None) => (), + Err(e) => errors.push(e), + } + } + + Item::visit_expand_nested(&mut items).map_err(|e| errors.push(e)); + + if errors.is_empty() { Ok(items) } else { Err(errors) } +} + +/// A type for mapping `CommentTy`s to an associated regex. +type CommentRePats = [(CommentTy, Regex); CommentTy::all().len()]; + +#[derive(Clone, Debug, PartialEq)] +pub struct Item<'src> { + pub val: ItemVal<'src>, + pub pos: LineCol, + /// Purely a debugging tool; we set this to `true` when we consume the `Item` in some way + /// when setting the config (other module), which allows us to assert that nothing + /// accidentally goes unused. + pub(super) used: Cell, +} + +impl<'src> Item<'src> { + fn new(item: ItemVal<'src>, line: usize, col: usize) -> Self { + Self { val: item, pos: LineCol { line, col }, used: Cell::new(false) } + } + + /// Expand any items that may have nested items. + fn visit_expand_nested(list: &mut Vec) -> Result<()> { + for item in list.iter_mut() { + match item.val { + // Revision-specific items need to have their content parsed. E.g. + // `//@ [abc,def] compile-flags: -O` + ItemVal::RevisionSpecificItems { revs, content } => { + let make_err = || { + Err("unable to parse revision directive '{content}'".into()).pos(item.pos) + }; + let content = match_any( + |matcher, pass| matcher.try_match_directive(content, pass), + make_err, + )? + .ok_or_else(|| make_err().unwrap_err())?; + + *item = Item { + val: ItemVal::RevisionSpecificExpanded { revs, content: Box::new(content) }, + pos: item.pos, + used: Cell::new(false), + }; + } + ItemVal::RevisionSpecificExpanded { .. } => unreachable!( + "if this visit has never happened before, there should be \ + no expanded nodes." + ), + _ => (), + } + } + + Ok(()) + } +} + +// TODO: parse revisions into `ItemVal`s. + +#[derive(Clone, Debug, PartialEq)] +pub enum ItemVal<'src> { + /* Flags that can only be set to true */ + BuildPass, + BuildFail, + CheckPass, + CheckFail, + RunPass, + RunFail, + NoPreferDynamic, + NoAutoCheckCfg, + + /* Boolean flags that can be set with `foo-bar` or `no-foo-bar` */ + ShouldIce(bool), + ShouldFail(bool), + BuildAuxDocs(bool), + ForceHost(bool), + CheckStdout(bool), + CheckRunResults(bool), + DontCheckCompilerStdout(bool), + DontCheckCompilerStderr(bool), + PrettyExpanded(bool), + PrettyCompareOnly(bool), + CheckTestLineNumbersMatch(bool), + StderrPerBitwidth(bool), + Incremental(bool), + DontCheckFailureStatus(bool), + RunRustfix(bool), + RustfixOnlyMachineApplicable(bool), + CompareOutputLinesBySubset(bool), + KnownBug(bool), + RemapSrcBase(bool), + + /* Key-value directivesthat can be set with `foo-bar: baz qux` */ + ErrorPattern(&'src str), + RegexErrorPattern(&'src str), + CompileFlags(&'src str), + Edition(&'src str), + RunFlags(&'src str), + PrettyMode(&'src str), + AuxBin(&'src str), + AuxBuild(&'src str), + AuxCrate(&'src str), + AuxCodegenBackend(&'src str), + ExecEnv(&'src str), + UnsetExecEnv(&'src str), + RustcEnv(&'src str), + UnsetRustcEnv(&'src str), + ForbidOutput(&'src str), + FailureStatus(&'src str), + AssemblyOutput(&'src str), + TestMirPass(&'src str), + LlvmCovFlags(&'src str), + FilecheckFlags(&'src str), + /// The revisions list + Revisions(&'src str), + + /* Directives that are single keys but have variable suffixes */ + /// Ignore something specific about this test. + /// + /// `//@ ignore-x86` + Ignore { + what: &'src str, + }, + /// Components needed by the test + /// + /// `//@ needs-rust-lld` + Needs { + what: &'src str, + }, + /// Run only if conditions are met + /// + /// `//@ only-linux` + Only { + what: &'src str, + }, + /// Normalizations make transformations in output files before checking + /// + /// `//@ normalize-stdout: foo -> bar` + /// `//@ normalize-stderr-32bit: bar -> baz` + /// `//@ normalize-stderr-test: "h[[:xdigit:]]{16}" -> "h[HASH]"` + Normalize { + what: &'src str, + }, + + /* Specialized patterns */ + /// Directives that apply to a single revision. Don't process this here; just extract the + /// key and value. The content will be reprocessed as a new item. + /// + /// Note that this does not yet recurse. + /// + /// `//@[revname] directive` + RevisionSpecificItems { + revs: &'src str, + content: &'src str, + }, + + /// Once we have parsed the entire file, we re-parse `RevisionSpecificItems` into + /// `RevisionSpecificExpanded`. + RevisionSpecificExpanded { + revs: &'src str, + content: Box, + }, + + /* Non-header things that we consume. */ + UiDirective { + /// If specified, a list of which revisions this should check for + revisions: Option<&'src str>, + /// `^`, `^^`, `|`, `~`, etc + adjust: Option<&'src str>, + /// Directives for filecheck, including `ERROR`, `WARN`, `HELP`, etc + content: &'src str, + }, + /// Directives intended for filecheck. + /// + /// `// CHECK:`, `// CHECK-NEXT`, ... + FileCheckDirective { + directive: &'src str, + content: Option<&'src str>, + }, +} + +impl<'src> ItemVal<'src> { + pub const fn to_item(self, line: usize, col: usize) -> Item<'src> { + Item { val: self, pos: LineCol::new(line, col), used: Cell::new(false) } + } + + /// True if this is an `//@` directive, i.e. something in a header` + pub fn is_header_directive(&self) -> bool { + match self { + ItemVal::BuildPass + | ItemVal::BuildFail + | ItemVal::CheckPass + | ItemVal::CheckFail + | ItemVal::RunPass + | ItemVal::RunFail + | ItemVal::NoPreferDynamic + | ItemVal::NoAutoCheckCfg + | ItemVal::ShouldIce(_) + | ItemVal::ShouldFail(_) + | ItemVal::BuildAuxDocs(_) + | ItemVal::ForceHost(_) + | ItemVal::CheckStdout(_) + | ItemVal::CheckRunResults(_) + | ItemVal::DontCheckCompilerStdout(_) + | ItemVal::DontCheckCompilerStderr(_) + | ItemVal::PrettyExpanded(_) + | ItemVal::PrettyCompareOnly(_) + | ItemVal::CheckTestLineNumbersMatch(_) + | ItemVal::StderrPerBitwidth(_) + | ItemVal::Incremental(_) + | ItemVal::DontCheckFailureStatus(_) + | ItemVal::RunRustfix(_) + | ItemVal::RustfixOnlyMachineApplicable(_) + | ItemVal::CompareOutputLinesBySubset(_) + | ItemVal::KnownBug(_) + | ItemVal::RemapSrcBase(_) + | ItemVal::ErrorPattern(_) + | ItemVal::RegexErrorPattern(_) + | ItemVal::CompileFlags(_) + | ItemVal::Edition(_) + | ItemVal::RunFlags(_) + | ItemVal::PrettyMode(_) + | ItemVal::AuxBin(_) + | ItemVal::AuxBuild(_) + | ItemVal::AuxCrate(_) + | ItemVal::AuxCodegenBackend(_) + | ItemVal::ExecEnv(_) + | ItemVal::UnsetExecEnv(_) + | ItemVal::RustcEnv(_) + | ItemVal::UnsetRustcEnv(_) + | ItemVal::ForbidOutput(_) + | ItemVal::FailureStatus(_) + | ItemVal::AssemblyOutput(_) + | ItemVal::TestMirPass(_) + | ItemVal::LlvmCovFlags(_) + | ItemVal::FilecheckFlags(_) + | ItemVal::Revisions(_) + | ItemVal::Ignore { .. } + | ItemVal::Needs { .. } + | ItemVal::Only { .. } + | ItemVal::Normalize { .. } + | ItemVal::RevisionSpecificItems { .. } + | ItemVal::RevisionSpecificExpanded { .. } => true, + ItemVal::UiDirective { .. } | ItemVal::FileCheckDirective { .. } => false, + } + } +} + +static ITEM_MATCHERS: [MatchItem; 54] = [ + /* set-once values */ + MatchItem::once("build-pass", ItemVal::BuildPass), + MatchItem::once("build-fail", ItemVal::BuildFail), + MatchItem::once("check-pass", ItemVal::CheckPass), + MatchItem::once("check-fail", ItemVal::CheckFail), + MatchItem::once("run-pass", ItemVal::RunPass), + MatchItem::once("run-fail", ItemVal::RunFail), + MatchItem::once("no-prefer-dynamic", ItemVal::NoPreferDynamic), + MatchItem::once("no-auto-check-cfg", ItemVal::NoAutoCheckCfg), + /* boolean flags */ + MatchItem::flag("should-ice", |b| ItemVal::ShouldIce(b)), + MatchItem::flag("should-fail", |b| ItemVal::ShouldFail(b)), + MatchItem::flag("build-aux-docs", |b| ItemVal::BuildAuxDocs(b)), + MatchItem::flag("force-host", |b| ItemVal::ForceHost(b)), + MatchItem::flag("check-stdout", |b| ItemVal::CheckStdout(b)), + MatchItem::flag("check-run-results", |b| ItemVal::CheckRunResults(b)), + MatchItem::flag("dont-check-compiler-stdout", |b| ItemVal::DontCheckCompilerStdout(b)), + MatchItem::flag("dont-check-compiler-stderr", |b| ItemVal::DontCheckCompilerStderr(b)), + MatchItem::flag("pretty-expanded", |b| ItemVal::PrettyExpanded(b)), + MatchItem::flag("pretty-compare-only", |b| ItemVal::PrettyCompareOnly(b)), + MatchItem::flag("check-test-line-numbers-match", |b| ItemVal::CheckTestLineNumbersMatch(b)), + MatchItem::flag("stderr-per-bitwidth", |b| ItemVal::StderrPerBitwidth(b)), + MatchItem::flag("incremental", |b| ItemVal::Incremental(b)), + MatchItem::flag("dont-check-failure-status", |b| ItemVal::DontCheckFailureStatus(b)), + MatchItem::flag("run-rustfix", |b| ItemVal::RunRustfix(b)), + MatchItem::flag("rustfix-only-machine-applicable", |b| { + ItemVal::RustfixOnlyMachineApplicable(b) + }), + MatchItem::flag("compare-output-lines-by-subset", |b| ItemVal::CompareOutputLinesBySubset(b)), + MatchItem::flag("known-bug", |b| ItemVal::KnownBug(b)), + MatchItem::flag("remap-src-base", |b| ItemVal::RemapSrcBase(b)), + /* exact matches */ + MatchItem::map("error-pattern", |s| ItemVal::ErrorPattern(s)), + MatchItem::map("regex-error-pattern", |s| ItemVal::RegexErrorPattern(s)), + MatchItem::map("compile-flags", |s| ItemVal::CompileFlags(s)), + MatchItem::map("run-flags", |s| ItemVal::RunFlags(s)), + MatchItem::map("pretty-mode", |s| ItemVal::PrettyMode(s)), + MatchItem::map("aux-bin", |s| ItemVal::AuxBin(s)), + MatchItem::map("aux-build", |s| ItemVal::AuxBuild(s)), + MatchItem::map("aux-crate", |s| ItemVal::AuxCrate(s)), + MatchItem::map("aux-codegen-backend", |s| ItemVal::AuxCodegenBackend(s)), + MatchItem::map("exec-env", |s| ItemVal::ExecEnv(s)), + MatchItem::map("unset-exec-env", |s| ItemVal::UnsetExecEnv(s)), + MatchItem::map("rustc-env", |s| ItemVal::RustcEnv(s)), + MatchItem::map("unset-rustc-env", |s| ItemVal::UnsetRustcEnv(s)), + MatchItem::map("forbid-output", |s| ItemVal::ForbidOutput(s)), + MatchItem::map("failure-status", |s| ItemVal::FailureStatus(s)), + MatchItem::map("assembly-output", |s| ItemVal::AssemblyOutput(s)), + MatchItem::map("test-mir-pass", |s| ItemVal::TestMirPass(s)), + MatchItem::map("llvm-cov-flags", |s| ItemVal::LlvmCovFlags(s)), + MatchItem::map("filecheck-flags", |s| ItemVal::FilecheckFlags(s)), + MatchItem::map("revisions", |s| ItemVal::Revisions(s)), + /* prefix-based rules */ + MatchItem::map_pfx("ignore", |what| ItemVal::Ignore { what }), + MatchItem::map_pfx("needs", |what| ItemVal::Needs { what }), + MatchItem::map_pfx("only", |what| ItemVal::Only { what }), + MatchItem::map_pfx("normalize", |what| ItemVal::Normalize { what }), + /* regex matchers */ + // Flags for revisions are within a `//@` directive + MatchItem::re_dir( + || Regex::new(r"\[(?P[\w\-,]+)\](?P.*)").unwrap(), + // Catch cases where an invalid revision name is specified + Some(|| Regex::new(r"\[.*\].*").unwrap()), + |c| ItemVal::RevisionSpecificItems { + revs: c.name("revs").unwrap().as_str().trim(), + content: c.name("content").unwrap().as_str().trim(), + }, + "usage: `[rev1,rev-2] directives ...`. Revision names may contain lowercase letters, \ + numbers, and hyphens.", + ), + MatchItem::re_global( + || { + let ok_template = r" + COMMENT # start with a comment + (?:\[ # optional revision spec + (?P \S+ ) # revision content. (not always valid, we check that later) + ])? + ~ # sigil + (?P \| | \^+ )? # adjustments like `^^` or `|` + \x20 # trailing space required for style (hex for verbose mode) + (?P.*) # the rest of the string + "; + cty_re_from_template(ok_template, "COMMENT") + }, + Some(|| { + let err_template = r" + COMMENT + \s* # whitespace after the comment is an easy typo + # Try a couple patterns. Note that these are very general and will also + # capture correct directives; however, since this RE always runs after the + # correct one, the goal is just to flag anything that _looks_ like somebody's + # attempt to write a UI directive. + (?: + ~ # match anything with a sigil + | (?:\[.*\]) # or something that looks like a revision + | [\^\|]+ # or something that looks like an adjustment + ) + + "; + cty_re_from_template(err_template, "COMMENT") + }), + |c| ItemVal::UiDirective { + revisions: c.name("revs").map(|m| m.as_str().trim()), + adjust: c.name("adjust").map(|m| m.as_str()), + content: c.name("content").unwrap().as_str().trim(), + }, + "usage: `//~ LABEL`, `//[revision]~ LABEL`, `//[rev1,rev2]~ LABEL`, ...", + ), + MatchItem::re_global( + || { + let ok_template = r" + COMMENT \s* + (?P CHECK[A-Z-]* ) \s*: + (?P .+ )? + "; + cty_re_from_template(ok_template, "COMMENT") + }, + // Checking for `// .*:` might be too restrictive, + Some(|| cty_re_from_template(r"COMMENT //\s*\S*\s*:", "COMMENT")), + |c| ItemVal::FileCheckDirective { + directive: c.name("directive").unwrap().as_str().trim(), + content: c.name("content").map(|m| m.as_str().trim()), + }, + "usage: `//~ LABEL`, `//[revision]~ LABEL`, `//[rev1,rev2]~ LABEL`, ...", + ), +]; + +/// Turn a single regex string into one for each comment type. Enables verbose mode for comments +/// in the regex. +fn cty_re_from_template(template: &str, placeholder: &str) -> [(CommentTy, Regex); 3] { + CommentTy::all() + .iter() + .map(|cty| { + ( + *cty, + regex::RegexBuilder::new(&template.replace(placeholder, &cty.as_str())) + .ignore_whitespace(true) + .build() + .unwrap(), + ) + }) + .collect::>() + .try_into() + .unwrap() +} + +/// Whether or not to check for errors. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Pass { + /// Only match exact matches. + ExactOnly, + /// Try to locate errors. + FindErrors, +} + +/// Turn syntax into an item +enum MatchItem { + /// Exact directives are those that are only ever an exact key+value. E.g. `foo` should + /// match, but `foo-x86` will never match. `Once` may only be set once, not to T/F. + DirectiveOnce { label: &'static str, v: ItemVal<'static> }, + + /// Exact directives are those that are only ever an exact key+value. E.g. `foo` should + /// match, but `foo-x86` will never match. + DirectiveFlag { label: &'static str, f: fn(bool) -> ItemVal<'static> }, + + /// Exact directives are those that are only ever an exact key+value. E.g. `foo` should + /// match, but `foo-x86` will never match. + DirectiveMap { label: &'static str, f: fn(&str) -> ItemVal }, + + /// Prefix directives are those that always start with a string but have a specifier after. + /// E.g. `` + PrefixDirective { label: &'static str, f: fn(&str) -> ItemVal }, + + /// Any herader directives that need to be matched by regex. This regex should NOT contain + /// `//@`. + ReDirective { + /// Regex to parse. + re: LazyLock, + /// Convert this regex to an item. Do only the bare minimum here! Further parsing should + /// take place when converting from `ItemList` to `TestProps`. + f: fn(regex::Captures) -> ItemVal, + /// Regex of near matches that look generally right, but should raise an error or + /// warning. This is purely for diagnostics. + error_re: Option>, + /// Example usage for help string + help: &'static str, + }, + + /// Any patterns that are not `//@` directives but need to be matched by regex, anywhere in + /// the file. + /// + /// The regexes types for this are a bit unusual; because we need to handle different comment + /// types (`//`, `#`, `;`, etc), we just always construct one pattern for each. + ReGlobal { + /// Regex to parse. + re: LazyLock, + /// Convert this regex to an item. Do only the bare minimum here. + f: fn(regex::Captures) -> ItemVal, + /// Regex of near matches that look generally right, but should raise an error or + /// warning. + error_re: Option>, + /// Example usage for help string + help: &'static str, + }, +} + +impl MatchItem { + /// Construct an exact directive that is a boolean flag + const fn once(label: &'static str, v: ItemVal<'static>) -> Self { + Self::DirectiveOnce { label, v } + } + + /// Construct an exact directive that is a boolean flag + const fn flag(label: &'static str, f: fn(bool) -> ItemVal<'static>) -> Self { + Self::DirectiveFlag { label, f } + } + + /// Construct an exact directive that has + const fn map(label: &'static str, f: fn(&str) -> ItemVal) -> Self { + Self::DirectiveMap { label, f } + } + + /// Construct a prefix directive. + const fn map_pfx(label: &'static str, f: fn(&str) -> ItemVal) -> Self { + Self::PrefixDirective { label, f } + } + + /// Construct a regex directive. + const fn re_dir( + re_fn: fn() -> Regex, + near_fn: Option Regex>, + f: fn(regex::Captures) -> ItemVal, + help: &'static str, + ) -> Self { + let error_re = match near_fn { + Some(n_f) => Some(LazyLock::new(n_f)), + None => None, + }; + + Self::ReDirective { re: LazyLock::new(re_fn), error_re, f, help } + } + + /// Construct a regex pattern that is not a directive. + const fn re_global( + re_fn: fn() -> CommentRePats, + near_fn: Option CommentRePats>, + f: fn(regex::Captures) -> ItemVal, + help: &'static str, + ) -> Self { + let error_re = match near_fn { + Some(n_f) => Some(LazyLock::new(n_f)), + None => None, + }; + + Self::ReGlobal { re: LazyLock::new(re_fn), error_re, f, help } + } + + /// Try to find an item that can be present in a directive. + fn try_match_directive<'src>(&self, mut s: &'src str, pass: Pass) -> ParseResult<'src> { + match self { + MatchItem::DirectiveOnce { label, v } => { + if s == *label { + return Ok(Some(v.clone())); + } + + maybe_err_if_similar(pass, s, label)?; + Ok(None) + } + + MatchItem::DirectiveFlag { label, f } => { + let mut val = true; + + if let Some(neg) = s.strip_prefix("no-") { + val = false; + s = neg; + }; + + // Found exact match; exit one way or another + if let Some(rest) = s.strip_prefix(label) { + return if rest.is_empty() { + Ok(Some(f(val))) + } else { + Err(format!( + "'{label}' is a flag and should not have anything after it; got '{rest}'" + ) + .into()) + }; + }; + + maybe_err_if_similar(pass, s, label)?; + Ok(None) + } + + MatchItem::DirectiveMap { label, f } => { + if let Some((k, v)) = s.split_once(':') { + if k == *label && !v.trim().is_empty() { + return Ok(Some(f(v.trim()))); + } else if k == *label { + Err(format!("'{label}' expects a key-value pair; empty value"))?; + } + }; + + maybe_err_if_similar(pass, s, label)?; + Ok(None) + } + + MatchItem::PrefixDirective { label, f } => { + if let Some(sfx) = s.strip_prefix(label).and_then(|rest| rest.strip_prefix('-')) { + return Ok(Some(f(sfx))); + }; + + maybe_err_if_similar(pass, s, label)?; + Ok(None) + } + + MatchItem::ReDirective { re, f, error_re, help } => { + if let Some(caps) = re.captures(s) { + Ok(Some(f(caps))) + } else if pass == Pass::FindErrors + && error_re.as_ref().is_some_and(|re| re.is_match(s)) + { + Err(format!("invalid directave: {help}").into()) + } else { + Ok(None) + } + } + // Globals cannot match directives + MatchItem::ReGlobal { .. } => Ok(None), + } + } + + fn try_match_global<'src>( + &self, + s: &'src str, + cty: CommentTy, + pass: Pass, + ) -> ParseResult<'src> { + match self { + // No directives can match globals + MatchItem::DirectiveOnce { .. } + | MatchItem::DirectiveFlag { .. } + | MatchItem::DirectiveMap { .. } + | MatchItem::PrefixDirective { .. } + | MatchItem::ReDirective { .. } => Ok(None), + MatchItem::ReGlobal { re, f, error_re, help } => { + // Find the right regex patterns for our comment types + let re = re.iter().find_map(|(c, r)| (*c == cty).then_some(r)).unwrap(); + let error_re = error_re.as_ref().map(|re_arr| { + re_arr.iter().find_map(|(c, r)| (*c == cty).then_some(r)).unwrap() + }); + + if let Some(caps) = re.captures(s) { + Ok(Some(f(caps))) + } else if pass == Pass::FindErrors + && error_re.as_ref().is_some_and(|re| re.is_match(s)) + { + Err(format!("invalid directave: {help}").into()) + } else { + Ok(None) + } + } + } + } +} + +/// Return vals: +/// +/// Err(e): something looked wrong +/// Ok(None): nothing found, continue parsing +/// Ok(Some(v)): found a match to handle +type ParseResult<'src> = Result>>; + +fn match_any<'src>( + f: impl Fn(&MatchItem, Pass) -> ParseResult<'src>, + default: impl FnOnce() -> ParseResult<'src>, +) -> ParseResult<'src> { + // Try to find an exact matching pattern. + for matcher in &ITEM_MATCHERS { + let res = f(matcher, Pass::ExactOnly); + match res { + Ok(Some(_)) | Err(_) => return res, + Ok(None) => (), + } + } + + // Since there were no exact matches, see if anything is a close enough match to + // cause an error. + for matcher in &ITEM_MATCHERS { + let res = f(matcher, Pass::FindErrors); + match res { + Ok(Some(ref v)) => panic!("should never return matches on error pass; got {v:?}"), + Ok(None) => (), + Err(_) => return res, + }; + } + + default() +} + +fn try_match_line(mut line: &str, cty: CommentTy) -> ParseResult { + let mut col = 1; + let end_trimmed = line.trim_end(); + line = end_trimmed.trim_start(); + + // This line is a directive; try to match it + if let Some(mut directive_line) = line.strip_prefix(cty.directive()) { + directive_line = directive_line.trim(); + let make_err = || { + Err(format!( + "unmatched directive syntax; `{}` must be followed by a directive", + cty.directive() + ) + .into()) + }; + + match_any(|matcher, pass| matcher.try_match_directive(directive_line, pass), make_err) + } else { + match_any(|matcher, pass| matcher.try_match_global(line, cty, pass), || Ok(None)) + } +} + +/// If the line looks like it should be this directive (starts with the label or is similar), +/// construct an error. +fn maybe_err_if_similar(pass: Pass, line: &str, label: &str) -> Result<()> { + if pass == Pass::ExactOnly { + return Ok(()); + } + + let make_err = || Err(format!("did you mean: '{label}'?").into()); + + if line.starts_with(label) { + return make_err(); + } + + let check_str = match line.split_once(':') { + Some((key, _value)) => key, + None => { + let Some(s) = line.split_whitespace().next() else { + // Empty or only ws string + return Ok(()); + }; + s + } + }; + + if strsim::jaro_winkler(check_str, label) > STRSIM_CONFIDENCE { make_err() } else { Ok(()) } +} + +#[cfg(test)] +#[path = "test_itemlist.rs"] +mod tests; diff --git a/src/tools/compiletest/src/load_cfg/prepare.rs b/src/tools/compiletest/src/load_cfg/prepare.rs new file mode 100644 index 0000000000000..2e041604d3b18 --- /dev/null +++ b/src/tools/compiletest/src/load_cfg/prepare.rs @@ -0,0 +1,494 @@ +//! Turn a list of `Items` + +use std::{collections::BTreeMap, iter, path::PathBuf, rc::Rc, str::FromStr, sync::LazyLock}; + +use regex::Regex; + +use crate::common::{FailMode, PassMode}; + +use super::{ + itemlist::{Item, ItemVal}, + CommentTy, Error, ErrorExt, LineCol, Result, +}; + +const REV_NAME_PAT: &str = r"^[\w-,]+$"; +static REV_NAME_RE: LazyLock = LazyLock::new(|| Regex::new(REV_NAME_PAT).unwrap()); + +type BoxStr = Box; +type RcStr = Rc; + +pub fn entrypoint(gcfg: GlobalConfig, items: &[Item]) -> Result { + let mut pcx = PassCtx::default(); + + for pass in ALL_PASSES { + pass(items, &mut pcx)?; + } + + Ok(pcx.into_test_props()) +} + +/// Used for `ignore-*` type directives +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum Platform { + // TODO we can do better here + X86, + Aarch64, +} + +pub struct TestProps { + /// Configuration that is always the same for all tests + pub global_cfg: Config, + /// Configuration that only applies to a specific revision + pub revisions: BTreeMap, +} + +/// Configuration for tests that is not configured by the test file. +#[derive(Debug)] +pub struct GlobalConfig { + /// Directory (if any) to use for incremental compilation. This is + /// not set by end-users; rather it is set by the incremental + /// testing harness and used when generating compilation + /// arguments. (In particular, it propagates to the aux-builds.) + pub incremental_dir: Option, +} + +/// Config that can be used for one platform or one +#[derive(Clone, Debug, Default)] +pub struct Config { + /// Lines that should be expected, in order, on standard out + pub error_patterns: Vec, + /// Regexes that should be expected, in order, on standard out + pub regex_error_patterns: Vec, + /// Extra flags to pass to the compiler + pub compile_flags: Vec, + /// Extra flags to pass when the compiled code is run (such as --bench) + pub run_flags: Vec, + + pub ui_directives: Vec, +} + +// impl Default for Config { +// fn default() -> Self { +// Config {} +// } +// } + +/// Context shared among all passes. +#[derive(Debug, Default)] +struct PassCtx { + // We use `Option` here so we can't accidentally misinterpret an empty vector as a + // completed but empty pass. + config: Option, + revisions: Option>, + rev_cfg: Option>, + ui_directives: Option>, + check_prefixes: Option>, +} + +impl PassCtx { + fn into_test_props(self) -> TestProps { + todo!() + } +} + +const ALL_PASSES: &[fn(&[Item], &mut PassCtx) -> Result<()>] = &[ + pass_initialize, + pass_check_ordering, + pass_validate_repetition, + pass_extract_revisions, + pass_build_default_config, + pass_ui_directives, + pass_build_revision_config, + pass_filecheck_directives, + pass_finalize, +]; + +/// Check that we haven't used this before and messed up our `used` tracking. +fn pass_initialize(items: &[Item], pcx: &mut PassCtx) -> Result<()> { + for item in items { + assert!(!item.used.get(), "found already used item {item:?}"); + } + Ok(()) +} + +/// Verify that header directives come before others (UI directives, filecheck, etc). +fn pass_check_ordering(items: &[Item], pcx: &mut PassCtx) -> Result<()> { + let mut last_header = None; + let mut first_nonheader = None; + + for item in items { + if item.val.is_header_directive() { + last_header = Some(item) + } else if first_nonheader.is_none() { + first_nonheader = Some(item) + } + } + + match (last_header, first_nonheader) { + (Some(lh), Some(fnh)) if fnh.pos.line <= lh.pos.line => Err(format!( + "Header directives should be at the top of the file, before any other \ + directives. Last header {lh:?}, first nonheader {fnh:?}" + ) + .into()), + (Some(_), Some(_)) | (Some(_), None) | (None, Some(_)) | (None, None) => Ok(()), + } +} + +/// Some keys may only be specified once, or once within a group. Check this early. +fn pass_validate_repetition(items: &[Item], _pcx: &mut PassCtx) -> Result<()> { + // TODO + Ok(()) +} + +/// Find where revisions are listed and extract that. This is its own pass just so we have this +/// information early. +fn pass_extract_revisions(items: &[Item], pcx: &mut PassCtx) -> Result<()> { + let mut iter = items.iter().filter_map(|v| matches!(v.val, ItemVal::Revisions(_)).then_some(v)); + let Some(first) = iter.next() else { + // Revisions not specified + pcx.revisions = Some(Vec::new()); + return Ok(()); + }; + + assert!(iter.count() == 0, "duplicates should have been checked already"); + first.used.set(true); + + let ItemVal::Revisions(all_revs) = first.val else { + unreachable!("filtered above"); + }; + + let revs: Vec<_> = all_revs.split_whitespace().map(Rc::from).collect(); + + for idx in 0..revs.len() { + let name = &revs[idx]; + if !REV_NAME_RE.is_match(&name) { + Err(format!("revision '{name}' is not valid. Expected: `{REV_NAME_PAT}`"))?; + } + + if revs[..idx].contains(&name) { + Err(format!("revision '{name}' is listed twice"))?; + } + } + + pcx.revisions = Some(revs); + Ok(()) +} + +/// Construct the config that is used everywhere by default. +fn pass_build_default_config(items: &[Item], pcx: &mut PassCtx) -> Result<()> { + let mut cfg = Config::default(); + + for item in items { + visit_default_config(&item.val, &mut cfg).pos(item.pos)?; + } + + pcx.config = Some(cfg); + Ok(()) +} + +/// Locate config that can apply to revisions. +fn pass_build_revision_config(items: &[Item], pcx: &mut PassCtx) -> Result<()> { + let default_cfg = pcx.config.as_ref().unwrap(); + let all_revs = pcx.revisions.as_ref().unwrap(); + + let mut map: BTreeMap, Config> = + all_revs.iter().map(|r| (Rc::clone(r), default_cfg.clone())).collect(); + + let mut iter = items + .iter() + .filter_map(|v| matches!(v.val, ItemVal::RevisionSpecificExpanded { .. }).then_some(v)); + + for item in iter { + let ItemVal::RevisionSpecificExpanded { revs, ref content } = item.val else { + unreachable!("filtered above"); + }; + + for rev in split_validate_revisions(revs, &all_revs) { + let rev = rev.pos(item.pos)?; + let mut cfg = map.get_mut(rev).unwrap(); + visit_revision_config(content, cfg).pos(item.pos)?; + } + + item.used.set(true); + } + + pcx.rev_cfg = Some(map); + Ok(()) +} + +#[derive(Clone, Copy, Debug)] +enum UiLevel { + Error, + Warn, + Help, + Note, +} + +impl UiLevel { + const ALL: &'static [&'static str] = + &["ERROR", "WARN", "WARNING", "SUGGESTION", "HELP", "NOTE"]; + + /// Extract self + fn prep_directive(s: &str) -> Result<(Self, RcStr)> { + let (dir, rest) = s.split_once(|ch: char| ch.is_whitespace()).unwrap_or((s, "")); + dir.parse().map(|v| (v, rest.trim().into())) + } +} + +impl FromStr for UiLevel { + type Err = Error; + + fn from_str(s: &str) -> Result { + let ret = match s { + "ERROR" => Self::Error, + "WARN" | "WARNING" => Self::Warn, + "SUGGESTION" | "HELP" => Self::Help, + "NOTE" => Self::Note, + _ => Err(format!("unknown revision '{s}. Must be one of '{:?}'", UiLevel::ALL))?, + }; + Ok(ret) + } +} + +#[derive(Clone, Debug)] +pub struct UiDirective { + line: usize, + requires_preceding: bool, + level: UiLevel, + content: RcStr, +} + +impl UiDirective { + fn new_to_cfg( + items: &[Item], + base_line: usize, + level: UiLevel, + content: &RcStr, + adjust: Option<&str>, + cfg: &mut Config, + ) -> Result<()> { + let (offset, requires_preceding) = if let Some(adj) = adjust { + if adj == "|" { + (1, true) + } else if adj.bytes().all(|b| b == b'^') { + (adj.bytes().len(), false) + } else { + Err(format!("invalid adjuster `{adj}`"))? + } + } else { + (0, false) + }; + + let line = match base_line.checked_sub(offset) { + Some(0) | None => Err(format!("an offset of {offset} points outside of the file"))?, + Some(v) => v, + }; + + let val = Self { line, level, requires_preceding, content: Rc::clone(content) }; + cfg.ui_directives.push(val); + + Ok(()) + } +} + +/// Extract UI error directives, apply them to revisions, and validate their position adjusters. +fn pass_ui_directives(items: &[Item], pcx: &mut PassCtx) -> Result<()> { + let all_revs = pcx.revisions.as_ref().unwrap(); + let mut iter = items + .iter() + .enumerate() + .filter_map(|(idx, v)| matches!(v.val, ItemVal::UiDirective { .. }).then_some((idx, v))); + + for (idx, item) in iter { + let ItemVal::UiDirective { revisions, adjust, content } = item.val else { + unreachable!("filtered above"); + }; + let (level, content) = UiLevel::prep_directive(content)?; + + if let Some(revs_str) = revisions { + for rev in split_validate_revisions(revs_str, &all_revs) { + let rev = rev.pos(item.pos)?; + let cfg = pcx.rev_cfg.as_mut().unwrap().get_mut(rev).unwrap(); + + UiDirective::new_to_cfg(items, item.pos.line, level, &content, adjust, cfg); + } + } else { + // If the revision is unspecified, applly to all revisions including default + for cfg in pcx + .rev_cfg + .as_mut() + .unwrap() + .values_mut() + .chain(iter::once(pcx.config.as_mut().unwrap())) + { + UiDirective::new_to_cfg(items, item.pos.line, level, &content, adjust, cfg); + } + } + + item.used.set(true); + } + + Ok(()) +} + +/// Extract filecheck directives and validate they match revisions. +fn pass_filecheck_directives(items: &[Item], pcx: &mut PassCtx) -> Result<()> { + Ok(()) +} + +/// Just check that no items that haven't been consumed in some way. +fn pass_finalize(items: &[Item], vctx: &mut PassCtx) -> Result<()> { + for item in items { + assert!(item.used.get(), "found unused item {item:?}"); + item.used.set(false); + } + Ok(()) +} + +/// Handle a single item and update `cfg` to match. +fn visit_default_config(item: &ItemVal, cfg: &mut Config) -> Result<()> { + match item { + ItemVal::BuildPass => todo!(), + ItemVal::BuildFail => todo!(), + ItemVal::CheckPass => todo!(), + ItemVal::CheckFail => todo!(), + ItemVal::RunPass => todo!(), + ItemVal::RunFail => todo!(), + ItemVal::NoPreferDynamic => todo!(), + ItemVal::NoAutoCheckCfg => todo!(), + ItemVal::ShouldIce(_) => todo!(), + ItemVal::ShouldFail(_) => todo!(), + ItemVal::BuildAuxDocs(_) => todo!(), + ItemVal::ForceHost(_) => todo!(), + ItemVal::CheckStdout(_) => todo!(), + ItemVal::CheckRunResults(_) => todo!(), + ItemVal::DontCheckCompilerStdout(_) => todo!(), + ItemVal::DontCheckCompilerStderr(_) => todo!(), + ItemVal::PrettyExpanded(_) => todo!(), + ItemVal::PrettyCompareOnly(_) => todo!(), + ItemVal::CheckTestLineNumbersMatch(_) => todo!(), + ItemVal::StderrPerBitwidth(_) => todo!(), + ItemVal::Incremental(_) => todo!(), + ItemVal::DontCheckFailureStatus(_) => todo!(), + ItemVal::RunRustfix(_) => todo!(), + ItemVal::RustfixOnlyMachineApplicable(_) => todo!(), + ItemVal::CompareOutputLinesBySubset(_) => todo!(), + ItemVal::KnownBug(_) => todo!(), + ItemVal::RemapSrcBase(_) => todo!(), + ItemVal::ErrorPattern(_) => todo!(), + ItemVal::RegexErrorPattern(_) => todo!(), + ItemVal::CompileFlags(_) => todo!(), + ItemVal::Edition(_) => todo!(), + ItemVal::RunFlags(_) => todo!(), + ItemVal::PrettyMode(_) => todo!(), + ItemVal::AuxBin(_) => todo!(), + ItemVal::AuxBuild(_) => todo!(), + ItemVal::AuxCrate(_) => todo!(), + ItemVal::AuxCodegenBackend(_) => todo!(), + ItemVal::ExecEnv(_) => todo!(), + ItemVal::UnsetExecEnv(_) => todo!(), + ItemVal::RustcEnv(_) => todo!(), + ItemVal::UnsetRustcEnv(_) => todo!(), + ItemVal::ForbidOutput(_) => todo!(), + ItemVal::FailureStatus(_) => todo!(), + ItemVal::AssemblyOutput(_) => todo!(), + ItemVal::TestMirPass(_) => todo!(), + ItemVal::LlvmCovFlags(_) => todo!(), + ItemVal::FilecheckFlags(_) => todo!(), + ItemVal::Revisions(_) => todo!(), + ItemVal::Ignore { what } => todo!(), + ItemVal::Needs { what } => todo!(), + ItemVal::Only { what } => todo!(), + ItemVal::Normalize { what } => todo!(), + ItemVal::RevisionSpecificItems { .. } => unreachable!("should have been expanded"), + ItemVal::RevisionSpecificExpanded { revs, content } => todo!(), + ItemVal::UiDirective { revisions, adjust, content } => todo!(), + ItemVal::FileCheckDirective { .. } => todo!(), + } + + Ok(()) +} + +/// Handle a single item that applies only to a revision. +fn visit_revision_config(val: &ItemVal, cfg: &mut Config) -> Result<()> { + match &val { + // Some directives are not allowed per-revision + ItemVal::BuildPass + | ItemVal::BuildFail + | ItemVal::CheckPass + | ItemVal::CheckFail + | ItemVal::RunPass + | ItemVal::RunFail + | ItemVal::NoPreferDynamic + | ItemVal::NoAutoCheckCfg => Err("TODO")?, + // Most directives can forward to the default config + ItemVal::ShouldIce(_) + | ItemVal::ShouldFail(_) + | ItemVal::BuildAuxDocs(_) + | ItemVal::ForceHost(_) + | ItemVal::CheckStdout(_) + | ItemVal::CheckRunResults(_) + | ItemVal::DontCheckCompilerStdout(_) + | ItemVal::DontCheckCompilerStderr(_) + | ItemVal::PrettyExpanded(_) + | ItemVal::PrettyCompareOnly(_) + | ItemVal::CheckTestLineNumbersMatch(_) + | ItemVal::StderrPerBitwidth(_) + | ItemVal::Incremental(_) + | ItemVal::DontCheckFailureStatus(_) + | ItemVal::RunRustfix(_) + | ItemVal::RustfixOnlyMachineApplicable(_) + | ItemVal::CompareOutputLinesBySubset(_) + | ItemVal::KnownBug(_) + | ItemVal::RemapSrcBase(_) + | ItemVal::ErrorPattern(_) + | ItemVal::RegexErrorPattern(_) + | ItemVal::CompileFlags(_) + | ItemVal::Edition(_) + | ItemVal::RunFlags(_) + | ItemVal::PrettyMode(_) + | ItemVal::AuxBin(_) + | ItemVal::AuxBuild(_) + | ItemVal::AuxCrate(_) + | ItemVal::AuxCodegenBackend(_) + | ItemVal::ExecEnv(_) + | ItemVal::UnsetExecEnv(_) + | ItemVal::RustcEnv(_) + | ItemVal::UnsetRustcEnv(_) + | ItemVal::ForbidOutput(_) + | ItemVal::FailureStatus(_) + | ItemVal::AssemblyOutput(_) + | ItemVal::TestMirPass(_) + | ItemVal::LlvmCovFlags(_) + | ItemVal::FilecheckFlags(_) + | ItemVal::Revisions(_) + | ItemVal::Ignore { .. } + | ItemVal::Needs { .. } + | ItemVal::Only { .. } + | ItemVal::Normalize { .. } => visit_default_config(val, cfg)?, + ItemVal::RevisionSpecificExpanded { revs, content } => Err("don't to that TODO")?, + ItemVal::RevisionSpecificItems { .. } => unreachable!("should have been expanded"), + ItemVal::UiDirective { .. } | ItemVal::FileCheckDirective { .. } => { + unreachable!("global directives shouldn't happen here") + } + } + + Ok(()) +} + +/// Split revisions by comma and ensure they exist in a list +fn split_validate_revisions<'a>( + revs_str: &'a str, + all_revs: &'a [RcStr], +) -> impl Iterator> + 'a { + revs_str.split(',').map(str::trim).map(move |s| { + all_revs + .iter() + .find(|r| ***r == *s) + .ok_or_else(|| format!("revision 's' was not found. Available: {all_revs:?}").into()) + }) +} + +#[cfg(test)] +#[path = "test_prepare.rs"] +mod tests; diff --git a/src/tools/compiletest/src/load_cfg/test_itemlist.rs b/src/tools/compiletest/src/load_cfg/test_itemlist.rs new file mode 100644 index 0000000000000..b2b9fbb958be4 --- /dev/null +++ b/src/tools/compiletest/src/load_cfg/test_itemlist.rs @@ -0,0 +1,307 @@ +use super::*; + +/// `(directive, expected)` +const DIRECTIVE_CHECK: &[(&str, ItemVal<'static>)] = &[ + // set-once values + ("build-pass", ItemVal::BuildPass), + ("build-fail", ItemVal::BuildFail), + ("check-pass", ItemVal::CheckPass), + ("check-fail", ItemVal::CheckFail), + ("run-pass", ItemVal::RunPass), + ("run-fail", ItemVal::RunFail), + ("no-prefer-dynamic", ItemVal::NoPreferDynamic), + ("no-auto-check-cfg", ItemVal::NoAutoCheckCfg), + // Boolean flags + ("should-ice", ItemVal::ShouldIce(true)), + ("no-should-ice", ItemVal::ShouldIce(false)), + ("should-fail", ItemVal::ShouldFail(true)), + ("no-should-fail", ItemVal::ShouldFail(false)), + ("build-aux-docs", ItemVal::BuildAuxDocs(true)), + ("no-build-aux-docs", ItemVal::BuildAuxDocs(false)), + ("force-host", ItemVal::ForceHost(true)), + ("no-force-host", ItemVal::ForceHost(false)), + ("check-stdout", ItemVal::CheckStdout(true)), + ("no-check-stdout", ItemVal::CheckStdout(false)), + ("check-run-results", ItemVal::CheckRunResults(true)), + ("no-check-run-results", ItemVal::CheckRunResults(false)), + ("dont-check-compiler-stdout", ItemVal::DontCheckCompilerStdout(true)), + ("no-dont-check-compiler-stdout", ItemVal::DontCheckCompilerStdout(false)), + ("dont-check-compiler-stderr", ItemVal::DontCheckCompilerStderr(true)), + ("no-dont-check-compiler-stderr", ItemVal::DontCheckCompilerStderr(false)), + ("pretty-expanded", ItemVal::PrettyExpanded(true)), + ("no-pretty-expanded", ItemVal::PrettyExpanded(false)), + ("pretty-compare-only", ItemVal::PrettyCompareOnly(true)), + ("no-pretty-compare-only", ItemVal::PrettyCompareOnly(false)), + ("check-test-line-numbers-match", ItemVal::CheckTestLineNumbersMatch(true)), + ("no-check-test-line-numbers-match", ItemVal::CheckTestLineNumbersMatch(false)), + ("stderr-per-bitwidth", ItemVal::StderrPerBitwidth(true)), + ("no-stderr-per-bitwidth", ItemVal::StderrPerBitwidth(false)), + ("incremental", ItemVal::Incremental(true)), + ("no-incremental", ItemVal::Incremental(false)), + ("dont-check-failure-status", ItemVal::DontCheckFailureStatus(true)), + ("no-dont-check-failure-status", ItemVal::DontCheckFailureStatus(false)), + ("run-rustfix", ItemVal::RunRustfix(true)), + ("no-run-rustfix", ItemVal::RunRustfix(false)), + ("rustfix-only-machine-applicable", { ItemVal::RustfixOnlyMachineApplicable(true) }), + ("no-rustfix-only-machine-applicable", { ItemVal::RustfixOnlyMachineApplicable(false) }), + ("compare-output-lines-by-subset", ItemVal::CompareOutputLinesBySubset(true)), + ("no-compare-output-lines-by-subset", ItemVal::CompareOutputLinesBySubset(false)), + ("known-bug", ItemVal::KnownBug(true)), + ("no-known-bug", ItemVal::KnownBug(false)), + ("remap-src-base", ItemVal::RemapSrcBase(true)), + ("no-remap-src-base", ItemVal::RemapSrcBase(false)), + // Mappings + ("error-pattern: testval", ItemVal::ErrorPattern("testval")), + ("regex-error-pattern: testval", ItemVal::RegexErrorPattern("testval")), + ("compile-flags: testval", ItemVal::CompileFlags("testval")), + ("run-flags: testval", ItemVal::RunFlags("testval")), + ("pretty-mode: testval", ItemVal::PrettyMode("testval")), + ("aux-bin: testval", ItemVal::AuxBin("testval")), + ("aux-build: testval", ItemVal::AuxBuild("testval")), + ("aux-crate: testval", ItemVal::AuxCrate("testval")), + ("aux-codegen-backend: testval", ItemVal::AuxCodegenBackend("testval")), + ("exec-env: testval", ItemVal::ExecEnv("testval")), + ("unset-exec-env: testval", ItemVal::UnsetExecEnv("testval")), + ("rustc-env: testval", ItemVal::RustcEnv("testval")), + ("unset-rustc-env: testval", ItemVal::UnsetRustcEnv("testval")), + ("forbid-output: testval", ItemVal::ForbidOutput("testval")), + ("failure-status: testval", ItemVal::FailureStatus("testval")), + ("assembly-output: testval", ItemVal::AssemblyOutput("testval")), + ("test-mir-pass: testval", ItemVal::TestMirPass("testval")), + ("llvm-cov-flags: testval", ItemVal::LlvmCovFlags("testval")), + ("filecheck-flags: testval", ItemVal::FilecheckFlags("testval")), + ("revisions: testval", ItemVal::Revisions("testval")), + // prefix-based rules + ("ignore-foo", ItemVal::Ignore { what: "foo" }), + ("needs-foo", ItemVal::Needs { what: "foo" }), + ("only-foo", ItemVal::Only { what: "foo" }), + ("normalize-foo", ItemVal::Normalize { what: "foo" }), + // regex-based rules + ( + "[rev1,rev-2] dir1 dir2", + ItemVal::RevisionSpecificItems { revs: "rev1,rev-2", content: "dir1 dir2" }, + ), + ( + "[1uNCOMmon_p4ttern-should_stillParse] anyth*ng & [e^erything!]", + ItemVal::RevisionSpecificItems { + revs: "1uNCOMmon_p4ttern-should_stillParse", + content: "anyth*ng & [e^erything!]", + }, + ), +]; + +const DIRECTIVE_ERRORS: &[&str] = &[""]; + +const GLOBAL_CHECK: &[(&str, ItemVal<'static>)] = &[ + ("//~ same line", ItemVal::UiDirective { revisions: None, adjust: None, content: "same line" }), + ( + "unimporatant text //~ middle of line", + ItemVal::UiDirective { revisions: None, adjust: None, content: "middle of line" }, + ), + ( + "text//~ middle of line", + ItemVal::UiDirective { revisions: None, adjust: None, content: "middle of line" }, + ), + ( + "//~^ line up", + ItemVal::UiDirective { revisions: None, adjust: Some("^"), content: "line up" }, + ), + ( + "//~| adj 1 up", + ItemVal::UiDirective { revisions: None, adjust: Some("|"), content: "adj 1 up" }, + ), + ( + "//~^^^^ adj many up", + ItemVal::UiDirective { revisions: None, adjust: Some("^^^^"), content: "adj many up" }, + ), + ( + "//[abc,def-ghi]~ revisions same line", + ItemVal::UiDirective { + revisions: Some("abc,def-ghi"), + adjust: None, + content: "revisions same line", + }, + ), + ( + "//[abc]~^ revisions line up", + ItemVal::UiDirective { + revisions: Some("abc"), + adjust: Some("^"), + content: "revisions line up", + }, + ), + ( + "//[abc]~| revisions adj 1 up", + ItemVal::UiDirective { + revisions: Some("abc"), + adjust: Some("|"), + content: "revisions adj 1 up", + }, + ), + ( + "//[abc]~^^^^ revisions many up", + ItemVal::UiDirective { + revisions: Some("abc"), + adjust: Some("^^^^"), + content: "revisions many up", + }, + ), +]; + +/// Things that should look like errors in global scope +const GLOBAL_ERRORS: &[&str] = &[ + // Regex patterns that are the wrong order. `(rev, sigil, adj)` is correct. + "//~", // standalone sigil + "//[abc]", // standalone rev + "//^^", // standalone adjustment + "//[abc]^~", // (rev, adj, sigil) + "//~[abc,def]|", // (sigil, rev, adj) + "//~^^[abc def]", // (sigil, adj, rev) + "//^^ [abc def]~", // (adj, rev, sigil) + "//|~[abc def]", // (adj, sigil, rev) + "// [abc] ^ ~", // some things with whitespace +]; + +/// Simple unit test that directives get caught +#[test] +fn test_dir_match() { + // (text, expected, actual) + let mut errors = Vec::new(); + + for (dir, expected) in DIRECTIVE_CHECK { + let mut found = None; + for matcher in &ITEM_MATCHERS { + if let Some(v) = matcher.try_match_directive(dir, Pass::ExactOnly).expect("no errors") { + found = Some(v); + } + } + + match found { + Some(actual) if &actual == expected => (), + Some(actual) => errors.push((dir, expected, Some(actual))), + None => errors.push((dir, expected, None)), + } + } + + assert!(errors.is_empty(), "unmatched: {errors:#?}"); +} + +/// Add the comment and make sure we still match +#[test] +fn test_directives_as_global() { + // (text, expected, actual) + let mut errors = Vec::new(); + + for (dir, expected) in DIRECTIVE_CHECK { + for cty in CommentTy::all() { + let line = format!("{} {dir}", cty.directive()); + let found = try_match_line(&line, *cty).expect("no errors"); + + match found { + Some(actual) if &actual == expected => (), + Some(actual) => errors.push((line.clone(), expected, format!("{actual:?}"))), + None => errors.push((line.clone(), expected, "None".to_owned())), + } + } + } + + assert!(errors.is_empty(), "unmatched: {errors:#?}"); +} + +/// Try directives that are supposed to be global +#[test] +fn test_global() { + // (text, expected, actual) + let mut errors = Vec::new(); + + for (line, expected) in GLOBAL_CHECK { + for cty in CommentTy::all() { + let line = line.replace("//", &cty.as_str()); + let found = try_match_line(&line, *cty).expect("no errors"); + + match found { + Some(actual) if &actual == expected => (), + Some(actual) => errors.push((line.clone(), expected, format!("{actual:?}"))), + None => errors.push((line.clone(), expected, "None".to_owned())), + } + } + } + + assert!(errors.is_empty(), "unmatched: {errors:#?}"); +} + +/// Try directives that are supposed to be global +#[test] +fn test_global_errors() { + // (text, expected, actual) + let mut unexpected_ok = Vec::new(); + + for line in GLOBAL_ERRORS { + for cty in CommentTy::all() { + let line = line.replace("//", &cty.as_str()); + let res = try_match_line(&line, *cty); + + if res.is_ok() { + // We should only see `Err` values. + unexpected_ok.push((line.clone(), format!("{res:?}"))); + } + } + } + + assert!(unexpected_ok.is_empty(), "should parse as errors but didn't: {unexpected_ok:#?}"); +} + +const SAMPLE_FILE: &str = r#" +//@ ignore-armv7 +//@ compile-flags: -O -C no-prepopulate-passes +//@ revisions: x86 other +//@[x86] should-fail +//@[other] compile-flags: -O + +// CHECK: define abcd +fn foo() {} + +#![feature(...)] +//~^ WARN be careful +//~| WARN seriously! + +fn fun() +//[rev-1]~^ ERROR no fun allowed +{} +"#; + +const SAMPLE_EXPECTED: LazyCell> = LazyCell::new(|| { + vec![ + ItemVal::Ignore { what: "armv7" }.to_item(2, 1), + ItemVal::CompileFlags("-O -C no-prepopulate-passes").to_item(3, 1), + ItemVal::Revisions("x86 other").to_item(4, 1), + ItemVal::RevisionSpecificExpanded { + revs: "x86", + content: Box::new(ItemVal::ShouldFail(true)), + } + .to_item(5, 1), + ItemVal::RevisionSpecificExpanded { + revs: "other", + content: Box::new(ItemVal::CompileFlags("-O")), + } + .to_item(6, 1), + ItemVal::FileCheckDirective { directive: "CHECK", content: Some("define abcd") } + .to_item(8, 1), + ItemVal::UiDirective { revisions: None, adjust: Some("^"), content: "WARN be careful" } + .to_item(12, 1), + ItemVal::UiDirective { revisions: None, adjust: Some("|"), content: "WARN seriously!" } + .to_item(13, 1), + ItemVal::UiDirective { + revisions: Some("rev-1"), + adjust: Some("^"), + content: "ERROR no fun allowed", + } + .to_item(16, 1), + ] +}); + +#[test] +fn test_whole_list() { + let parsed = parse(SAMPLE_FILE, CommentTy::Slashes).unwrap(); + assert_eq!(parsed, *SAMPLE_EXPECTED, "{parsed:#?}\n{:#?}", *SAMPLE_EXPECTED); +} diff --git a/src/tools/compiletest/src/load_cfg/test_prepare.rs b/src/tools/compiletest/src/load_cfg/test_prepare.rs new file mode 100644 index 0000000000000..8b137891791fe --- /dev/null +++ b/src/tools/compiletest/src/load_cfg/test_prepare.rs @@ -0,0 +1 @@ +