summaryrefslogtreecommitdiffstats
path: root/vendor/ui_test-0.20.0/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-30 18:31:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-30 18:31:44 +0000
commitc23a457e72abe608715ac76f076f47dc42af07a5 (patch)
tree2772049aaf84b5c9d0ed12ec8d86812f7a7904b6 /vendor/ui_test-0.20.0/src
parentReleasing progress-linux version 1.73.0+dfsg1-1~progress7.99u1. (diff)
downloadrustc-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.rs120
-rw-r--r--vendor/ui_test-0.20.0/src/config.rs272
-rw-r--r--vendor/ui_test-0.20.0/src/config/args.rs90
-rw-r--r--vendor/ui_test-0.20.0/src/dependencies.rs320
-rw-r--r--vendor/ui_test-0.20.0/src/diff.rs164
-rw-r--r--vendor/ui_test-0.20.0/src/error.rs81
-rw-r--r--vendor/ui_test-0.20.0/src/github_actions.rs96
-rw-r--r--vendor/ui_test-0.20.0/src/lib.rs1270
-rw-r--r--vendor/ui_test-0.20.0/src/mode.rs93
-rw-r--r--vendor/ui_test-0.20.0/src/parser.rs822
-rw-r--r--vendor/ui_test-0.20.0/src/parser/spanned.rs264
-rw-r--r--vendor/ui_test-0.20.0/src/parser/tests.rs139
-rw-r--r--vendor/ui_test-0.20.0/src/rustc_stderr.rs201
-rw-r--r--vendor/ui_test-0.20.0/src/status_emitter.rs972
-rw-r--r--vendor/ui_test-0.20.0/src/tests.rs350
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),
+ }
+}