diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 18:31:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 18:31:44 +0000 |
commit | c23a457e72abe608715ac76f076f47dc42af07a5 (patch) | |
tree | 2772049aaf84b5c9d0ed12ec8d86812f7a7904b6 /vendor/ui_test-0.20.0/src | |
parent | Releasing progress-linux version 1.73.0+dfsg1-1~progress7.99u1. (diff) | |
download | rustc-c23a457e72abe608715ac76f076f47dc42af07a5.tar.xz rustc-c23a457e72abe608715ac76f076f47dc42af07a5.zip |
Merging upstream version 1.74.1+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/ui_test-0.20.0/src')
-rw-r--r-- | vendor/ui_test-0.20.0/src/cmd.rs | 120 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/config.rs | 272 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/config/args.rs | 90 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/dependencies.rs | 320 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/diff.rs | 164 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/error.rs | 81 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/github_actions.rs | 96 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/lib.rs | 1270 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/mode.rs | 93 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/parser.rs | 822 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/parser/spanned.rs | 264 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/parser/tests.rs | 139 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/rustc_stderr.rs | 201 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/status_emitter.rs | 972 | ||||
-rw-r--r-- | vendor/ui_test-0.20.0/src/tests.rs | 350 |
15 files changed, 5254 insertions, 0 deletions
diff --git a/vendor/ui_test-0.20.0/src/cmd.rs b/vendor/ui_test-0.20.0/src/cmd.rs new file mode 100644 index 000000000..53fa3e0f4 --- /dev/null +++ b/vendor/ui_test-0.20.0/src/cmd.rs @@ -0,0 +1,120 @@ +use std::{ + ffi::OsString, + path::{Path, PathBuf}, + process::Command, +}; + +#[derive(Debug, Clone)] +/// A command, its args and its environment. Used for +/// the main command, the dependency builder and the cfg-reader. +pub struct CommandBuilder { + /// Path to the binary. + pub program: PathBuf, + /// Arguments to the binary. + pub args: Vec<OsString>, + /// A flag to prefix before the path to where output files should be written. + pub out_dir_flag: Option<OsString>, + /// A flag to set as the last flag in the command, so the `build` caller can + /// append the filename themselves. + pub input_file_flag: Option<OsString>, + /// Environment variables passed to the binary that is executed. + /// The environment variable is removed if the second tuple field is `None` + pub envs: Vec<(OsString, Option<OsString>)>, +} + +impl CommandBuilder { + /// Uses the `CARGO` env var or just a program named `cargo` and the argument `build`. + pub fn cargo() -> Self { + Self { + program: PathBuf::from(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())), + args: vec!["build".into()], + out_dir_flag: Some("--target-dir".into()), + input_file_flag: Some("--manifest-path".into()), + envs: vec![], + } + } + + /// Uses the `RUSTC` env var or just a program named `rustc` and the argument `--error-format=json`. + /// + /// Take care to only append unless you actually meant to overwrite the defaults. + /// Overwriting the defaults may make `//~ ERROR` style comments stop working. + pub fn rustc() -> Self { + Self { + program: PathBuf::from(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into())), + args: vec!["--error-format=json".into()], + out_dir_flag: Some("--out-dir".into()), + input_file_flag: None, + envs: vec![], + } + } + + /// Same as [`CommandBuilder::rustc`], but with arguments for obtaining the cfgs. + pub fn cfgs() -> Self { + Self { + args: vec!["--print".into(), "cfg".into()], + ..Self::rustc() + } + } + + /// Build a `CommandBuilder` for a command without any argumemnts. + /// You can still add arguments later. + pub fn cmd(cmd: impl Into<PathBuf>) -> Self { + Self { + program: cmd.into(), + args: vec![], + out_dir_flag: None, + input_file_flag: None, + envs: vec![], + } + } + + /// Render the command like you'd use it on a command line. + pub fn display(&self) -> impl std::fmt::Display + '_ { + struct Display<'a>(&'a CommandBuilder); + impl std::fmt::Display for Display<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (var, val) in &self.0.envs { + if let Some(val) = val { + write!(f, "{var:?}={val:?} ")?; + } + } + self.0.program.display().fmt(f)?; + for arg in &self.0.args { + write!(f, " {arg:?}")?; + } + if let Some(flag) = &self.0.out_dir_flag { + write!(f, " {flag:?} OUT_DIR")?; + } + if let Some(flag) = &self.0.input_file_flag { + write!(f, " {flag:?}")?; + } + Ok(()) + } + } + Display(self) + } + + /// Create a command with the given settings. + pub fn build(&self, out_dir: &Path) -> Command { + let mut cmd = Command::new(&self.program); + cmd.args(self.args.iter()); + if let Some(flag) = &self.out_dir_flag { + cmd.arg(flag).arg(out_dir); + } + if let Some(flag) = &self.input_file_flag { + cmd.arg(flag); + } + self.apply_env(&mut cmd); + cmd + } + + pub(crate) fn apply_env(&self, cmd: &mut Command) { + for (var, val) in self.envs.iter() { + if let Some(val) = val { + cmd.env(var, val); + } else { + cmd.env_remove(var); + } + } + } +} diff --git a/vendor/ui_test-0.20.0/src/config.rs b/vendor/ui_test-0.20.0/src/config.rs new file mode 100644 index 000000000..6779bab7d --- /dev/null +++ b/vendor/ui_test-0.20.0/src/config.rs @@ -0,0 +1,272 @@ +use regex::bytes::Regex; + +use crate::{dependencies::build_dependencies, CommandBuilder, Filter, Match, Mode, RustfixMode}; +pub use color_eyre; +use color_eyre::eyre::Result; +use std::{ + ffi::OsString, + num::NonZeroUsize, + path::{Path, PathBuf}, +}; + +mod args; +pub use args::Args; + +#[derive(Debug, Clone)] +/// Central datastructure containing all information to run the tests. +pub struct Config { + /// Host triple; usually will be auto-detected. + pub host: Option<String>, + /// `None` to run on the host, otherwise a target triple + pub target: Option<String>, + /// Filters applied to stderr output before processing it. + /// By default contains a filter for replacing backslashes in paths with + /// regular slashes. + /// On windows, contains a filter to remove `\r`. + pub stderr_filters: Filter, + /// Filters applied to stdout output before processing it. + /// On windows, contains a filter to remove `\r`. + pub stdout_filters: Filter, + /// The folder in which to start searching for .rs files + pub root_dir: PathBuf, + /// The mode in which to run the tests. + pub mode: Mode, + /// The binary to actually execute. + pub program: CommandBuilder, + /// The command to run to obtain the cfgs that the output is supposed to + pub cfgs: CommandBuilder, + /// What to do in case the stdout/stderr output differs from the expected one. + pub output_conflict_handling: OutputConflictHandling, + /// Path to a `Cargo.toml` that describes which dependencies the tests can access. + pub dependencies_crate_manifest_path: Option<PathBuf>, + /// The command to run can be changed from `cargo` to any custom command to build the + /// dependencies in `dependencies_crate_manifest_path`. + pub dependency_builder: CommandBuilder, + /// Where to dump files like the binaries compiled from tests. + /// Defaults to `target/ui` in the current directory. + pub out_dir: PathBuf, + /// The default edition to use on all tests. + pub edition: Option<String>, + /// Skip test files whose names contain any of these entries. + pub skip_files: Vec<String>, + /// Only test files whose names contain any of these entries. + pub filter_files: Vec<String>, + /// Override the number of threads to use. + pub threads: Option<NonZeroUsize>, +} + +impl Config { + /// Create a configuration for testing the output of running + /// `rustc` on the test files. + pub fn rustc(root_dir: impl Into<PathBuf>) -> Self { + Self { + host: None, + target: None, + stderr_filters: vec![ + (Match::PathBackslash, b"/"), + #[cfg(windows)] + (Match::Exact(vec![b'\r']), b""), + #[cfg(windows)] + (Match::Exact(br"\\?\".to_vec()), b""), + ], + stdout_filters: vec![ + (Match::PathBackslash, b"/"), + #[cfg(windows)] + (Match::Exact(vec![b'\r']), b""), + #[cfg(windows)] + (Match::Exact(br"\\?\".to_vec()), b""), + ], + root_dir: root_dir.into(), + mode: Mode::Fail { + require_patterns: true, + rustfix: RustfixMode::MachineApplicable, + }, + program: CommandBuilder::rustc(), + cfgs: CommandBuilder::cfgs(), + output_conflict_handling: OutputConflictHandling::Bless, + dependencies_crate_manifest_path: None, + dependency_builder: CommandBuilder::cargo(), + out_dir: std::env::var_os("CARGO_TARGET_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| std::env::current_dir().unwrap().join("target")) + .join("ui"), + edition: Some("2021".into()), + skip_files: Vec::new(), + filter_files: Vec::new(), + threads: None, + } + } + + /// Create a configuration for testing the output of running + /// `cargo` on the test `Cargo.toml` files. + pub fn cargo(root_dir: impl Into<PathBuf>) -> Self { + Self { + program: CommandBuilder::cargo(), + edition: None, + mode: Mode::Fail { + require_patterns: true, + rustfix: RustfixMode::Disabled, + }, + ..Self::rustc(root_dir) + } + } + + /// Populate the config with the values from parsed command line arguments. + /// If neither `--bless` or `--check` are provided `default_bless` is used. + /// + /// The default output conflict handling command suggests adding `--bless` + /// to the end of the current command. + pub fn with_args(&mut self, args: &Args, default_bless: bool) { + let Args { + ref filters, + quiet: _, + check, + bless, + threads, + ref skip, + } = *args; + + self.threads = threads.or(self.threads); + + self.filter_files.extend_from_slice(filters); + self.skip_files.extend_from_slice(skip); + + let bless = match (bless, check) { + (_, true) => false, + (true, _) => true, + _ => default_bless, + }; + self.output_conflict_handling = if bless { + OutputConflictHandling::Bless + } else { + OutputConflictHandling::Error(format!( + "{} --bless", + std::env::args() + .map(|s| format!("{s:?}")) + .collect::<Vec<_>>() + .join(" ") + )) + }; + } + + /// Replace all occurrences of a path in stderr/stdout with a byte string. + pub fn path_filter(&mut self, path: &Path, replacement: &'static (impl AsRef<[u8]> + ?Sized)) { + self.path_stderr_filter(path, replacement); + self.path_stdout_filter(path, replacement); + } + + /// Replace all occurrences of a path in stderr with a byte string. + pub fn path_stderr_filter( + &mut self, + path: &Path, + replacement: &'static (impl AsRef<[u8]> + ?Sized), + ) { + let pattern = path.canonicalize().unwrap(); + self.stderr_filters + .push((pattern.parent().unwrap().into(), replacement.as_ref())); + } + + /// Replace all occurrences of a path in stdout with a byte string. + pub fn path_stdout_filter( + &mut self, + path: &Path, + replacement: &'static (impl AsRef<[u8]> + ?Sized), + ) { + let pattern = path.canonicalize().unwrap(); + self.stdout_filters + .push((pattern.parent().unwrap().into(), replacement.as_ref())); + } + + /// Replace all occurrences of a regex pattern in stderr/stdout with a byte string. + pub fn filter(&mut self, pattern: &str, replacement: &'static (impl AsRef<[u8]> + ?Sized)) { + self.stderr_filter(pattern, replacement); + self.stdout_filter(pattern, replacement); + } + + /// Replace all occurrences of a regex pattern in stderr with a byte string. + pub fn stderr_filter( + &mut self, + pattern: &str, + replacement: &'static (impl AsRef<[u8]> + ?Sized), + ) { + self.stderr_filters + .push((Regex::new(pattern).unwrap().into(), replacement.as_ref())); + } + + /// Replace all occurrences of a regex pattern in stdout with a byte string. + pub fn stdout_filter( + &mut self, + pattern: &str, + replacement: &'static (impl AsRef<[u8]> + ?Sized), + ) { + self.stdout_filters + .push((Regex::new(pattern).unwrap().into(), replacement.as_ref())); + } + + /// Compile dependencies and return the right flags + /// to find the dependencies. + pub fn build_dependencies(&self) -> Result<Vec<OsString>> { + let dependencies = build_dependencies(self)?; + let mut args = vec![]; + for (name, artifacts) in dependencies.dependencies { + for dependency in artifacts { + args.push("--extern".into()); + let mut dep = OsString::from(&name); + dep.push("="); + dep.push(dependency); + args.push(dep); + } + } + for import_path in dependencies.import_paths { + args.push("-L".into()); + args.push(import_path.into()); + } + Ok(args) + } + + /// Make sure we have the host and target triples. + pub fn fill_host_and_target(&mut self) -> Result<()> { + if self.host.is_none() { + self.host = Some( + rustc_version::VersionMeta::for_command(std::process::Command::new( + &self.program.program, + )) + .map_err(|err| { + color_eyre::eyre::Report::new(err).wrap_err(format!( + "failed to parse rustc version info: {}", + self.program.display() + )) + })? + .host, + ); + } + if self.target.is_none() { + self.target = Some(self.host.clone().unwrap()); + } + Ok(()) + } + + pub(crate) fn has_asm_support(&self) -> bool { + static ASM_SUPPORTED_ARCHS: &[&str] = &[ + "x86", "x86_64", "arm", "aarch64", "riscv32", + "riscv64", + // These targets require an additional asm_experimental_arch feature. + // "nvptx64", "hexagon", "mips", "mips64", "spirv", "wasm32", + ]; + ASM_SUPPORTED_ARCHS + .iter() + .any(|arch| self.target.as_ref().unwrap().contains(arch)) + } +} + +#[derive(Debug, Clone)] +/// The different options for what to do when stdout/stderr files differ from the actual output. +pub enum OutputConflictHandling { + /// The string should be a command that can be executed to bless all tests. + Error(String), + /// Ignore mismatches in the stderr/stdout files. + Ignore, + /// Instead of erroring if the stderr/stdout differs from the expected + /// automatically replace it with the found output (after applying filters). + Bless, +} diff --git a/vendor/ui_test-0.20.0/src/config/args.rs b/vendor/ui_test-0.20.0/src/config/args.rs new file mode 100644 index 000000000..2d65dc7d5 --- /dev/null +++ b/vendor/ui_test-0.20.0/src/config/args.rs @@ -0,0 +1,90 @@ +//! Default argument processing when `ui_test` is used +//! as a test driver. + +use std::{borrow::Cow, num::NonZeroUsize}; + +use color_eyre::eyre::{bail, ensure, Result}; + +/// Plain arguments if `ui_test` is used as a binary. +#[derive(Debug, Default)] +pub struct Args { + /// Filters that will be used to match on individual tests + pub filters: Vec<String>, + + /// Whether to minimize output given to the user. + pub quiet: bool, + + /// Whether to error on mismatches between `.stderr` files and actual + /// output. + pub check: bool, + + /// Whether to overwrite `.stderr` files on mismtach with the actual + /// output. + pub bless: bool, + + /// The number of threads to use + pub threads: Option<NonZeroUsize>, + + /// Skip tests whose names contain any of these entries. + pub skip: Vec<String>, +} + +impl Args { + /// Parse the program arguments. + /// This is meant to be used if `ui_test` is used as a `harness=false` test, called from `cargo test`. + pub fn test() -> Result<Self> { + Self::default().parse_args(std::env::args().skip(1)) + } + + /// Parse arguments into an existing `Args` struct. + pub fn parse_args(mut self, mut iter: impl Iterator<Item = String>) -> Result<Self> { + while let Some(arg) = iter.next() { + if arg == "--" { + continue; + } + if arg == "--quiet" { + self.quiet = true; + } else if arg == "--check" { + self.check = true; + } else if arg == "--bless" { + self.bless = true; + } else if let Some(skip) = parse_value("--skip", &arg, &mut iter)? { + self.skip.push(skip.into_owned()); + } else if arg == "--help" { + bail!("available flags: --quiet, --check, --bless, --test-threads=n, --skip") + } else if let Some(n) = parse_value("--test-threads", &arg, &mut iter)? { + self.threads = Some(n.parse()?); + } else if arg.starts_with("--") { + bail!( + "unknown command line flag `{arg}`: {:?}", + iter.collect::<Vec<_>>() + ); + } else { + self.filters.push(arg); + } + } + Ok(self) + } +} + +fn parse_value<'a>( + name: &str, + arg: &'a str, + iter: &mut impl Iterator<Item = String>, +) -> Result<Option<Cow<'a, str>>> { + let with_eq = match arg.strip_prefix(name) { + Some(s) => s, + None => return Ok(None), + }; + if let Some(n) = with_eq.strip_prefix('=') { + Ok(Some(n.into())) + } else { + ensure!(with_eq.is_empty(), "`{name}` can only be followed by `=`"); + + if let Some(next) = iter.next() { + Ok(Some(next.into())) + } else { + bail!("`name` must be followed by a value") + } + } +} diff --git a/vendor/ui_test-0.20.0/src/dependencies.rs b/vendor/ui_test-0.20.0/src/dependencies.rs new file mode 100644 index 000000000..0e95e0f84 --- /dev/null +++ b/vendor/ui_test-0.20.0/src/dependencies.rs @@ -0,0 +1,320 @@ +use cargo_metadata::{camino::Utf8PathBuf, DependencyKind}; +use cargo_platform::Cfg; +use color_eyre::eyre::{bail, eyre, Result}; +use std::{ + collections::{hash_map::Entry, HashMap, HashSet}, + ffi::OsString, + path::PathBuf, + process::Command, + str::FromStr, + sync::{Arc, OnceLock, RwLock}, +}; + +use crate::{ + build_aux, status_emitter::StatusEmitter, Config, Errored, Mode, OutputConflictHandling, +}; + +#[derive(Default, Debug)] +pub struct Dependencies { + /// All paths that must be imported with `-L dependency=`. This is for + /// finding proc macros run on the host and dependencies for the target. + pub import_paths: Vec<PathBuf>, + /// The name as chosen in the `Cargo.toml` and its corresponding rmeta file. + pub dependencies: Vec<(String, Vec<Utf8PathBuf>)>, +} + +fn cfgs(config: &Config) -> Result<Vec<Cfg>> { + let mut cmd = config.cfgs.build(&config.out_dir); + cmd.arg("--target").arg(config.target.as_ref().unwrap()); + let output = cmd.output()?; + let stdout = String::from_utf8(output.stdout)?; + + if !output.status.success() { + let stderr = String::from_utf8(output.stderr)?; + bail!( + "failed to obtain `cfg` information from {cmd:?}:\nstderr:\n{stderr}\n\nstdout:{stdout}" + ); + } + let mut cfgs = vec![]; + + for line in stdout.lines() { + cfgs.push(Cfg::from_str(line)?); + } + + Ok(cfgs) +} + +/// Compiles dependencies and returns the crate names and corresponding rmeta files. +pub(crate) fn build_dependencies(config: &Config) -> Result<Dependencies> { + let manifest_path = match &config.dependencies_crate_manifest_path { + Some(path) => path.to_owned(), + None => return Ok(Default::default()), + }; + let manifest_path = &manifest_path; + let mut build = config.dependency_builder.build(&config.out_dir); + build.arg(manifest_path); + + if let Some(target) = &config.target { + build.arg(format!("--target={target}")); + } + + // Reusable closure for setting up the environment both for artifact generation and `cargo_metadata` + let set_locking = |cmd: &mut Command| match (&config.output_conflict_handling, &config.mode) { + (_, Mode::Yolo { .. }) => {} + (OutputConflictHandling::Error(_), _) => { + cmd.arg("--locked"); + } + _ => {} + }; + + set_locking(&mut build); + build.arg("--message-format=json"); + + let output = build.output()?; + + if !output.status.success() { + let stdout = String::from_utf8(output.stdout)?; + let stderr = String::from_utf8(output.stderr)?; + bail!("failed to compile dependencies:\ncommand: {build:?}\nstderr:\n{stderr}\n\nstdout:{stdout}"); + } + + // Collect all artifacts generated + let artifact_output = output.stdout; + let artifact_output = String::from_utf8(artifact_output)?; + let mut import_paths: HashSet<PathBuf> = HashSet::new(); + let mut artifacts = HashMap::new(); + for line in artifact_output.lines() { + let Ok(message) = serde_json::from_str::<cargo_metadata::Message>(line) else { + continue; + }; + if let cargo_metadata::Message::CompilerArtifact(artifact) = message { + if artifact + .filenames + .iter() + .any(|f| f.ends_with("build-script-build")) + { + continue; + } + // Check that we only collect rmeta and rlib crates, not build script crates + if artifact + .filenames + .iter() + .any(|f| !matches!(f.extension(), Some("rlib" | "rmeta"))) + { + continue; + } + for filename in &artifact.filenames { + import_paths.insert(filename.parent().unwrap().into()); + } + let package_id = artifact.package_id; + if let Some(prev) = artifacts.insert(package_id.clone(), Ok(artifact.filenames)) { + artifacts.insert( + package_id.clone(), + Err(format!("{prev:#?} vs {:#?}", artifacts[&package_id])), + ); + } + } + } + + // Check which crates are mentioned in the crate itself + let mut metadata = cargo_metadata::MetadataCommand::new().cargo_command(); + metadata.arg("--manifest-path").arg(manifest_path); + config.dependency_builder.apply_env(&mut metadata); + set_locking(&mut metadata); + let output = metadata.output()?; + + if !output.status.success() { + let stdout = String::from_utf8(output.stdout)?; + let stderr = String::from_utf8(output.stderr)?; + bail!("failed to run cargo-metadata:\nstderr:\n{stderr}\n\nstdout:{stdout}"); + } + + let output = output.stdout; + let output = String::from_utf8(output)?; + + let cfg = cfgs(config)?; + + for line in output.lines() { + if !line.starts_with('{') { + continue; + } + let metadata: cargo_metadata::Metadata = serde_json::from_str(line)?; + // Only take artifacts that are defined in the Cargo.toml + + // First, find the root artifact + let root = metadata + .packages + .iter() + .find(|package| { + package.manifest_path.as_std_path().canonicalize().unwrap() + == manifest_path.canonicalize().unwrap() + }) + .unwrap(); + + // Then go over all of its dependencies + let dependencies = root + .dependencies + .iter() + .filter(|dep| matches!(dep.kind, DependencyKind::Normal)) + // Only consider dependencies that are enabled on the current target + .filter(|dep| match &dep.target { + Some(platform) => platform.matches(config.target.as_ref().unwrap(), &cfg), + None => true, + }) + .map(|dep| { + let package = metadata + .packages + .iter() + .find(|&p| p.name == dep.name && dep.req.matches(&p.version)) + .expect("dependency does not exist"); + ( + package, + dep.rename.clone().unwrap_or_else(|| package.name.clone()), + ) + }) + // Also expose the root crate + .chain(std::iter::once((root, root.name.clone()))) + .filter_map(|(package, name)| { + // Get the id for the package matching the version requirement of the dep + let id = &package.id; + // Return the name chosen in `Cargo.toml` and the path to the corresponding artifact + match artifacts.remove(id) { + Some(Ok(artifacts)) => Some(Ok((name.replace('-', "_"), artifacts))), + Some(Err(what)) => Some(Err(eyre!("`ui_test` does not support crates that appear as both build-dependencies and core dependencies: {id}: {what}"))), + None => { + if name == root.name { + // If there are no artifacts, this is the root crate and it is being built as a binary/test + // instead of a library. We simply add no artifacts, meaning you can't depend on functions + // and types declared in the root crate. + None + } else { + panic!("no artifact found for `{name}`(`{id}`):`\n{artifact_output}") + } + } + } + }) + .collect::<Result<Vec<_>>>()?; + let import_paths = import_paths.into_iter().collect(); + return Ok(Dependencies { + dependencies, + import_paths, + }); + } + + bail!("no json found in cargo-metadata output") +} + +#[derive(PartialEq, Eq, Debug, Hash, Clone)] +pub enum Build { + /// Build the dependencies. + Dependencies, + /// Build an aux-build. + Aux { aux_file: PathBuf }, +} +impl Build { + fn description(&self) -> String { + match self { + Build::Dependencies => "Building dependencies".into(), + Build::Aux { aux_file } => format!("Building aux file {}", aux_file.display()), + } + } +} + +pub struct BuildManager<'a> { + #[allow(clippy::type_complexity)] + cache: RwLock<HashMap<Build, Arc<OnceLock<Result<Vec<OsString>, ()>>>>>, + status_emitter: &'a dyn StatusEmitter, +} + +impl<'a> BuildManager<'a> { + pub fn new(status_emitter: &'a dyn StatusEmitter) -> Self { + Self { + cache: Default::default(), + status_emitter, + } + } + /// This function will block until the build is done and then return the arguments + /// that need to be passed in order to build the dependencies. + /// The error is only reported once, all follow up invocations of the same build will + /// have a generic error about a previous build failing. + pub fn build(&self, what: Build, config: &Config) -> Result<Vec<OsString>, Errored> { + // Fast path without much contention. + if let Some(res) = self.cache.read().unwrap().get(&what).and_then(|o| o.get()) { + return res.clone().map_err(|()| Errored { + command: Command::new(format!("{what:?}")), + errors: vec![], + stderr: b"previous build failed".to_vec(), + stdout: vec![], + }); + } + let mut lock = self.cache.write().unwrap(); + let once = match lock.entry(what.clone()) { + Entry::Occupied(entry) => { + if let Some(res) = entry.get().get() { + return res.clone().map_err(|()| Errored { + command: Command::new(format!("{what:?}")), + errors: vec![], + stderr: b"previous build failed".to_vec(), + stdout: vec![], + }); + } + entry.get().clone() + } + Entry::Vacant(entry) => { + let once = Arc::new(OnceLock::new()); + entry.insert(once.clone()); + once + } + }; + drop(lock); + + let mut err = None; + once.get_or_init(|| { + let build = self + .status_emitter + .register_test(what.description().into()) + .for_revision(""); + let res = match &what { + Build::Dependencies => match config.build_dependencies() { + Ok(args) => Ok(args), + Err(e) => { + err = Some(Errored { + command: Command::new(format!("{what:?}")), + errors: vec![], + stderr: format!("{e:?}").into_bytes(), + stdout: vec![], + }); + Err(()) + } + }, + Build::Aux { aux_file } => match build_aux(aux_file, config, self) { + Ok(args) => Ok(args.iter().map(Into::into).collect()), + Err(e) => { + err = Some(e); + Err(()) + } + }, + }; + build.done( + &res.as_ref() + .map(|_| crate::TestOk::Ok) + .map_err(|()| Errored { + command: Command::new(what.description()), + errors: vec![], + stderr: vec![], + stdout: vec![], + }), + ); + res + }) + .clone() + .map_err(|()| { + err.unwrap_or_else(|| Errored { + command: Command::new(what.description()), + errors: vec![], + stderr: b"previous build failed".to_vec(), + stdout: vec![], + }) + }) + } +} diff --git a/vendor/ui_test-0.20.0/src/diff.rs b/vendor/ui_test-0.20.0/src/diff.rs new file mode 100644 index 000000000..b0346c250 --- /dev/null +++ b/vendor/ui_test-0.20.0/src/diff.rs @@ -0,0 +1,164 @@ +use colored::*; +use prettydiff::{basic::DiffOp, basic::DiffOp::*, diff_lines, diff_words}; + +/// How many lines of context are displayed around the actual diffs +const CONTEXT: usize = 2; + +fn skip(skipped_lines: &[&str]) { + // When the amount of skipped lines is exactly `CONTEXT * 2`, we already + // print all the context and don't actually skip anything. + match skipped_lines.len().checked_sub(CONTEXT * 2) { + Some(skipped @ 2..) => { + // Print an initial `CONTEXT` amount of lines. + for line in &skipped_lines[..CONTEXT] { + println!(" {line}"); + } + println!("... {skipped} lines skipped ..."); + // Print `... n lines skipped ...` followed by the last `CONTEXT` lines. + for line in &skipped_lines[skipped + CONTEXT..] { + println!(" {line}"); + } + } + _ => { + // Print all the skipped lines if the amount of context desired is less than the amount of lines + for line in skipped_lines { + println!(" {line}"); + } + } + } +} + +fn row(row: DiffOp<'_, &str>) { + match row { + Remove(l) => { + for l in l { + println!("{}{}", "-".red(), l.red()); + } + } + Equal(l) => { + skip(l); + } + Replace(l, r) => { + for (l, r) in l.iter().zip(r) { + print_line_diff(l, r); + } + } + Insert(r) => { + for r in r { + println!("{}{}", "+".green(), r.green()); + } + } + } +} + +fn print_line_diff(l: &str, r: &str) { + let diff = diff_words(l, r); + let diff = diff.diff(); + if has_both_insertions_and_deletions(&diff) + || !colored::control::SHOULD_COLORIZE.should_colorize() + { + // The line both adds and removes chars, print both lines, but highlight their differences instead of + // drawing the entire line in red/green. + print!("{}", "-".red()); + for char in &diff { + match *char { + Replace(l, _) | Remove(l) => { + for l in l { + print!("{}", l.to_string().on_red()) + } + } + Insert(_) => {} + Equal(l) => { + for l in l { + print!("{l}") + } + } + } + } + println!(); + print!("{}", "+".green()); + for char in diff { + match char { + Remove(_) => {} + Replace(_, r) | Insert(r) => { + for r in r { + print!("{}", r.to_string().on_green()) + } + } + Equal(r) => { + for r in r { + print!("{r}") + } + } + } + } + println!(); + } else { + // The line only adds or only removes chars, print a single line highlighting their differences. + print!("{}", "~".yellow()); + for char in diff { + match char { + Remove(l) => { + for l in l { + print!("{}", l.to_string().on_red()) + } + } + Equal(w) => { + for w in w { + print!("{w}") + } + } + Insert(r) => { + for r in r { + print!("{}", r.to_string().on_green()) + } + } + Replace(l, r) => { + for l in l { + print!("{}", l.to_string().on_red()) + } + for r in r { + print!("{}", r.to_string().on_green()) + } + } + } + } + println!(); + } +} + +fn has_both_insertions_and_deletions(diff: &[DiffOp<'_, &str>]) -> bool { + let mut seen_l = false; + let mut seen_r = false; + for char in diff { + let is_whitespace = |s: &[&str]| s.iter().any(|s| s.chars().any(|s| s.is_whitespace())); + match char { + Insert(l) if !is_whitespace(l) => seen_l = true, + Remove(r) if !is_whitespace(r) => seen_r = true, + Replace(l, r) if !is_whitespace(l) && !is_whitespace(r) => return true, + _ => {} + } + } + seen_l && seen_r +} + +pub fn print_diff(expected: &[u8], actual: &[u8]) { + let expected_str = String::from_utf8_lossy(expected); + let actual_str = String::from_utf8_lossy(actual); + + if expected_str.as_bytes() != expected || actual_str.as_bytes() != actual { + println!( + "{}", + "Non-UTF8 characters in output, diff may be imprecise.".red() + ); + } + + let pat = |c: char| c.is_whitespace() && c != ' ' && c != '\n' && c != '\r'; + let expected_str = expected_str.replace(pat, "â–‘"); + let actual_str = actual_str.replace(pat, "â–‘"); + + for r in diff_lines(&expected_str, &actual_str).diff() { + row(r); + } + println!() +} diff --git a/vendor/ui_test-0.20.0/src/error.rs b/vendor/ui_test-0.20.0/src/error.rs new file mode 100644 index 000000000..7298a699e --- /dev/null +++ b/vendor/ui_test-0.20.0/src/error.rs @@ -0,0 +1,81 @@ +use crate::{ + parser::{Pattern, Spanned}, + rustc_stderr::{Message, Span}, + Mode, +}; +use std::{num::NonZeroUsize, path::PathBuf, process::ExitStatus}; + +/// All the ways in which a test can fail. +#[derive(Debug)] +#[must_use] +pub enum Error { + /// Got an invalid exit status for the given mode. + ExitStatus { + /// The expected mode. + mode: Mode, + /// The exit status of the command. + status: ExitStatus, + /// The expected exit status as set in the file or derived from the mode. + expected: i32, + }, + /// A pattern was declared but had no matching error. + PatternNotFound(Spanned<Pattern>), + /// A ui test checking for failure does not have any failure patterns + NoPatternsFound, + /// A ui test checking for success has failure patterns + PatternFoundInPassTest, + /// Stderr/Stdout differed from the `.stderr`/`.stdout` file present. + OutputDiffers { + /// The file containing the expected output that differs from the actual output. + path: PathBuf, + /// The output from the command. + actual: Vec<u8>, + /// The contents of the file. + expected: Vec<u8>, + /// A command, that when run, causes the output to get blessed instead of erroring. + bless_command: String, + }, + /// There were errors that don't have a pattern. + ErrorsWithoutPattern { + /// The main message of the error. + msgs: Vec<Message>, + /// File and line information of the error. + path: Option<Spanned<PathBuf>>, + }, + /// A comment failed to parse. + InvalidComment { + /// The comment + msg: String, + /// The character range in which it was defined. + span: Span, + }, + /// Conflicting comments + MultipleRevisionsWithResults { + /// The comment being looked for + kind: String, + /// The lines where conflicts happened + lines: Vec<NonZeroUsize>, + }, + /// A subcommand (e.g. rustfix) of a test failed. + Command { + /// The name of the subcommand (e.g. "rustfix"). + kind: String, + /// The exit status of the command. + status: ExitStatus, + }, + /// This catches crashes of ui tests and reports them along the failed test. + Bug(String), + /// An auxiliary build failed with its own set of errors. + Aux { + /// Path to the aux file. + path: PathBuf, + /// The errors that occurred during the build of the aux file. + errors: Vec<Error>, + /// The line in which the aux file was requested to be built. + line: NonZeroUsize, + }, + /// An error occured applying [`rustfix`] suggestions + Rustfix(anyhow::Error), +} + +pub(crate) type Errors = Vec<Error>; diff --git a/vendor/ui_test-0.20.0/src/github_actions.rs b/vendor/ui_test-0.20.0/src/github_actions.rs new file mode 100644 index 000000000..01e1d3bb2 --- /dev/null +++ b/vendor/ui_test-0.20.0/src/github_actions.rs @@ -0,0 +1,96 @@ +//! An interface to github actions workflow commands. + +use std::{ + fmt::{Debug, Write}, + num::NonZeroUsize, +}; + +/// Shows an error message directly in a github diff view on drop. +pub struct Error { + file: String, + line: usize, + title: String, + message: String, +} +impl Error { + /// Set a line for this error. By default the message is shown at the top of the file. + pub fn line(mut self, line: NonZeroUsize) -> Self { + self.line = line.get(); + self + } +} + +/// Create an error to be shown for the given file and with the given title. +pub fn error(file: impl std::fmt::Display, title: impl Into<String>) -> Error { + Error { + file: file.to_string(), + line: 0, + title: title.into(), + message: String::new(), + } +} + +impl Write for Error { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.message.write_str(s) + } +} + +impl Drop for Error { + fn drop(&mut self) { + if std::env::var_os("GITHUB_ACTION").is_some() { + let Error { + file, + line, + title, + message, + } = self; + let message = message.trim(); + let message = if message.is_empty() { + "::no message".into() + } else { + format!("::{}", github_action_multiline_escape(message)) + }; + println!("::error file={file},line={line},title={title}{message}"); + } + } +} + +/// Append to the summary file that will be shown for the entire CI run. +pub fn summary() -> Option<impl std::io::Write> { + let path = std::env::var_os("GITHUB_STEP_SUMMARY")?; + Some(std::fs::OpenOptions::new().append(true).open(path).unwrap()) +} + +fn github_action_multiline_escape(s: &str) -> String { + s.replace('%', "%25") + .replace('\n', "%0A") + .replace('\r', "%0D") +} + +/// All github actions log messages from this call to the Drop of the return value +/// will be grouped and hidden by default in logs. Note that nesting these does +/// not really work. +pub fn group(name: impl std::fmt::Display) -> Group { + if std::env::var_os("GITHUB_ACTION").is_some() { + println!("::group::{name}"); + } + Group(()) +} + +/// A guard that closes the current github actions log group on drop. +pub struct Group(()); + +impl Debug for Group { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("a handle that will close the github action group on drop") + } +} + +impl Drop for Group { + fn drop(&mut self) { + if std::env::var_os("GITHUB_ACTION").is_some() { + println!("::endgroup::"); + } + } +} diff --git a/vendor/ui_test-0.20.0/src/lib.rs b/vendor/ui_test-0.20.0/src/lib.rs new file mode 100644 index 000000000..ba051be8c --- /dev/null +++ b/vendor/ui_test-0.20.0/src/lib.rs @@ -0,0 +1,1270 @@ +#![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<u8>), + /// 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<Regex> 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::<true> { 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<Command> { + 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<TestOk, Errored>; + +/// Information about a test failure. +#[derive(Debug)] +pub struct Errored { + /// Command that failed + command: Command, + /// The errors that were encountered. + errors: Vec<Error>, + /// The full stderr of the test run. + stderr: Vec<u8>, + /// The full stdout of the test run. + stdout: Vec<u8>, +} + +struct TestRun { + result: TestResult, + status: Box<dyn status_emitter::TestStatus>, +} + +/// 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<Config>, + 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::<Vec<_>>(); + 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("<unknown>"), + errors: vec![Error::Bug( + *Box::<dyn std::any::Any + Send + 'static>::downcast::<String>( + 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<SUBMISSION: Send, RESULT: Send>( + num_threads: NonZeroUsize, + submitter: impl FnOnce(Sender<SUBMISSION>) + Send, + runner: impl Sync + Fn(&Receiver<SUBMISSION>, Sender<RESULT>) -> Result<()>, + collector: impl FnOnce(Receiver<RESULT>) + 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<u8>, +) -> Result<Vec<TestRun>, 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<Comments, Errored> { + 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<Command, Errored> { + 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<Vec<OsString>, 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<u8>, Vec<u8>) { + 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<Vec<OsString>, 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<Mode>, + 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<OsString>, +) -> 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::<Vec<_>>(); + 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<Error>, + 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<Vec<Message>>, + mut messages_from_unknown_file_or_line: Vec<Message>, + 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<Message>| -> 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<u8> { + // 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<Item = Component<'a>> { + 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) +} diff --git a/vendor/ui_test-0.20.0/src/mode.rs b/vendor/ui_test-0.20.0/src/mode.rs new file mode 100644 index 000000000..ce37adc4c --- /dev/null +++ b/vendor/ui_test-0.20.0/src/mode.rs @@ -0,0 +1,93 @@ +use super::Error; +use crate::parser::Comments; +use crate::parser::MaybeSpanned; +use crate::Errored; +use std::fmt::Display; +use std::process::ExitStatus; + +/// When to run rustfix on tests +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum RustfixMode { + /// Do not run rustfix on the test + Disabled, + /// Apply only `MachineApplicable` suggestions emitted by the test + MachineApplicable, + /// Apply all suggestions emitted by the test + Everything, +} + +impl RustfixMode { + pub(crate) fn enabled(self) -> bool { + self != RustfixMode::Disabled + } +} + +#[derive(Copy, Clone, Debug)] +/// Decides what is expected of each test's exit status. +pub enum Mode { + /// The test passes a full execution of the rustc driver + Pass, + /// The test produces an executable binary that can get executed on the host + Run { + /// The expected exit code + exit_code: i32, + }, + /// The rustc driver panicked + Panic, + /// The rustc driver emitted an error + Fail { + /// Whether failing tests must have error patterns. Set to false if you just care about .stderr output. + require_patterns: bool, + /// When to run rustfix on the test + rustfix: RustfixMode, + }, + /// Run the tests, but always pass them as long as all annotations are satisfied and stderr files match. + Yolo { + /// When to run rustfix on the test + rustfix: RustfixMode, + }, +} + +impl Mode { + pub(crate) fn ok(self, status: ExitStatus) -> Result<(), Error> { + let expected = match self { + Mode::Run { exit_code } => exit_code, + Mode::Pass => 0, + Mode::Panic => 101, + Mode::Fail { .. } => 1, + Mode::Yolo { .. } => return Ok(()), + }; + if status.code() == Some(expected) { + Ok(()) + } else { + Err(Error::ExitStatus { + mode: self, + status, + expected, + }) + } + } + pub(crate) fn maybe_override( + self, + comments: &Comments, + revision: &str, + ) -> Result<MaybeSpanned<Self>, Errored> { + let mode = comments.find_one_for_revision(revision, "mode changes", |r| r.mode)?; + Ok(mode.map_or(MaybeSpanned::new_config(self), Into::into)) + } +} + +impl Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Mode::Run { exit_code } => write!(f, "run({exit_code})"), + Mode::Pass => write!(f, "pass"), + Mode::Panic => write!(f, "panic"), + Mode::Fail { + require_patterns: _, + rustfix: _, + } => write!(f, "fail"), + Mode::Yolo { rustfix: _ } => write!(f, "yolo"), + } + } +} diff --git a/vendor/ui_test-0.20.0/src/parser.rs b/vendor/ui_test-0.20.0/src/parser.rs new file mode 100644 index 000000000..62ee5a1d1 --- /dev/null +++ b/vendor/ui_test-0.20.0/src/parser.rs @@ -0,0 +1,822 @@ +use std::{ + collections::HashMap, + num::NonZeroUsize, + path::{Path, PathBuf}, + process::Command, +}; + +use bstr::{ByteSlice, Utf8Error}; +use regex::bytes::Regex; + +use crate::{ + rustc_stderr::{Level, Span}, + Error, Errored, Mode, +}; + +use color_eyre::eyre::{Context, Result}; + +pub(crate) use spanned::*; + +mod spanned; +#[cfg(test)] +mod tests; + +/// This crate supports various magic comments that get parsed as file-specific +/// configuration values. This struct parses them all in one go and then they +/// get processed by their respective use sites. +#[derive(Default, Debug)] +pub(crate) struct Comments { + /// List of revision names to execute. Can only be specified once + pub revisions: Option<Vec<String>>, + /// Comments that are only available under specific revisions. + /// The defaults are in key `vec![]` + pub revisioned: HashMap<Vec<String>, Revisioned>, +} + +impl Comments { + /// Check that a comment isn't specified twice across multiple differently revisioned statements. + /// e.g. `//@[foo, bar] error-in-other-file: bop` and `//@[foo, baz] error-in-other-file boop` would end up + /// specifying two error patterns that are available in revision `foo`. + pub fn find_one_for_revision<'a, T: 'a>( + &'a self, + revision: &'a str, + kind: &str, + f: impl Fn(&'a Revisioned) -> OptWithLine<T>, + ) -> Result<OptWithLine<T>, Errored> { + let mut result = None; + let mut errors = vec![]; + for rev in self.for_revision(revision) { + if let Some(found) = f(rev).into_inner() { + if result.is_some() { + errors.push(found.line()); + } else { + result = found.into(); + } + } + } + if errors.is_empty() { + Ok(result.into()) + } else { + Err(Errored { + command: Command::new(format!("<finding flags for revision `{revision}`>")), + errors: vec![Error::MultipleRevisionsWithResults { + kind: kind.to_string(), + lines: errors, + }], + stderr: vec![], + stdout: vec![], + }) + } + } + + /// Returns an iterator over all revisioned comments that match the revision. + pub fn for_revision<'a>(&'a self, revision: &'a str) -> impl Iterator<Item = &'a Revisioned> { + self.revisioned.iter().filter_map(move |(k, v)| { + if k.is_empty() || k.iter().any(|rev| rev == revision) { + Some(v) + } else { + None + } + }) + } + + pub(crate) fn edition( + &self, + revision: &str, + config: &crate::Config, + ) -> Result<Option<MaybeSpanned<String>>, Errored> { + let edition = + self.find_one_for_revision(revision, "`edition` annotations", |r| r.edition.clone())?; + let edition = edition + .into_inner() + .map(MaybeSpanned::from) + .or(config.edition.clone().map(MaybeSpanned::new_config)); + Ok(edition) + } +} + +#[derive(Debug)] +/// Comments that can be filtered for specific revisions. +pub(crate) struct Revisioned { + /// The character range in which this revisioned item was first added. + /// Used for reporting errors on unknown revisions. + pub span: Span, + /// Don't run this test if any of these filters apply + pub ignore: Vec<Condition>, + /// Only run this test if all of these filters apply + pub only: Vec<Condition>, + /// Generate one .stderr file per bit width, by prepending with `.64bit` and similar + pub stderr_per_bitwidth: bool, + /// Additional flags to pass to the executable + pub compile_flags: Vec<String>, + /// Additional env vars to set for the executable + pub env_vars: Vec<(String, String)>, + /// Normalizations to apply to the stderr output before emitting it to disk + pub normalize_stderr: Vec<(Regex, Vec<u8>)>, + /// Normalizations to apply to the stdout output before emitting it to disk + pub normalize_stdout: Vec<(Regex, Vec<u8>)>, + /// Arbitrary patterns to look for in the stderr. + /// The error must be from another file, as errors from the current file must be + /// checked via `error_matches`. + pub error_in_other_files: Vec<Spanned<Pattern>>, + pub error_matches: Vec<ErrorMatch>, + /// Ignore diagnostics below this level. + /// `None` means pick the lowest level from the `error_pattern`s. + pub require_annotations_for_level: OptWithLine<Level>, + pub aux_builds: Vec<Spanned<PathBuf>>, + pub edition: OptWithLine<String>, + /// Overwrites the mode from `Config`. + pub mode: OptWithLine<Mode>, + pub needs_asm_support: bool, + /// Don't run [`rustfix`] for this test + pub no_rustfix: OptWithLine<()>, +} + +#[derive(Debug)] +struct CommentParser<T> { + /// The comments being built. + comments: T, + /// Any errors that ocurred during comment parsing. + errors: Vec<Error>, + /// The available commands and their parsing logic + commands: HashMap<&'static str, CommandParserFunc>, +} + +type CommandParserFunc = fn(&mut CommentParser<&mut Revisioned>, args: Spanned<&str>, span: Span); + +impl<T> std::ops::Deref for CommentParser<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.comments + } +} + +impl<T> std::ops::DerefMut for CommentParser<T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.comments + } +} + +/// The conditions used for "ignore" and "only" filters. +#[derive(Debug)] +pub(crate) enum Condition { + /// The given string must appear in the host triple. + Host(String), + /// The given string must appear in the target triple. + Target(String), + /// Tests that the bitwidth is the given one. + Bitwidth(u8), + /// Tests that the target is the host. + OnHost, +} + +#[derive(Debug, Clone)] +/// An error pattern parsed from a `//~` comment. +pub enum Pattern { + SubString(String), + Regex(Regex), +} + +#[derive(Debug)] +pub(crate) struct ErrorMatch { + pub pattern: Spanned<Pattern>, + pub level: Level, + /// The line this pattern is expecting to find a message in. + pub line: NonZeroUsize, +} + +impl Condition { + fn parse(c: &str) -> std::result::Result<Self, String> { + if c == "on-host" { + Ok(Condition::OnHost) + } else if let Some(bits) = c.strip_suffix("bit") { + let bits: u8 = bits.parse().map_err(|_err| { + format!("invalid ignore/only filter ending in 'bit': {c:?} is not a valid bitwdith") + })?; + Ok(Condition::Bitwidth(bits)) + } else if let Some(triple_substr) = c.strip_prefix("target-") { + Ok(Condition::Target(triple_substr.to_owned())) + } else if let Some(triple_substr) = c.strip_prefix("host-") { + Ok(Condition::Host(triple_substr.to_owned())) + } else { + Err(format!( + "`{c}` is not a valid condition, expected `on-host`, /[0-9]+bit/, /host-.*/, or /target-.*/" + )) + } + } +} + +impl Comments { + pub(crate) fn parse_file(path: &Path) -> Result<std::result::Result<Self, Vec<Error>>> { + let content = + std::fs::read(path).wrap_err_with(|| format!("failed to read {}", path.display()))?; + Ok(Self::parse(&content)) + } + + /// Parse comments in `content`. + /// `path` is only used to emit diagnostics if parsing fails. + pub(crate) fn parse( + content: &(impl AsRef<[u8]> + ?Sized), + ) -> std::result::Result<Self, Vec<Error>> { + let mut parser = CommentParser { + comments: Comments::default(), + errors: vec![], + commands: CommentParser::<_>::commands(), + }; + + let mut fallthrough_to = None; // The line that a `|` will refer to. + for (l, line) in content.as_ref().lines().enumerate() { + let l = NonZeroUsize::new(l + 1).unwrap(); // enumerate starts at 0, but line numbers start at 1 + let span = Span { + line_start: l, + line_end: l, + column_start: NonZeroUsize::new(1).unwrap(), + column_end: NonZeroUsize::new(line.chars().count() + 1).unwrap(), + }; + match parser.parse_checked_line(&mut fallthrough_to, Spanned::new(line, span)) { + Ok(()) => {} + Err(e) => parser.error(span, format!("Comment is not utf8: {e:?}")), + } + } + if let Some(revisions) = &parser.comments.revisions { + for (key, revisioned) in &parser.comments.revisioned { + for rev in key { + if !revisions.contains(rev) { + parser.errors.push(Error::InvalidComment { + msg: format!("the revision `{rev}` is not known"), + span: revisioned.span, + }) + } + } + } + } else { + for (key, revisioned) in &parser.comments.revisioned { + if !key.is_empty() { + parser.errors.push(Error::InvalidComment { + msg: "there are no revisions in this test".into(), + span: revisioned.span, + }) + } + } + } + if parser.errors.is_empty() { + Ok(parser.comments) + } else { + Err(parser.errors) + } + } +} + +impl CommentParser<Comments> { + fn parse_checked_line( + &mut self, + fallthrough_to: &mut Option<NonZeroUsize>, + line: Spanned<&[u8]>, + ) -> std::result::Result<(), Utf8Error> { + if let Some(command) = line.strip_prefix(b"//@") { + self.parse_command(command.to_str()?.trim()) + } else if let Some((_, pattern)) = line.split_once_str("//~") { + let (revisions, pattern) = self.parse_revisions(pattern.to_str()?); + self.revisioned(revisions, |this| { + this.parse_pattern(pattern, fallthrough_to) + }) + } else { + *fallthrough_to = None; + for pos in line.find_iter("//") { + let (_, rest) = line.to_str()?.split_at(pos + 2); + for rest in std::iter::once(rest).chain(rest.strip_prefix(" ")) { + if let Some('@' | '~' | '[' | ']' | '^' | '|') = rest.chars().next() { + self.error( + rest.span(), + format!( + "comment looks suspiciously like a test suite command: `{}`\n\ + All `//@` test suite commands must be at the start of the line.\n\ + The `//` must be directly followed by `@` or `~`.", + *rest, + ), + ); + } else { + let mut parser = Self { + errors: vec![], + comments: Comments::default(), + commands: std::mem::take(&mut self.commands), + }; + parser.parse_command(rest); + if parser.errors.is_empty() { + self.error( + rest.span(), + "a compiletest-rs style comment was detected.\n\ + Please use text that could not also be interpreted as a command,\n\ + and prefix all actual commands with `//@`", + ); + } + self.commands = parser.commands; + } + } + } + } + Ok(()) + } +} + +impl<CommentsType> CommentParser<CommentsType> { + fn error(&mut self, span: Span, s: impl Into<String>) { + self.errors.push(Error::InvalidComment { + msg: s.into(), + span, + }); + } + + fn check(&mut self, span: Span, cond: bool, s: impl Into<String>) { + if !cond { + self.error(span, s); + } + } + + fn check_some<T>(&mut self, span: Span, opt: Option<T>, s: impl Into<String>) -> Option<T> { + self.check(span, opt.is_some(), s); + opt + } +} + +impl CommentParser<Comments> { + fn parse_command(&mut self, command: Spanned<&str>) { + let (revisions, command) = self.parse_revisions(command); + + // Commands are letters or dashes, grab everything until the first character that is neither of those. + let (command, args) = match command + .char_indices() + .find_map(|(i, c)| (!c.is_alphanumeric() && c != '-' && c != '_').then_some(i)) + { + None => (command, Spanned::new("", command.span().shrink_to_end())), + Some(i) => { + let (command, args) = command.split_at(i); + // Commands are separated from their arguments by ':' or ' ' + let next = args + .chars() + .next() + .expect("the `position` above guarantees that there is at least one char"); + self.check( + args.span().shrink_to_start(), + next == ':', + "test command must be followed by `:` (or end the line)", + ); + (command, args.split_at(next.len_utf8()).1.trim()) + } + }; + + if *command == "revisions" { + self.check( + revisions.span(), + revisions.is_empty(), + "revisions cannot be declared under a revision", + ); + self.check( + revisions.span(), + self.revisions.is_none(), + "cannot specify `revisions` twice", + ); + self.revisions = Some(args.split_whitespace().map(|s| s.to_string()).collect()); + return; + } + self.revisioned(revisions, |this| this.parse_command(command, args)); + } + + fn revisioned( + &mut self, + revisions: Spanned<Vec<String>>, + f: impl FnOnce(&mut CommentParser<&mut Revisioned>), + ) { + let span = revisions.span(); + let revisions = revisions.into_inner(); + let mut this = CommentParser { + errors: std::mem::take(&mut self.errors), + commands: std::mem::take(&mut self.commands), + comments: self + .revisioned + .entry(revisions) + .or_insert_with(|| Revisioned { + span, + ignore: Default::default(), + only: Default::default(), + stderr_per_bitwidth: Default::default(), + compile_flags: Default::default(), + env_vars: Default::default(), + normalize_stderr: Default::default(), + normalize_stdout: Default::default(), + error_in_other_files: Default::default(), + error_matches: Default::default(), + require_annotations_for_level: Default::default(), + aux_builds: Default::default(), + edition: Default::default(), + mode: Default::default(), + needs_asm_support: Default::default(), + no_rustfix: Default::default(), + }), + }; + f(&mut this); + let CommentParser { + errors, commands, .. + } = this; + self.commands = commands; + self.errors = errors; + } +} + +impl CommentParser<&mut Revisioned> { + fn parse_normalize_test( + &mut self, + args: Spanned<&str>, + mode: &str, + ) -> Option<(Regex, Vec<u8>)> { + let (from, rest) = self.parse_str(args); + + let to = match rest.strip_prefix("->") { + Some(v) => v, + None => { + self.error( + rest.span(), + format!( + "normalize-{mode}-test needs a pattern and replacement separated by `->`" + ), + ); + return None; + } + } + .trim_start(); + let (to, rest) = self.parse_str(to); + + self.check( + rest.span(), + rest.is_empty(), + "trailing text after pattern replacement", + ); + + let regex = self.parse_regex(from)?; + Some((regex, to.as_bytes().to_owned())) + } + + fn commands() -> HashMap<&'static str, CommandParserFunc> { + let mut commands = HashMap::<_, CommandParserFunc>::new(); + macro_rules! commands { + ($($name:expr => ($this:ident, $args:ident, $span:ident)$block:block)*) => { + $(commands.insert($name, |$this, $args, $span| { + $block + });)* + }; + } + commands! { + "compile-flags" => (this, args, _span){ + if let Some(parsed) = comma::parse_command(*args) { + this.compile_flags.extend(parsed); + } else { + this.error(args.span(), format!("`{}` contains an unclosed quotation mark", *args)); + } + } + "rustc-env" => (this, args, _span){ + for env in args.split_whitespace() { + if let Some((k, v)) = this.check_some( + args.span(), + env.split_once('='), + "environment variables must be key/value pairs separated by a `=`", + ) { + this.env_vars.push((k.to_string(), v.to_string())); + } + } + } + "normalize-stderr-test" => (this, args, _span){ + if let Some(res) = this.parse_normalize_test(args, "stderr") { + this.normalize_stderr.push(res) + } + } + "normalize-stdout-test" => (this, args, _span){ + if let Some(res) = this.parse_normalize_test(args, "stdout") { + this.normalize_stdout.push(res) + } + } + "error-pattern" => (this, _args, span){ + this.error(span, "`error-pattern` has been renamed to `error-in-other-file`"); + } + "error-in-other-file" => (this, args, _span){ + let args = args.trim(); + let pat = this.parse_error_pattern(args); + this.error_in_other_files.push(pat); + } + "stderr-per-bitwidth" => (this, _args, span){ + // args are ignored (can be used as comment) + this.check( + span, + !this.stderr_per_bitwidth, + "cannot specify `stderr-per-bitwidth` twice", + ); + this.stderr_per_bitwidth = true; + } + "run-rustfix" => (this, _args, span){ + this.error(span, "rustfix is now ran by default when applicable suggestions are found"); + } + "no-rustfix" => (this, _args, span){ + // args are ignored (can be used as comment) + let prev = this.no_rustfix.set((), span); + this.check( + span, + prev.is_none(), + "cannot specify `no-rustfix` twice", + ); + } + "needs-asm-support" => (this, _args, span){ + // args are ignored (can be used as comment) + this.check( + span, + !this.needs_asm_support, + "cannot specify `needs-asm-support` twice", + ); + this.needs_asm_support = true; + } + "aux-build" => (this, args, _span){ + let name = match args.split_once(":") { + Some((name, rest)) => { + this.error(rest.span(), "proc macros are now auto-detected, you can remove the `:proc-macro` after the file name"); + name + }, + None => args, + }; + this.aux_builds.push(name.map(Into::into)); + } + "edition" => (this, args, span){ + let prev = this.edition.set((*args).into(), args.span()); + this.check(span, prev.is_none(), "cannot specify `edition` twice"); + } + "check-pass" => (this, _args, span){ + let prev = this.mode.set(Mode::Pass, span); + // args are ignored (can be used as comment) + this.check( + span, + prev.is_none(), + "cannot specify test mode changes twice", + ); + } + "run" => (this, args, span){ + this.check( + span, + this.mode.is_none(), + "cannot specify test mode changes twice", + ); + let mut set = |exit_code| this.mode.set(Mode::Run { exit_code }, args.span()); + if args.is_empty() { + set(0); + } else { + match args.parse() { + Ok(exit_code) => {set(exit_code);}, + Err(err) => this.error(args.span(), err.to_string()), + } + } + } + "require-annotations-for-level" => (this, args, span){ + let args = args.trim(); + let prev = match args.parse() { + Ok(it) => this.require_annotations_for_level.set(it, args.span()), + Err(msg) => { + this.error(args.span(), msg); + None + }, + }; + + this.check( + span, + prev.is_none(), + "cannot specify `require-annotations-for-level` twice", + ); + } + } + commands + } + + fn parse_command(&mut self, command: Spanned<&str>, args: Spanned<&str>) { + if let Some(command_handler) = self.commands.get(*command) { + command_handler(self, args, command.span()); + } else if let Some(s) = command.strip_prefix("ignore-") { + // args are ignored (can be used as comment) + match Condition::parse(*s) { + Ok(cond) => self.ignore.push(cond), + Err(msg) => self.error(s.span(), msg), + } + } else if let Some(s) = command.strip_prefix("only-") { + // args are ignored (can be used as comment) + match Condition::parse(*s) { + Ok(cond) => self.only.push(cond), + Err(msg) => self.error(s.span(), msg), + } + } else { + let best_match = self + .commands + .keys() + .min_by_key(|key| levenshtein::levenshtein(key, *command)) + .unwrap(); + self.error( + command.span(), + format!( + "`{}` is not a command known to `ui_test`, did you mean `{best_match}`?", + *command + ), + ); + } + } +} + +impl<CommentsType> CommentParser<CommentsType> { + fn parse_regex(&mut self, regex: Spanned<&str>) -> Option<Regex> { + match Regex::new(*regex) { + Ok(regex) => Some(regex), + Err(err) => { + self.error(regex.span(), format!("invalid regex: {err:?}")); + None + } + } + } + + /// Parses a string literal. `s` has to start with `"`; everything until the next `"` is + /// returned in the first component. `\` can be used to escape arbitrary character. + /// Second return component is the rest of the string with leading whitespace removed. + fn parse_str<'a>(&mut self, s: Spanned<&'a str>) -> (Spanned<&'a str>, Spanned<&'a str>) { + match s.strip_prefix("\"") { + Some(s) => { + let mut escaped = false; + for (i, c) in s.char_indices() { + if escaped { + // Accept any character as literal after a `\`. + escaped = false; + } else if c == '"' { + let (a, b) = s.split_at(i); + let b = b.split_at(1).1; + return (a, b.trim_start()); + } else { + escaped = c == '\\'; + } + } + self.error(s.span(), format!("no closing quotes found for {}", *s)); + (s, Spanned::new("", s.span())) + } + None => { + if s.is_empty() { + self.error(s.span(), "expected quoted string, but found end of line") + } else { + self.error( + s.span(), + format!("expected `\"`, got `{}`", s.chars().next().unwrap()), + ) + } + (s, Spanned::new("", s.span())) + } + } + } + + // parse something like \[[a-z]+(,[a-z]+)*\] + fn parse_revisions<'a>( + &mut self, + pattern: Spanned<&'a str>, + ) -> (Spanned<Vec<String>>, Spanned<&'a str>) { + match pattern.strip_prefix("[") { + Some(s) => { + // revisions + let end = s.char_indices().find_map(|(i, c)| match c { + ']' => Some(i), + _ => None, + }); + let Some(end) = end else { + self.error(s.span(), "`[` without corresponding `]`"); + return ( + Spanned::new(vec![], pattern.span().shrink_to_start()), + pattern, + ); + }; + let (revision, pattern) = s.split_at(end); + let revisions = revision.split(',').map(|s| s.trim().to_string()).collect(); + ( + Spanned::new(revisions, revision.span()), + // 1.. because `split_at` includes the separator + pattern.split_at(1).1.trim_start(), + ) + } + _ => ( + Spanned::new(vec![], pattern.span().shrink_to_start()), + pattern, + ), + } + } +} + +impl CommentParser<&mut Revisioned> { + // parse something like (\[[a-z]+(,[a-z]+)*\])?(?P<offset>\||[\^]+)? *(?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*) + fn parse_pattern(&mut self, pattern: Spanned<&str>, fallthrough_to: &mut Option<NonZeroUsize>) { + let (match_line, pattern) = match pattern.chars().next() { + Some('|') => ( + match fallthrough_to { + Some(fallthrough) => *fallthrough, + None => { + self.error(pattern.span(), "`//~|` pattern without preceding line"); + return; + } + }, + pattern.split_at(1).1, + ), + Some('^') => { + let offset = pattern.chars().take_while(|&c| c == '^').count(); + match pattern + .span() + .line_start + .get() + .checked_sub(offset) + .and_then(NonZeroUsize::new) + { + // lines are one-indexed, so a target line of 0 is invalid, but also + // prevented via `NonZeroUsize` + Some(match_line) => (match_line, pattern.split_at(offset).1), + _ => { + self.error(pattern.span(), format!( + "//~^ pattern is trying to refer to {} lines above, but there are only {} lines above", + offset, + pattern.line().get() - 1, + )); + return; + } + } + } + Some(_) => (pattern.span().line_start, pattern), + None => { + self.error(pattern.span(), "no pattern specified"); + return; + } + }; + + let pattern = pattern.trim_start(); + let offset = match pattern.chars().position(|c| !c.is_ascii_alphabetic()) { + Some(offset) => offset, + None => { + self.error(pattern.span(), "pattern without level"); + return; + } + }; + + let (level, pattern) = pattern.split_at(offset); + let level = match (*level).parse() { + Ok(level) => level, + Err(msg) => { + self.error(level.span(), msg); + return; + } + }; + let pattern = match pattern.strip_prefix(":") { + Some(offset) => offset, + None => { + self.error(pattern.span(), "no `:` after level found"); + return; + } + }; + + let pattern = pattern.trim(); + + self.check(pattern.span(), !pattern.is_empty(), "no pattern specified"); + + let pattern = self.parse_error_pattern(pattern); + + *fallthrough_to = Some(match_line); + + self.error_matches.push(ErrorMatch { + pattern, + level, + line: match_line, + }); + } +} + +impl Pattern { + pub(crate) fn matches(&self, message: &str) -> bool { + match self { + Pattern::SubString(s) => message.contains(s), + Pattern::Regex(r) => r.is_match(message.as_bytes()), + } + } +} + +impl<CommentsType> CommentParser<CommentsType> { + fn parse_error_pattern(&mut self, pattern: Spanned<&str>) -> Spanned<Pattern> { + if let Some(regex) = pattern.strip_prefix("/") { + match regex.strip_suffix("/") { + Some(regex) => match self.parse_regex(regex) { + Some(r) => Spanned::new(Pattern::Regex(r), regex.span()), + None => Spanned::new(Pattern::SubString(pattern.to_string()), regex.span()), + }, + None => { + self.error( + regex.span(), + "expected regex pattern due to leading `/`, but found no closing `/`", + ); + Spanned::new(Pattern::SubString(pattern.to_string()), regex.span()) + } + } + } else { + Spanned::new(Pattern::SubString(pattern.to_string()), pattern.span()) + } + } +} diff --git a/vendor/ui_test-0.20.0/src/parser/spanned.rs b/vendor/ui_test-0.20.0/src/parser/spanned.rs new file mode 100644 index 000000000..8c5f98ecd --- /dev/null +++ b/vendor/ui_test-0.20.0/src/parser/spanned.rs @@ -0,0 +1,264 @@ +use std::num::NonZeroUsize; + +use bstr::{ByteSlice, Utf8Error}; + +use crate::rustc_stderr::Span; + +#[derive(Default, Debug, Clone, Copy)] +pub struct MaybeSpanned<T> { + data: T, + span: Option<Span>, +} + +impl<T> std::ops::Deref for MaybeSpanned<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl<T> MaybeSpanned<T> { + /// Values from the `Config` struct don't have lines. + pub fn new_config(data: T) -> Self { + Self { data, span: None } + } + + pub fn span(&self) -> Option<Span> { + self.span + } + + pub fn into_inner(self) -> T { + self.data + } +} + +impl<T> From<Spanned<T>> for MaybeSpanned<T> { + fn from(value: Spanned<T>) -> Self { + Self { + data: value.data, + span: Some(value.span), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Spanned<T> { + data: T, + span: Span, +} + +impl<T> std::ops::Deref for Spanned<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl<'a> Spanned<&'a str> { + pub fn strip_prefix(&self, prefix: &str) -> Option<Self> { + let data = self.data.strip_prefix(prefix)?; + let mut span = self.span; + span.column_start = + NonZeroUsize::new(span.column_start.get() + prefix.chars().count()).unwrap(); + Some(Self { span, data }) + } + + pub fn strip_suffix(&self, suffix: &str) -> Option<Self> { + let data = self.data.strip_suffix(suffix)?; + let mut span = self.span; + span.column_end = + NonZeroUsize::new(span.column_end.get() - suffix.chars().count()).unwrap(); + Some(Self { span, data }) + } + + pub fn trim_start(&self) -> Self { + let data = self.data.trim_start(); + let mut span = self.span; + span.column_start = NonZeroUsize::new( + span.column_start.get() + self.data.chars().count() - data.chars().count(), + ) + .unwrap(); + Self { data, span } + } + + pub fn trim_end(&self) -> Self { + let data = self.data.trim_end(); + let mut span = self.span; + span.column_end = NonZeroUsize::new( + span.column_end.get() - (self.data.chars().count() - data.chars().count()), + ) + .unwrap(); + Self { data, span } + } + + pub fn trim(&self) -> Self { + self.trim_start().trim_end() + } + + pub fn split_at(&self, i: usize) -> (Self, Self) { + let (a, b) = self.data.split_at(i); + ( + Self { + data: a, + span: Span { + column_end: NonZeroUsize::new(self.span.column_start.get() + a.chars().count()) + .unwrap(), + ..self.span + }, + }, + Self { + data: b, + span: Span { + column_start: NonZeroUsize::new( + self.span.column_start.get() + a.chars().count(), + ) + .unwrap(), + ..self.span + }, + }, + ) + } + + pub fn split_once(&self, splitter: &str) -> Option<(Self, Self)> { + let (a, b) = self.data.split_once(splitter)?; + Some(( + Self { + data: a, + span: Span { + column_end: NonZeroUsize::new(self.span.column_start.get() + a.chars().count()) + .unwrap(), + ..self.span + }, + }, + Self { + data: b, + span: Span { + column_start: NonZeroUsize::new( + self.span.column_start.get() + a.chars().count() + splitter.chars().count(), + ) + .unwrap(), + ..self.span + }, + }, + )) + } +} + +impl<'a> Spanned<&'a [u8]> { + pub fn strip_prefix(&self, prefix: &[u8]) -> Option<Self> { + let data = self.data.strip_prefix(prefix)?; + let mut span = self.span; + span.column_start = NonZeroUsize::new(span.column_start.get() + prefix.len()).unwrap(); + Some(Self { span, data }) + } + + pub fn split_once_str(&self, splitter: &str) -> Option<(Self, Self)> { + let (a, b) = self.data.split_once_str(splitter)?; + Some(( + Self { + data: a, + span: Span { + column_end: NonZeroUsize::new(self.span.column_start.get() + a.len()).unwrap(), + ..self.span + }, + }, + Self { + data: b, + span: Span { + column_start: NonZeroUsize::new( + self.span.column_start.get() + a.len() + splitter.len(), + ) + .unwrap(), + ..self.span + }, + }, + )) + } + + pub fn to_str(self) -> Result<Spanned<&'a str>, Utf8Error> { + Ok(Spanned { + data: self.data.to_str()?, + span: self.span, + }) + } +} + +impl<T> Spanned<T> { + pub fn new(data: T, span: Span) -> Self { + Self { data, span } + } + + pub fn line(&self) -> NonZeroUsize { + self.span.line_start + } + + pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Spanned<U> { + Spanned { + data: f(self.data), + span: self.span, + } + } + + pub fn into_inner(self) -> T { + self.data + } + + pub fn span(&self) -> Span { + self.span + } +} + +#[derive(Debug, Clone, Copy)] +pub struct OptWithLine<T>(Option<Spanned<T>>); + +impl<T> std::ops::Deref for OptWithLine<T> { + type Target = Option<Spanned<T>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<T> From<Option<Spanned<T>>> for OptWithLine<T> { + fn from(value: Option<Spanned<T>>) -> Self { + Self(value) + } +} + +impl<T> From<Spanned<T>> for OptWithLine<T> { + fn from(value: Spanned<T>) -> Self { + Self(Some(value)) + } +} + +impl<T> Default for OptWithLine<T> { + fn default() -> Self { + Self(Default::default()) + } +} + +impl<T> OptWithLine<T> { + pub fn new(data: T, span: Span) -> Self { + Self(Some(Spanned::new(data, span))) + } + + /// Tries to set the value if not already set. Returns newly passed + /// value in case there was already a value there. + #[must_use] + pub fn set(&mut self, data: T, span: Span) -> Option<Spanned<T>> { + let new = Spanned::new(data, span); + if self.0.is_some() { + Some(new) + } else { + self.0 = Some(new); + None + } + } + + #[must_use] + pub fn into_inner(self) -> Option<Spanned<T>> { + self.0 + } +} diff --git a/vendor/ui_test-0.20.0/src/parser/tests.rs b/vendor/ui_test-0.20.0/src/parser/tests.rs new file mode 100644 index 000000000..f1b3b4cbf --- /dev/null +++ b/vendor/ui_test-0.20.0/src/parser/tests.rs @@ -0,0 +1,139 @@ +use crate::{ + parser::{Condition, Pattern}, + Error, +}; + +use super::Comments; + +#[test] +fn parse_simple_comment() { + let s = r" +use std::mem; + +fn main() { + let _x: &i32 = unsafe { mem::transmute(16usize) }; //~ ERROR: encountered a dangling reference (address $HEX is unallocated) +} + "; + let comments = Comments::parse(s).unwrap(); + println!("parsed comments: {:#?}", comments); + assert_eq!(comments.revisioned.len(), 1); + let revisioned = &comments.revisioned[&vec![]]; + assert_eq!(revisioned.error_matches[0].pattern.line().get(), 5); + match &*revisioned.error_matches[0].pattern { + Pattern::SubString(s) => { + assert_eq!( + s, + "encountered a dangling reference (address $HEX is unallocated)" + ) + } + other => panic!("expected substring, got {other:?}"), + } +} + +#[test] +fn parse_missing_level() { + let s = r" +use std::mem; + +fn main() { + let _x: &i32 = unsafe { mem::transmute(16usize) }; //~ encountered a dangling reference (address $HEX is unallocated) +} + "; + let errors = Comments::parse(s).unwrap_err(); + println!("parsed comments: {:#?}", errors); + assert_eq!(errors.len(), 1); + match &errors[0] { + Error::InvalidComment { msg, span } if span.line_start.get() == 5 => { + assert_eq!(msg, "unknown level `encountered`") + } + _ => unreachable!(), + } +} + +#[test] +fn parse_slash_slash_at() { + let s = r" +//@ error-in-other-file: foomp +use std::mem; + + "; + let comments = Comments::parse(s).unwrap(); + println!("parsed comments: {:#?}", comments); + assert_eq!(comments.revisioned.len(), 1); + let revisioned = &comments.revisioned[&vec![]]; + let pat = &revisioned.error_in_other_files[0]; + assert_eq!(format!("{:?}", **pat), r#"SubString("foomp")"#); + assert_eq!(pat.line().get(), 2); +} + +#[test] +fn parse_regex_error_pattern() { + let s = r" +//@ error-in-other-file: /foomp/ +use std::mem; + + "; + let comments = Comments::parse(s).unwrap(); + println!("parsed comments: {:#?}", comments); + assert_eq!(comments.revisioned.len(), 1); + let revisioned = &comments.revisioned[&vec![]]; + let pat = &revisioned.error_in_other_files[0]; + assert_eq!(format!("{:?}", **pat), r#"Regex(Regex("foomp"))"#); + assert_eq!(pat.line().get(), 2); +} + +#[test] +fn parse_slash_slash_at_fail() { + let s = r" +//@ error-patttern foomp +use std::mem; + + "; + let errors = Comments::parse(s).unwrap_err(); + println!("parsed comments: {:#?}", errors); + assert_eq!(errors.len(), 2); + match &errors[0] { + Error::InvalidComment { msg, span } if span.line_start.get() == 2 => { + assert!(msg.contains("must be followed by `:`")) + } + _ => unreachable!(), + } + match &errors[1] { + Error::InvalidComment { msg, span } if span.line_start.get() == 2 => { + assert_eq!(msg, "`error-patttern` is not a command known to `ui_test`, did you mean `error-pattern`?"); + } + _ => unreachable!(), + } +} + +#[test] +fn missing_colon_fail() { + let s = r" +//@stderr-per-bitwidth hello +use std::mem; + + "; + let errors = Comments::parse(s).unwrap_err(); + println!("parsed comments: {:#?}", errors); + assert_eq!(errors.len(), 1); + match &errors[0] { + Error::InvalidComment { msg, span } if span.line_start.get() == 2 => { + assert!(msg.contains("must be followed by `:`")) + } + _ => unreachable!(), + } +} + +#[test] +fn parse_x86_64() { + let s = r"//@ only-target-x86_64-unknown-linux"; + let comments = Comments::parse(s).unwrap(); + println!("parsed comments: {:#?}", comments); + assert_eq!(comments.revisioned.len(), 1); + let revisioned = &comments.revisioned[&vec![]]; + assert_eq!(revisioned.only.len(), 1); + match &revisioned.only[0] { + Condition::Target(t) => assert_eq!(t, "x86_64-unknown-linux"), + _ => unreachable!(), + } +} diff --git a/vendor/ui_test-0.20.0/src/rustc_stderr.rs b/vendor/ui_test-0.20.0/src/rustc_stderr.rs new file mode 100644 index 000000000..4a371ce1d --- /dev/null +++ b/vendor/ui_test-0.20.0/src/rustc_stderr.rs @@ -0,0 +1,201 @@ +use bstr::ByteSlice; +use regex::Regex; +use std::{ + num::NonZeroUsize, + path::{Path, PathBuf}, +}; + +#[derive(serde::Deserialize, Debug)] +struct RustcMessage { + rendered: Option<String>, + spans: Vec<RustcSpan>, + level: String, + message: String, + children: Vec<RustcMessage>, +} + +#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) enum Level { + Ice = 5, + Error = 4, + Warn = 3, + Help = 2, + Note = 1, + /// Only used for "For more information about this error, try `rustc --explain EXXXX`". + FailureNote = 0, +} + +#[derive(Debug)] +/// A diagnostic message. +pub struct Message { + pub(crate) level: Level, + pub(crate) message: String, + pub(crate) line_col: Option<Span>, +} + +/// Information about macro expansion. +#[derive(serde::Deserialize, Debug)] +struct Expansion { + span: RustcSpan, +} + +#[derive(serde::Deserialize, Debug)] +struct RustcSpan { + #[serde(flatten)] + line_col: Span, + file_name: PathBuf, + is_primary: bool, + expansion: Option<Box<Expansion>>, +} + +#[derive(serde::Deserialize, Debug, Copy, Clone)] +pub struct Span { + pub line_start: NonZeroUsize, + pub column_start: NonZeroUsize, + pub line_end: NonZeroUsize, + pub column_end: NonZeroUsize, +} + +impl Span { + pub const INVALID: Self = Self { + line_start: NonZeroUsize::MAX, + column_start: NonZeroUsize::MAX, + line_end: NonZeroUsize::MAX, + column_end: NonZeroUsize::MAX, + }; + + pub fn shrink_to_end(self) -> Span { + Self { + line_start: self.line_end, + column_start: self.column_end, + ..self + } + } + + pub fn shrink_to_start(self) -> Span { + Self { + line_end: self.line_start, + column_end: self.column_start, + ..self + } + } +} + +impl std::str::FromStr for Level { + type Err = String; + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "ERROR" | "error" => Ok(Self::Error), + "WARN" | "warning" => Ok(Self::Warn), + "HELP" | "help" => Ok(Self::Help), + "NOTE" | "note" => Ok(Self::Note), + "failure-note" => Ok(Self::FailureNote), + "error: internal compiler error" => Ok(Self::Ice), + _ => Err(format!("unknown level `{s}`")), + } + } +} + +#[derive(Debug)] +pub(crate) struct Diagnostics { + /// Rendered and concatenated version of all diagnostics. + /// This is equivalent to non-json diagnostics. + pub rendered: Vec<u8>, + /// Per line, a list of messages for that line. + pub messages: Vec<Vec<Message>>, + /// Messages not on any line (usually because they are from libstd) + pub messages_from_unknown_file_or_line: Vec<Message>, +} + +impl RustcMessage { + fn line(&self, file: &Path) -> Option<Span> { + let span = |primary| self.spans.iter().find_map(|span| span.line(file, primary)); + span(true).or_else(|| span(false)) + } + + /// Put the message and its children into the line-indexed list. + fn insert_recursive( + self, + file: &Path, + messages: &mut Vec<Vec<Message>>, + messages_from_unknown_file_or_line: &mut Vec<Message>, + line: Option<Span>, + ) { + let line = self.line(file).or(line); + let msg = Message { + level: self.level.parse().unwrap(), + message: self.message, + line_col: line, + }; + if let Some(line) = line { + if messages.len() <= line.line_start.get() { + messages.resize_with(line.line_start.get() + 1, Vec::new); + } + messages[line.line_start.get()].push(msg); + // All other messages go into the general bin, unless they are specifically of the + // "aborting due to X previous errors" variety, as we never want to match those. They + // only count the number of errors and provide no useful information about the tests. + } else if !(msg.message.starts_with("aborting due to") + && msg.message.contains("previous error")) + { + messages_from_unknown_file_or_line.push(msg); + } + for child in self.children { + child.insert_recursive(file, messages, messages_from_unknown_file_or_line, line) + } + } +} + +impl RustcSpan { + /// Returns the most expanded line number *in the given file*, if possible. + fn line(&self, file: &Path, primary: bool) -> Option<Span> { + if let Some(exp) = &self.expansion { + if let Some(line) = exp.span.line(file, primary && !self.is_primary) { + return Some(line); + } + } + ((!primary || self.is_primary) && self.file_name == file).then_some(self.line_col) + } +} + +pub(crate) fn filter_annotations_from_rendered(rendered: &str) -> std::borrow::Cow<'_, str> { + let annotations = Regex::new(r" *//(\[[a-z,]+\])?~.*").unwrap(); + annotations.replace_all(rendered, "") +} + +pub(crate) fn process(file: &Path, stderr: &[u8]) -> Diagnostics { + let mut rendered = Vec::new(); + let mut messages = vec![]; + let mut messages_from_unknown_file_or_line = vec![]; + for (line_number, line) in stderr.lines_with_terminator().enumerate() { + if line.starts_with_str(b"{") { + match serde_json::from_slice::<RustcMessage>(line) { + Ok(msg) => { + rendered.extend( + filter_annotations_from_rendered(msg.rendered.as_ref().unwrap()).as_bytes(), + ); + msg.insert_recursive( + file, + &mut messages, + &mut messages_from_unknown_file_or_line, + None, + ); + } + Err(err) => { + panic!( + "failed to parse rustc JSON output at line {line_number}: {err}: {}", + line.to_str_lossy() + ) + } + } + } else { + // FIXME: do we want to throw interpreter stderr into a separate file? + rendered.extend(line); + } + } + Diagnostics { + rendered, + messages, + messages_from_unknown_file_or_line, + } +} diff --git a/vendor/ui_test-0.20.0/src/status_emitter.rs b/vendor/ui_test-0.20.0/src/status_emitter.rs new file mode 100644 index 000000000..b32235907 --- /dev/null +++ b/vendor/ui_test-0.20.0/src/status_emitter.rs @@ -0,0 +1,972 @@ +//! Variaous schemes for reporting messages during testing or after testing is done. + +use annotate_snippets::{ + display_list::{DisplayList, FormatOptions}, + snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}, +}; +use bstr::ByteSlice; +use colored::Colorize; +use crossbeam_channel::{Sender, TryRecvError}; +use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; + +use crate::{ + github_actions, + parser::Pattern, + rustc_stderr::{Message, Span}, + Error, Errored, Errors, TestOk, TestResult, +}; +use std::{ + collections::HashMap, + fmt::{Debug, Write as _}, + io::Write as _, + num::NonZeroUsize, + panic::RefUnwindSafe, + path::{Path, PathBuf}, + process::Command, + sync::atomic::AtomicBool, + time::Duration, +}; + +/// A generic way to handle the output of this crate. +pub trait StatusEmitter: Sync + RefUnwindSafe { + /// Invoked the moment we know a test will later be run. + /// Useful for progress bars and such. + fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus>; + + /// Create a report about the entire test run at the end. + #[allow(clippy::type_complexity)] + fn finalize( + &self, + failed: usize, + succeeded: usize, + ignored: usize, + filtered: usize, + ) -> Box<dyn Summary>; +} + +/// Information about a specific test run. +pub trait TestStatus: Send + Sync + RefUnwindSafe { + /// Create a copy of this test for a new revision. + fn for_revision(&self, revision: &str) -> Box<dyn TestStatus>; + + /// Invoked before each failed test prints its errors along with a drop guard that can + /// gets invoked afterwards. + fn failed_test<'a>( + &'a self, + cmd: &'a Command, + stderr: &'a [u8], + stdout: &'a [u8], + ) -> Box<dyn Debug + 'a>; + + /// Change the status of the test while it is running to supply some kind of progress + fn update_status(&self, msg: String); + + /// A test has finished, handle the result immediately. + fn done(&self, _result: &TestResult) {} + + /// The path of the test file. + fn path(&self) -> &Path; + + /// The revision, usually an empty string. + fn revision(&self) -> &str; +} + +/// Report a summary at the end of a test run. +pub trait Summary { + /// A test has finished, handle the result. + fn test_failure(&mut self, _status: &dyn TestStatus, _errors: &Errors) {} +} + +impl Summary for () {} + +/// A human readable output emitter. +#[derive(Clone)] +pub struct Text { + sender: Sender<Msg>, + progress: bool, +} + +#[derive(Debug)] +enum Msg { + Pop(String, Option<String>), + Push(String), + Inc, + IncLength, + Finish, + Status(String, String), +} + +impl Text { + fn start_thread() -> Sender<Msg> { + let (sender, receiver) = crossbeam_channel::unbounded(); + std::thread::spawn(move || { + let bars = MultiProgress::new(); + let mut progress = None; + let mut threads: HashMap<String, ProgressBar> = HashMap::new(); + 'outer: loop { + std::thread::sleep(Duration::from_millis(100)); + loop { + match receiver.try_recv() { + Ok(val) => match val { + Msg::Pop(msg, new_msg) => { + let Some(spinner) = threads.remove(&msg) else { + // This can happen when a test was not run at all, because it failed directly during + // comment parsing. + continue; + }; + spinner.set_style( + ProgressStyle::with_template("{prefix} {msg}").unwrap(), + ); + if let Some(new_msg) = new_msg { + bars.remove(&spinner); + let spinner = bars.insert(0, spinner); + spinner.tick(); + spinner.finish_with_message(new_msg); + } else { + spinner.finish_and_clear(); + } + } + Msg::Status(msg, status) => { + threads.get_mut(&msg).unwrap().set_message(status); + } + Msg::Push(msg) => { + let spinner = + bars.add(ProgressBar::new_spinner().with_prefix(msg.clone())); + spinner.set_style( + ProgressStyle::with_template("{prefix} {spinner} {msg}") + .unwrap(), + ); + threads.insert(msg, spinner); + } + Msg::IncLength => { + progress + .get_or_insert_with(|| bars.add(ProgressBar::new(0))) + .inc_length(1); + } + Msg::Inc => { + progress.as_ref().unwrap().inc(1); + } + Msg::Finish => return, + }, + Err(TryRecvError::Disconnected) => break 'outer, + Err(TryRecvError::Empty) => break, + } + } + for spinner in threads.values() { + spinner.tick() + } + if let Some(progress) = &progress { + progress.tick() + } + } + assert_eq!(threads.len(), 0); + if let Some(progress) = progress { + progress.tick(); + assert!(progress.is_finished()); + } + }); + sender + } + + /// Print one line per test that gets run. + pub fn verbose() -> Self { + Self { + sender: Self::start_thread(), + progress: false, + } + } + /// Print a progress bar. + pub fn quiet() -> Self { + Self { + sender: Self::start_thread(), + progress: true, + } + } +} + +struct TextTest { + text: Text, + path: PathBuf, + revision: String, + first: AtomicBool, +} + +impl TextTest { + fn msg(&self) -> String { + if self.revision.is_empty() { + self.path.display().to_string() + } else { + format!("{} ({})", self.path.display(), self.revision) + } + } +} + +impl TestStatus for TextTest { + fn done(&self, result: &TestResult) { + if self.text.progress { + self.text.sender.send(Msg::Inc).unwrap(); + self.text.sender.send(Msg::Pop(self.msg(), None)).unwrap(); + } else { + let result = match result { + Ok(TestOk::Ok) => "ok".green(), + Err(Errored { .. }) => "FAILED".red().bold(), + Ok(TestOk::Ignored) => "ignored (in-test comment)".yellow(), + Ok(TestOk::Filtered) => return, + }; + let old_msg = self.msg(); + let msg = format!("... {result}"); + if ProgressDrawTarget::stdout().is_hidden() { + println!("{old_msg} {msg}"); + std::io::stdout().flush().unwrap(); + } else { + self.text.sender.send(Msg::Pop(old_msg, Some(msg))).unwrap(); + } + } + } + + fn update_status(&self, msg: String) { + self.text.sender.send(Msg::Status(self.msg(), msg)).unwrap(); + } + + fn failed_test<'a>( + &self, + cmd: &Command, + stderr: &'a [u8], + stdout: &'a [u8], + ) -> Box<dyn Debug + 'a> { + println!(); + let path = self.path.display().to_string(); + print!("{}", path.underline().bold()); + let revision = if self.revision.is_empty() { + String::new() + } else { + format!(" (revision `{}`)", self.revision) + }; + print!("{revision}"); + print!(" {}", "FAILED:".red().bold()); + println!(); + println!("command: {cmd:?}"); + println!(); + + #[derive(Debug)] + struct Guard<'a> { + stderr: &'a [u8], + stdout: &'a [u8], + } + impl<'a> Drop for Guard<'a> { + fn drop(&mut self) { + println!("full stderr:"); + std::io::stdout().write_all(self.stderr).unwrap(); + println!(); + println!("full stdout:"); + std::io::stdout().write_all(self.stdout).unwrap(); + println!(); + println!(); + } + } + Box::new(Guard { stderr, stdout }) + } + + fn path(&self) -> &Path { + &self.path + } + + fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> { + assert_eq!(self.revision, ""); + if !self.first.swap(false, std::sync::atomic::Ordering::Relaxed) && self.text.progress { + self.text.sender.send(Msg::IncLength).unwrap(); + } + + let text = Self { + text: self.text.clone(), + path: self.path.clone(), + revision: revision.to_owned(), + first: AtomicBool::new(false), + }; + self.text.sender.send(Msg::Push(text.msg())).unwrap(); + Box::new(text) + } + + fn revision(&self) -> &str { + &self.revision + } +} + +impl StatusEmitter for Text { + fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> { + if self.progress { + self.sender.send(Msg::IncLength).unwrap(); + } + Box::new(TextTest { + text: self.clone(), + path, + revision: String::new(), + first: AtomicBool::new(true), + }) + } + + fn finalize( + &self, + failures: usize, + succeeded: usize, + ignored: usize, + filtered: usize, + ) -> Box<dyn Summary> { + self.sender.send(Msg::Finish).unwrap(); + while !self.sender.is_empty() { + std::thread::sleep(Duration::from_millis(10)); + } + if !ProgressDrawTarget::stdout().is_hidden() { + // The progress bars do not have a trailing newline, so let's + // add it here. + println!(); + } + // Print all errors in a single thread to show reliable output + if failures == 0 { + println!(); + print!("test result: {}.", "ok".green()); + if succeeded > 0 { + print!(" {} passed;", succeeded.to_string().green()); + } + if ignored > 0 { + print!(" {} ignored;", ignored.to_string().yellow()); + } + if filtered > 0 { + print!(" {} filtered out;", filtered.to_string().yellow()); + } + println!(); + println!(); + Box::new(()) + } else { + struct Summarizer { + failures: Vec<String>, + succeeded: usize, + ignored: usize, + filtered: usize, + } + + impl Summary for Summarizer { + fn test_failure(&mut self, status: &dyn TestStatus, errors: &Errors) { + for error in errors { + print_error(error, status.path()); + } + + self.failures.push(if status.revision().is_empty() { + format!(" {}", status.path().display()) + } else { + format!( + " {} (revision {})", + status.path().display(), + status.revision() + ) + }); + } + } + + impl Drop for Summarizer { + fn drop(&mut self) { + println!("{}", "FAILURES:".red().underline().bold()); + for line in &self.failures { + println!("{line}"); + } + println!(); + print!("test result: {}.", "FAIL".red()); + print!(" {} failed;", self.failures.len().to_string().green()); + if self.succeeded > 0 { + print!(" {} passed;", self.succeeded.to_string().green()); + } + if self.ignored > 0 { + print!(" {} ignored;", self.ignored.to_string().yellow()); + } + if self.filtered > 0 { + print!(" {} filtered out;", self.filtered.to_string().yellow()); + } + println!(); + println!(); + } + } + Box::new(Summarizer { + failures: vec![], + succeeded, + ignored, + filtered, + }) + } + } +} + +fn print_error(error: &Error, path: &Path) { + match error { + Error::ExitStatus { + mode, + status, + expected, + } => { + println!("{mode} test got {status}, but expected {expected}") + } + Error::Command { kind, status } => { + println!("{kind} failed with {status}"); + } + Error::PatternNotFound(pattern) => { + let msg = match &**pattern { + Pattern::SubString(s) => { + format!("substring `{s}` {} in stderr output", "not found") + } + Pattern::Regex(r) => { + format!("`/{r}/` does {} stderr output", "not match") + } + }; + create_error( + msg, + &[( + &[("expected because of this pattern", Some(pattern.span()))], + pattern.line(), + )], + path, + ); + } + Error::NoPatternsFound => { + println!("{}", "no error patterns found in fail test".red()); + } + Error::PatternFoundInPassTest => { + println!("{}", "error pattern found in pass test".red()) + } + Error::OutputDiffers { + path: output_path, + actual, + expected, + bless_command, + } => { + println!("{}", "actual output differed from expected".underline()); + println!( + "Execute `{}` to update `{}` to the actual output", + bless_command, + output_path.display() + ); + println!("{}", format!("--- {}", output_path.display()).red()); + println!( + "{}", + format!( + "+++ <{} output>", + output_path.extension().unwrap().to_str().unwrap() + ) + .green() + ); + crate::diff::print_diff(expected, actual); + } + Error::ErrorsWithoutPattern { path, msgs } => { + if let Some(path) = path.as_ref() { + let line = path.line(); + let msgs = msgs + .iter() + .map(|msg| (format!("{:?}: {}", msg.level, msg.message), msg.line_col)) + .collect::<Vec<_>>(); + create_error( + format!("There were {} unmatched diagnostics", msgs.len()), + &[( + &msgs + .iter() + .map(|(msg, lc)| (msg.as_ref(), *lc)) + .collect::<Vec<_>>(), + line, + )], + path, + ); + } else { + println!( + "There were {} unmatched diagnostics that occurred outside the testfile and had no pattern", + msgs.len(), + ); + for Message { + level, + message, + line_col: _, + } in msgs + { + println!(" {level:?}: {message}") + } + } + } + Error::InvalidComment { msg, span } => { + create_error(msg, &[(&[("", Some(*span))], span.line_start)], path) + } + Error::MultipleRevisionsWithResults { kind, lines } => { + let title = format!("multiple {kind} found"); + create_error( + title, + &lines + .iter() + .map(|&line| (&[] as &[_], line)) + .collect::<Vec<_>>(), + path, + ) + } + Error::Bug(msg) => { + println!("A bug in `ui_test` occurred: {msg}"); + } + Error::Aux { + path: aux_path, + errors, + line, + } => { + println!("Aux build from {}:{line} failed", path.display()); + for error in errors { + print_error(error, aux_path); + } + } + Error::Rustfix(error) => { + println!( + "failed to apply suggestions for {} with rustfix: {error}", + path.display() + ); + println!("Add //@no-rustfix to the test file to ignore rustfix suggestions"); + } + } + println!(); +} + +#[allow(clippy::type_complexity)] +fn create_error( + s: impl AsRef<str>, + lines: &[(&[(&str, Option<Span>)], NonZeroUsize)], + file: &Path, +) { + let source = std::fs::read_to_string(file).unwrap(); + let source: Vec<_> = source.split_inclusive('\n').collect(); + let file = file.display().to_string(); + let msg = Snippet { + title: Some(Annotation { + id: None, + annotation_type: AnnotationType::Error, + label: Some(s.as_ref()), + }), + slices: lines + .iter() + .map(|(label, line)| { + let source = source[line.get() - 1]; + let len = source.chars().count(); + Slice { + source, + line_start: line.get(), + origin: Some(&file), + annotations: label + .iter() + .map(|(label, lc)| SourceAnnotation { + range: lc.map_or((0, len - 1), |lc| { + assert_eq!(lc.line_start, *line); + if lc.line_end > lc.line_start { + (lc.column_start.get() - 1, len - 1) + } else if lc.column_start == lc.column_end { + if lc.column_start.get() - 1 == len { + // rustc sometimes produces spans pointing *after* the `\n` at the end of the line, + // but we want to render an annotation at the end. + (lc.column_start.get() - 2, lc.column_start.get() - 1) + } else { + (lc.column_start.get() - 1, lc.column_start.get()) + } + } else { + (lc.column_start.get() - 1, lc.column_end.get() - 1) + } + }), + label, + annotation_type: AnnotationType::Error, + }) + .collect(), + fold: false, + } + }) + .collect(), + footer: vec![], + opt: FormatOptions { + color: colored::control::SHOULD_COLORIZE.should_colorize(), + anonymized_line_numbers: false, + margin: None, + }, + }; + println!("{}", DisplayList::from(msg)); +} + +fn gha_error(error: &Error, test_path: &str, revision: &str) { + match error { + Error::ExitStatus { + mode, + status, + expected, + } => { + github_actions::error( + test_path, + format!("{mode} test{revision} got {status}, but expected {expected}"), + ); + } + Error::Command { kind, status } => { + github_actions::error(test_path, format!("{kind}{revision} failed with {status}")); + } + Error::PatternNotFound(pattern) => { + github_actions::error(test_path, format!("Pattern not found{revision}")) + .line(pattern.line()); + } + Error::NoPatternsFound => { + github_actions::error( + test_path, + format!("no error patterns found in fail test{revision}"), + ); + } + Error::PatternFoundInPassTest => { + github_actions::error( + test_path, + format!("error pattern found in pass test{revision}"), + ); + } + Error::OutputDiffers { + path: output_path, + actual, + expected, + bless_command, + } => { + if expected.is_empty() { + let mut err = github_actions::error( + test_path, + "test generated output, but there was no output file", + ); + writeln!( + err, + "you likely need to bless the tests with `{bless_command}`" + ) + .unwrap(); + return; + } + + let mut line = 1; + for r in + prettydiff::diff_lines(expected.to_str().unwrap(), actual.to_str().unwrap()).diff() + { + use prettydiff::basic::DiffOp::*; + match r { + Equal(s) => { + line += s.len(); + continue; + } + Replace(l, r) => { + let mut err = github_actions::error( + output_path.display().to_string(), + "actual output differs from expected", + ) + .line(NonZeroUsize::new(line + 1).unwrap()); + writeln!(err, "this line was expected to be `{}`", r[0]).unwrap(); + line += l.len(); + } + Remove(l) => { + let mut err = github_actions::error( + output_path.display().to_string(), + "extraneous lines in output", + ) + .line(NonZeroUsize::new(line + 1).unwrap()); + writeln!( + err, + "remove this line and possibly later ones by blessing the test" + ) + .unwrap(); + line += l.len(); + } + Insert(r) => { + let mut err = github_actions::error( + output_path.display().to_string(), + "missing line in output", + ) + .line(NonZeroUsize::new(line + 1).unwrap()); + writeln!(err, "bless the test to create a line containing `{}`", r[0]) + .unwrap(); + // Do not count these lines, they don't exist in the original file and + // would thus mess up the line number. + } + } + } + } + Error::ErrorsWithoutPattern { path, msgs } => { + if let Some(path) = path.as_ref() { + let line = path.line(); + let path = path.display(); + let mut err = + github_actions::error(&path, format!("Unmatched diagnostics{revision}")) + .line(line); + for Message { + level, + message, + line_col: _, + } in msgs + { + writeln!(err, "{level:?}: {message}").unwrap(); + } + } else { + let mut err = github_actions::error( + test_path, + format!("Unmatched diagnostics outside the testfile{revision}"), + ); + for Message { + level, + message, + line_col: _, + } in msgs + { + writeln!(err, "{level:?}: {message}").unwrap(); + } + } + } + Error::InvalidComment { msg, span } => { + let mut err = github_actions::error(test_path, format!("Could not parse comment")) + .line(span.line_start); + writeln!(err, "{msg}").unwrap(); + } + Error::MultipleRevisionsWithResults { kind, lines } => { + github_actions::error(test_path, format!("multiple {kind} found")).line(lines[0]); + } + Error::Bug(_) => {} + Error::Aux { + path: aux_path, + errors, + line, + } => { + github_actions::error(test_path, format!("Aux build failed")).line(*line); + for error in errors { + gha_error(error, &aux_path.display().to_string(), "") + } + } + Error::Rustfix(error) => { + github_actions::error( + test_path, + format!("failed to apply suggestions with rustfix: {error}"), + ); + } + } +} + +/// Emits Github Actions Workspace commands to show the failures directly in the github diff view. +/// If the const generic `GROUP` boolean is `true`, also emit `::group` commands. +pub struct Gha<const GROUP: bool> { + /// Show a specific name for the final summary. + pub name: String, +} + +#[derive(Clone)] +struct PathAndRev<const GROUP: bool> { + path: PathBuf, + revision: String, +} + +impl<const GROUP: bool> TestStatus for PathAndRev<GROUP> { + fn path(&self) -> &Path { + &self.path + } + + fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> { + assert_eq!(self.revision, ""); + Box::new(Self { + path: self.path.clone(), + revision: revision.to_owned(), + }) + } + + fn failed_test(&self, _cmd: &Command, _stderr: &[u8], _stdout: &[u8]) -> Box<dyn Debug> { + if GROUP { + Box::new(github_actions::group(format_args!( + "{}:{}", + self.path.display(), + self.revision + ))) + } else { + Box::new(()) + } + } + + fn revision(&self) -> &str { + &self.revision + } + + fn update_status(&self, _msg: String) {} +} + +impl<const GROUP: bool> StatusEmitter for Gha<GROUP> { + fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> { + Box::new(PathAndRev::<GROUP> { + path, + revision: String::new(), + }) + } + + fn finalize( + &self, + _failures: usize, + succeeded: usize, + ignored: usize, + filtered: usize, + ) -> Box<dyn Summary> { + struct Summarizer<const GROUP: bool> { + failures: Vec<String>, + succeeded: usize, + ignored: usize, + filtered: usize, + name: String, + } + + impl<const GROUP: bool> Summary for Summarizer<GROUP> { + fn test_failure(&mut self, status: &dyn TestStatus, errors: &Errors) { + let revision = if status.revision().is_empty() { + "".to_string() + } else { + format!(" (revision: {})", status.revision()) + }; + for error in errors { + gha_error(error, &status.path().display().to_string(), &revision); + } + self.failures + .push(format!("{}{revision}", status.path().display())); + } + } + impl<const GROUP: bool> Drop for Summarizer<GROUP> { + fn drop(&mut self) { + if let Some(mut file) = github_actions::summary() { + writeln!(file, "### {}", self.name).unwrap(); + for line in &self.failures { + writeln!(file, "* {line}").unwrap(); + } + writeln!(file).unwrap(); + writeln!(file, "| failed | passed | ignored | filtered out |").unwrap(); + writeln!(file, "| --- | --- | --- | --- |").unwrap(); + writeln!( + file, + "| {} | {} | {} | {} |", + self.failures.len(), + self.succeeded, + self.ignored, + self.filtered, + ) + .unwrap(); + } + } + } + + Box::new(Summarizer::<GROUP> { + failures: vec![], + succeeded, + ignored, + filtered, + name: self.name.clone(), + }) + } +} + +impl<T: TestStatus, U: TestStatus> TestStatus for (T, U) { + fn done(&self, result: &TestResult) { + self.0.done(result); + self.1.done(result); + } + + fn failed_test<'a>( + &'a self, + cmd: &'a Command, + stderr: &'a [u8], + stdout: &'a [u8], + ) -> Box<dyn Debug + 'a> { + Box::new(( + self.0.failed_test(cmd, stderr, stdout), + self.1.failed_test(cmd, stderr, stdout), + )) + } + + fn path(&self) -> &Path { + let path = self.0.path(); + assert_eq!(path, self.1.path()); + path + } + + fn revision(&self) -> &str { + let rev = self.0.revision(); + assert_eq!(rev, self.1.revision()); + rev + } + + fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> { + Box::new((self.0.for_revision(revision), self.1.for_revision(revision))) + } + + fn update_status(&self, msg: String) { + self.0.update_status(msg.clone()); + self.1.update_status(msg) + } +} + +impl<T: StatusEmitter, U: StatusEmitter> StatusEmitter for (T, U) { + fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> { + Box::new(( + self.0.register_test(path.clone()), + self.1.register_test(path), + )) + } + + fn finalize( + &self, + failures: usize, + succeeded: usize, + ignored: usize, + filtered: usize, + ) -> Box<dyn Summary> { + Box::new(( + self.1.finalize(failures, succeeded, ignored, filtered), + self.0.finalize(failures, succeeded, ignored, filtered), + )) + } +} + +impl<T: TestStatus + ?Sized> TestStatus for Box<T> { + fn done(&self, result: &TestResult) { + (**self).done(result); + } + + fn path(&self) -> &Path { + (**self).path() + } + + fn revision(&self) -> &str { + (**self).revision() + } + + fn for_revision(&self, revision: &str) -> Box<dyn TestStatus> { + (**self).for_revision(revision) + } + + fn failed_test<'a>( + &'a self, + cmd: &'a Command, + stderr: &'a [u8], + stdout: &'a [u8], + ) -> Box<dyn Debug + 'a> { + (**self).failed_test(cmd, stderr, stdout) + } + + fn update_status(&self, msg: String) { + (**self).update_status(msg) + } +} + +impl<T: StatusEmitter + ?Sized> StatusEmitter for Box<T> { + fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus> { + (**self).register_test(path) + } + + fn finalize( + &self, + failures: usize, + succeeded: usize, + ignored: usize, + filtered: usize, + ) -> Box<dyn Summary> { + (**self).finalize(failures, succeeded, ignored, filtered) + } +} + +impl Summary for (Box<dyn Summary>, Box<dyn Summary>) { + fn test_failure(&mut self, status: &dyn TestStatus, errors: &Errors) { + self.0.test_failure(status, errors); + self.1.test_failure(status, errors); + } +} diff --git a/vendor/ui_test-0.20.0/src/tests.rs b/vendor/ui_test-0.20.0/src/tests.rs new file mode 100644 index 000000000..791027d4f --- /dev/null +++ b/vendor/ui_test-0.20.0/src/tests.rs @@ -0,0 +1,350 @@ +use std::path::{Path, PathBuf}; + +use crate::rustc_stderr::Level; +use crate::rustc_stderr::Message; + +use super::*; + +fn config() -> Config { + Config { + root_dir: PathBuf::from("$RUSTROOT"), + program: CommandBuilder::cmd("cake"), + ..Config::rustc(PathBuf::new()) + } +} + +#[test] +fn issue_2156() { + let s = r" +use std::mem; + +fn main() { + let _x: &i32 = unsafe { mem::transmute(16usize) }; //~ ERROR: encountered a dangling reference (address $HEX is unallocated) +} + "; + let comments = Comments::parse(s).unwrap(); + let mut errors = vec![]; + let config = config(); + let messages = vec![ + vec![], vec![], vec![], vec![], vec![], + vec![ + Message { + message:"Undefined Behavior: type validation failed: encountered a dangling reference (address 0x10 is unallocated)".to_string(), + level: Level::Error, + line_col: None, + } + ] + ]; + check_annotations( + messages, + vec![], + Path::new("moobar"), + &mut errors, + &config, + "", + &comments, + ) + .unwrap(); + match &errors[..] { + [Error::PatternNotFound(pattern), Error::ErrorsWithoutPattern { path, .. }] + if path.as_ref().is_some_and(|p| p.line().get() == 5) && pattern.line().get() == 5 => {} + _ => panic!("{:#?}", errors), + } +} + +#[test] +fn find_pattern() { + let s = r" +use std::mem; + +fn main() { + let _x: &i32 = unsafe { mem::transmute(16usize) }; //~ ERROR: encountered a dangling reference (address 0x10 is unallocated) +} + "; + let comments = Comments::parse(s).unwrap(); + let config = config(); + { + let messages = vec![vec![], vec![], vec![], vec![], vec![], vec![ + Message { + message: "Undefined Behavior: type validation failed: encountered a dangling reference (address 0x10 is unallocated)".to_string(), + level: Level::Error, + line_col: None, + } + ] + ]; + let mut errors = vec![]; + check_annotations( + messages, + vec![], + Path::new("moobar"), + &mut errors, + &config, + "", + &comments, + ) + .unwrap(); + match &errors[..] { + [] => {} + _ => panic!("{:#?}", errors), + } + } + + // only difference to above is a wrong line number + { + let messages = vec![vec![], vec![], vec![], vec![], vec![ + Message { + message: "Undefined Behavior: type validation failed: encountered a dangling reference (address 0x10 is unallocated)".to_string(), + level: Level::Error, + line_col: None, + } + ] + ]; + let mut errors = vec![]; + check_annotations( + messages, + vec![], + Path::new("moobar"), + &mut errors, + &config, + "", + &comments, + ) + .unwrap(); + match &errors[..] { + [Error::PatternNotFound(pattern), Error::ErrorsWithoutPattern { path, .. }] + if path.as_ref().is_some_and(|p| p.line().get() == 4) + && pattern.line().get() == 5 => {} + _ => panic!("not the expected error: {:#?}", errors), + } + } + + // only difference to first is a wrong level + { + let messages = vec![ + vec![], vec![], vec![], vec![], vec![], + vec![ + Message { + message: "Undefined Behavior: type validation failed: encountered a dangling reference (address 0x10 is unallocated)".to_string(), + level: Level::Note, + line_col: None, + } + ] + ]; + let mut errors = vec![]; + check_annotations( + messages, + vec![], + Path::new("moobar"), + &mut errors, + &config, + "", + &comments, + ) + .unwrap(); + match &errors[..] { + // Note no `ErrorsWithoutPattern`, because there are no `//~NOTE` in the test file, so we ignore them + [Error::PatternNotFound(pattern)] if pattern.line().get() == 5 => {} + _ => panic!("not the expected error: {:#?}", errors), + } + } +} + +#[test] +fn duplicate_pattern() { + let s = r" +use std::mem; + +fn main() { + let _x: &i32 = unsafe { mem::transmute(16usize) }; //~ ERROR: encountered a dangling reference (address 0x10 is unallocated) + //~^ ERROR: encountered a dangling reference (address 0x10 is unallocated) +} + "; + let comments = Comments::parse(s).unwrap(); + let config = config(); + let messages = vec![ + vec![], vec![], vec![], vec![], vec![], + vec![ + Message { + message: "Undefined Behavior: type validation failed: encountered a dangling reference (address 0x10 is unallocated)".to_string(), + level: Level::Error, + line_col: None, + } + ] + ]; + let mut errors = vec![]; + check_annotations( + messages, + vec![], + Path::new("moobar"), + &mut errors, + &config, + "", + &comments, + ) + .unwrap(); + match &errors[..] { + [Error::PatternNotFound(pattern)] if pattern.line().get() == 6 => {} + _ => panic!("{:#?}", errors), + } +} + +#[test] +fn missing_pattern() { + let s = r" +use std::mem; + +fn main() { + let _x: &i32 = unsafe { mem::transmute(16usize) }; //~ ERROR: encountered a dangling reference (address 0x10 is unallocated) +} + "; + let comments = Comments::parse(s).unwrap(); + let config = config(); + let messages = vec![ + vec![], vec![], vec![], vec![], vec![], + vec![ + Message { + message: "Undefined Behavior: type validation failed: encountered a dangling reference (address 0x10 is unallocated)".to_string(), + level: Level::Error, + line_col: None, + }, + Message { + message: "Undefined Behavior: type validation failed: encountered a dangling reference (address 0x10 is unallocated)".to_string(), + level: Level::Error, + line_col: None, + } + ] + ]; + let mut errors = vec![]; + check_annotations( + messages, + vec![], + Path::new("moobar"), + &mut errors, + &config, + "", + &comments, + ) + .unwrap(); + match &errors[..] { + [Error::ErrorsWithoutPattern { path, .. }] + if path.as_ref().is_some_and(|p| p.line().get() == 5) => {} + _ => panic!("{:#?}", errors), + } +} + +#[test] +fn missing_warn_pattern() { + let s = r" +use std::mem; + +fn main() { + let _x: &i32 = unsafe { mem::transmute(16usize) }; //~ ERROR: encountered a dangling reference (address 0x10 is unallocated) + //~^ WARN: cake +} + "; + let comments = Comments::parse(s).unwrap(); + let config = config(); + let messages= vec![ + vec![], + vec![], + vec![], + vec![], + vec![], + vec![ + Message { + message: "Undefined Behavior: type validation failed: encountered a dangling reference (address 0x10 is unallocated)".to_string(), + level: Level::Error, + line_col: None, + }, + Message { + message: "kaboom".to_string(), + level: Level::Warn, + line_col: None, + }, + Message { + message: "cake".to_string(), + level: Level::Warn, + line_col: None, + }, + ], + ]; + let mut errors = vec![]; + check_annotations( + messages, + vec![], + Path::new("moobar"), + &mut errors, + &config, + "", + &comments, + ) + .unwrap(); + match &errors[..] { + [Error::ErrorsWithoutPattern { path, msgs, .. }] + if path.as_ref().is_some_and(|p| p.line().get() == 5) => + { + match &msgs[..] { + [Message { + message, + level: Level::Warn, + line_col: _, + }] if message == "kaboom" => {} + _ => panic!("{:#?}", msgs), + } + } + _ => panic!("{:#?}", errors), + } +} + +#[test] +fn missing_implicit_warn_pattern() { + let s = r" +use std::mem; +//@require-annotations-for-level: ERROR +fn main() { + let _x: &i32 = unsafe { mem::transmute(16usize) }; //~ ERROR: encountered a dangling reference (address 0x10 is unallocated) + //~^ WARN: cake +} + "; + let comments = Comments::parse(s).unwrap(); + let config = config(); + let messages = vec![ + vec![], + vec![], + vec![], + vec![], + vec![], + vec![ + Message { + message: "Undefined Behavior: type validation failed: encountered a dangling reference (address 0x10 is unallocated)".to_string(), + level: Level::Error, + line_col: None, + }, + Message { + message: "kaboom".to_string(), + level: Level::Warn, + line_col: None, + }, + Message { + message: "cake".to_string(), + level: Level::Warn, + line_col: None, + }, + ], + ]; + let mut errors = vec![]; + check_annotations( + messages, + vec![], + Path::new("moobar"), + &mut errors, + &config, + "", + &comments, + ) + .unwrap(); + match &errors[..] { + [] => {} + _ => panic!("{:#?}", errors), + } +} |