diff --git a/CHANGELOG.md b/CHANGELOG.md index 447afd98..66dd125b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* `Config::custom_comments` +* `Revisioned::custom` + +### Fixed + +### Changed + +* removed `Revisioned::no_rustfix` in favor of turning that into a rustc-specific custom flag +* removed `Revisioned::edition` in favor of turning that into a rustc-specific custom flag +* removed `Revisioned::needs_asm_support` in favor of turning that into a rustc-specific custom flag +* replaced `Mode::Run` with a rustc-specific run flag +* replaced rustfix with a rustc-specific rustfix flag +* replaced `rustfix` fields of `Mode::Fail` and `Mode::Yolo` by instead overwriting the rustc-specific custom flag + +### Removed + +## [0.22.3] - 2024-04-05 + +### Added + * Reexporting `eyre::Result` at the root level ### Fixed diff --git a/Cargo.lock b/Cargo.lock index dd6b1929..450fb2b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -614,7 +614,7 @@ dependencies = [ [[package]] name = "ui_test" -version = "0.22.3" +version = "0.23.0" dependencies = [ "annotate-snippets", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 334c3ec7..e3ad5e1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ui_test" -version = "0.22.3" +version = "0.23.0" edition = "2021" license = "MIT OR Apache-2.0" description = "A test framework for testing rustc diagnostics output" diff --git a/src/config.rs b/src/config.rs index 3837f9e9..7c2a3b16 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,18 +1,22 @@ use regex::bytes::Regex; -use spanned::{Span, Spanned}; +use spanned::Spanned; use crate::{ + core::Flag, dependencies::build_dependencies, filter::Match, - per_test_config::{Comments, Condition}, - CommandBuilder, Mode, RustfixMode, + parser::CommandParserFunc, + per_test_config::{Comments, Condition, Run, TestConfig}, + CommandBuilder, Errored, Mode, RustfixMode, }; pub use color_eyre; use color_eyre::eyre::Result; use std::{ + collections::HashMap, ffi::OsString, num::NonZeroUsize, path::{Path, PathBuf}, + process::{Command, Output}, }; mod args; @@ -55,6 +59,8 @@ pub struct Config { pub filter_exact: bool, /// The default settings settable via `@` comments pub comment_defaults: Comments, + /// Custom comment parsers + pub custom_comments: HashMap<&'static str, CommandParserFunc>, } impl Config { @@ -62,10 +68,70 @@ impl Config { /// `rustc` on the test files. pub fn rustc(root_dir: impl Into) -> Self { let mut comment_defaults = Comments::default(); + + #[derive(Debug)] + struct Edition(String); + + impl Flag for Edition { + fn clone_inner(&self) -> Box { + Box::new(Edition(self.0.clone())) + } + + fn apply(&self, cmd: &mut std::process::Command) { + cmd.arg("--edition").arg(&self.0); + } + } + + #[derive(Debug)] + struct NeedsAsmSupport; + + impl Flag for NeedsAsmSupport { + fn clone_inner(&self) -> Box { + Box::new(NeedsAsmSupport) + } + fn test_condition(&self, config: &Config) -> bool { + let target = config.target.as_ref().unwrap(); + 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| target.contains(arch)) + } + } + + impl Flag for RustfixMode { + fn clone_inner(&self) -> Box { + Box::new(*self) + } + fn post_test_action( + &self, + config: &TestConfig<'_>, + cmd: Command, + output: &Output, + ) -> Result, Errored> { + let global_rustfix = match *config.mode()? { + Mode::Pass | Mode::Panic => RustfixMode::Disabled, + Mode::Fail { .. } | Mode::Yolo => *self, + }; + + if config.run_rustfix(output.clone(), global_rustfix)? { + Ok(None) + } else { + Ok(Some(cmd)) + } + } + } + let _ = comment_defaults .base() - .edition - .set("2021".into(), Span::default()); + .custom + .insert("edition", Spanned::dummy(Box::new(Edition("2021".into())))); + let _ = comment_defaults.base().custom.insert( + "rustfix", + Spanned::dummy(Box::new(RustfixMode::MachineApplicable)), + ); let filters = vec![ (Match::PathBackslash, b"/".to_vec()), #[cfg(windows)] @@ -77,10 +143,9 @@ impl Config { comment_defaults.base().normalize_stdout = filters; comment_defaults.base().mode = Spanned::dummy(Mode::Fail { require_patterns: true, - rustfix: RustfixMode::MachineApplicable, }) .into(); - Self { + let mut config = Self { host: None, target: None, root_dir: root_dir.into(), @@ -100,7 +165,72 @@ impl Config { run_only_ignored: false, filter_exact: false, comment_defaults, - } + custom_comments: Default::default(), + }; + config + .custom_comments + .insert("no-rustfix", |parser, _args, span| { + // args are ignored (can be used as comment) + let prev = parser + .custom + .insert("no-rustfix", Spanned::new(Box::new(()), span.clone())); + parser.check(span, prev.is_none(), "cannot specify `no-rustfix` twice"); + }); + + config + .custom_comments + .insert("edition", |parser, args, span| { + let prev = parser.custom.insert( + "edition", + Spanned::new(Box::new(Edition((*args).into())), args.span()), + ); + parser.check(span, prev.is_none(), "cannot specify `edition` twice"); + }); + + config + .custom_comments + .insert("needs-asm-support", |parser, args, span| { + let prev = parser.custom.insert( + "needs-asm-support", + Spanned::new(Box::new(NeedsAsmSupport), args.span()), + ); + parser.check( + span, + prev.is_none(), + "cannot specify `needs-asm-support` twice", + ); + }); + + config.custom_comments.insert("run", |parser, args, span| { + parser.check( + span.clone(), + parser.mode.is_none(), + "cannot specify test mode changes twice", + ); + let set = |exit_code| { + parser.custom.insert( + "run", + Spanned::new(Box::new(Run { exit_code }), args.span()), + ); + parser.mode = Spanned::new(Mode::Pass, args.span()).into(); + + let prev = parser + .custom + .insert("no-rustfix", Spanned::new(Box::new(()), span.clone())); + parser.check(span, prev.is_none(), "`run` implies `no-rustfix`"); + }; + if args.is_empty() { + set(0); + } else { + match args.content.parse() { + Ok(exit_code) => { + set(exit_code); + } + Err(err) => parser.error(args.span(), err.to_string()), + } + } + }); + config } /// Create a configuration for testing the output of running @@ -108,14 +238,10 @@ impl Config { pub fn cargo(root_dir: impl Into) -> Self { let mut this = Self { program: CommandBuilder::cargo(), + custom_comments: Default::default(), ..Self::rustc(root_dir) }; - this.comment_defaults.base().edition = Default::default(); - this.comment_defaults.base().mode = Spanned::dummy(Mode::Fail { - require_patterns: true, - rustfix: RustfixMode::Disabled, - }) - .into(); + this.comment_defaults.base().custom.clear(); this } @@ -269,18 +395,6 @@ impl Config { .expect("target should have been filled in") } - 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)) - } - pub(crate) fn get_pointer_width(&self) -> u8 { // Taken 1:1 from compiletest-rs fn get_pointer_width(triple: &str) -> u8 { @@ -320,7 +434,7 @@ impl Config { } if comments .for_revision(revision) - .any(|r| r.needs_asm_support && !self.has_asm_support()) + .any(|r| r.custom.values().any(|flag| flag.test_condition(self))) { return self.run_only_ignored; } diff --git a/src/core.rs b/src/core.rs index e36a5fe0..127d6ea5 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,12 +1,15 @@ //! Basic operations useful for building a testsuite +use crate::per_test_config::TestConfig; use crate::test_result::Errored; +use crate::Config; use bstr::ByteSlice as _; use color_eyre::eyre::Result; use crossbeam_channel::unbounded; use crossbeam_channel::Receiver; use crossbeam_channel::Sender; use std::num::NonZeroUsize; +use std::panic::UnwindSafe; use std::path::Component; use std::path::Path; use std::path::Prefix; @@ -119,3 +122,40 @@ pub fn run_and_collect( Ok(()) }) } + +/// Tester-specific flag that gets parsed from `//@` comments. +pub trait Flag: Send + Sync + UnwindSafe + std::fmt::Debug { + /// Clone the boxed value and create a new box. + fn clone_inner(&self) -> Box; + + /// Modify a command to what the flag specifies + fn apply(&self, _cmd: &mut Command) {} + + /// Whether this flag causes a test to be filtered out + fn test_condition(&self, _config: &Config) -> bool { + false + } + + /// Run an action after a test is finished. + /// Returns the `cmd` back if no action was taken. + fn post_test_action( + &self, + _config: &TestConfig<'_>, + cmd: Command, + _output: &Output, + ) -> Result, Errored> { + Ok(Some(cmd)) + } +} + +impl Flag for () { + fn clone_inner(&self) -> Box { + Box::new(()) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_inner() + } +} diff --git a/src/dependencies.rs b/src/dependencies.rs index b08adc69..bec7df1b 100644 --- a/src/dependencies.rs +++ b/src/dependencies.rs @@ -338,7 +338,7 @@ impl<'a> BuildManager<'a> { stderr: err.to_string().into_bytes(), stdout: vec![], })?; - let comments = Comments::parse(&file_contents, config.comment_defaults.clone(), aux_file) + let comments = Comments::parse(&file_contents, config, aux_file) .map_err(|errors| Errored::new(errors, "parse aux comments"))?; assert_eq!( comments.revisions, None, diff --git a/src/error.rs b/src/error.rs index c4cb0767..06d95832 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,6 @@ use crate::{ parser::{Pattern, Span, Spanned}, rustc_stderr::Message, - Mode, }; use std::{num::NonZeroUsize, path::PathBuf, process::ExitStatus}; @@ -12,7 +11,7 @@ pub enum Error { /// Got an invalid exit status for the given mode. ExitStatus { /// The expected mode. - mode: Mode, + mode: String, /// The exit status of the command. status: ExitStatus, /// The expected exit status as set in the file or derived from the mode. diff --git a/src/lib.rs b/src/lib.rs index 0b2227c1..dd776399 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub use color_eyre; use color_eyre::eyre::eyre; +use color_eyre::eyre::Context as _; pub use color_eyre::eyre::Result; pub use core::run_and_collect; pub use core::CrateType; @@ -126,7 +127,9 @@ pub fn test_command(mut config: Config, path: &Path) -> Result { config.fill_host_and_target()?; let extra_args = config.build_dependencies()?; - let comments = Comments::parse_file(config.comment_defaults.clone(), path)? + let content = + std::fs::read(path).wrap_err_with(|| format!("failed to read {}", path.display()))?; + let comments = Comments::parse(&content, &config, path) .map_err(|errors| color_eyre::eyre::eyre!("{errors:#?}"))?; let config = TestConfig { config, @@ -301,12 +304,8 @@ fn parse_and_test_file( mut config: Config, file_contents: Vec, ) -> Result, Errored> { - let comments = Comments::parse( - &file_contents, - config.comment_defaults.clone(), - status.path(), - ) - .map_err(|errors| Errored::new(errors, "parse comments"))?; + let comments = Comments::parse(&file_contents, &config, status.path()) + .map_err(|errors| Errored::new(errors, "parse comments"))?; const EMPTY: &[String] = &[String::new()]; // Run the test for all revisions let revisions = comments.revisions.as_deref().unwrap_or(EMPTY); diff --git a/src/mode.rs b/src/mode.rs index 16d330bb..c2aed4ad 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -24,32 +24,21 @@ impl RustfixMode { 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, - }, + Yolo, } impl Mode { #[allow(clippy::result_large_err)] 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, @@ -59,7 +48,7 @@ impl Mode { Ok(()) } else { Err(Error::ExitStatus { - mode: self, + mode: self.to_string(), status, expected, }) @@ -70,14 +59,12 @@ impl Mode { 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"), + Mode::Yolo => write!(f, "yolo"), } } } diff --git a/src/parser.rs b/src/parser.rs index f625596b..4da12f90 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -8,9 +8,11 @@ use std::{ use bstr::{ByteSlice, Utf8Error}; use regex::bytes::Regex; -use crate::{filter::Match, rustc_stderr::Level, test_result::Errored, Error, Mode}; +use crate::{ + core::Flag, filter::Match, rustc_stderr::Level, test_result::Errored, Config, Error, Mode, +}; -use color_eyre::eyre::{Context, Result}; +use color_eyre::eyre::Result; pub(crate) use spanned::*; @@ -94,13 +96,6 @@ impl Comments { }) } - pub(crate) fn edition(&self, revision: &str) -> Result>, Errored> { - let edition = self - .find_one_for_revision(revision, "`edition` annotations", |r| r.edition.clone())? - .into_inner(); - Ok(edition) - } - /// The comments set for all revisions pub fn base(&mut self) -> &mut Revisioned { self.revisioned.get_mut(&[][..]).unwrap() @@ -125,6 +120,14 @@ impl Comments { })?; Ok(mode) } + + pub(crate) fn apply_custom(&self, revision: &str, cmd: &mut Command) { + for rev in self.for_revision(revision) { + for flag in rev.custom.values() { + flag.content.apply(cmd); + } + } + } } #[derive(Debug, Clone, Default)] @@ -157,20 +160,21 @@ pub struct Revisioned { pub require_annotations_for_level: OptWithLine, /// Files that get built and exposed as dependencies to the current test. pub aux_builds: Vec>, - /// Set the `--edition` flag on the test. - pub edition: OptWithLine, /// The mode this test is being run in. pub mode: OptWithLine, - pub(crate) needs_asm_support: bool, - /// Don't run [`rustfix`] for this test - pub no_rustfix: OptWithLine<()>, /// Prefix added to all diagnostic code matchers. Note this will make it impossible /// match codes which do not contain this prefix. pub diagnostic_code_prefix: OptWithLine, + /// Tester-specific flags. + /// The keys are just labels for overwriting or retrieving the value later. + /// They are mostly used by `Config::custom_comments` handlers, + /// `ui_test` itself only ever looks at the values, not the keys. + pub custom: HashMap<&'static str, Spanned>>, } +/// Main entry point to parsing comments and handling parsing errors. #[derive(Debug)] -struct CommentParser { +pub struct CommentParser { /// The comments being built. comments: T, /// Any errors that ocurred during comment parsing. @@ -179,7 +183,8 @@ struct CommentParser { commands: HashMap<&'static str, CommandParserFunc>, } -type CommandParserFunc = fn(&mut CommentParser<&mut Revisioned>, args: Spanned<&str>, span: Span); +pub type CommandParserFunc = + fn(&mut CommentParser<&mut Revisioned>, args: Spanned<&str>, span: Span); impl std::ops::Deref for CommentParser { type Target = T; @@ -279,29 +284,35 @@ enum ParsePatternResult { } impl Comments { - pub(crate) fn parse_file( - comments: Comments, - path: &Path, - ) -> Result>> { - let content = - std::fs::read(path).wrap_err_with(|| format!("failed to read {}", path.display()))?; - Ok(Self::parse(&content, comments, path)) - } - /// Parse comments in `content`. /// `path` is only used to emit diagnostics if parsing fails. pub(crate) fn parse( content: &(impl AsRef<[u8]> + ?Sized), - comments: Comments, + config: &Config, file: &Path, ) -> std::result::Result> { - let mut parser = CommentParser { - comments, + CommentParser::new(config).parse(content, file) + } +} + +impl CommentParser { + fn new(config: &Config) -> Self { + let mut this = Self { + comments: config.comment_defaults.clone(), errors: vec![], - commands: CommentParser::<_>::commands(), + commands: Self::commands(), }; + this.commands + .extend(config.custom_comments.iter().map(|(&k, &v)| (k, v))); + this + } - let defaults = std::mem::take(parser.comments.revisioned.get_mut(&[][..]).unwrap()); + fn parse( + mut self, + content: &(impl AsRef<[u8]> + ?Sized), + file: &Path, + ) -> std::result::Result> { + let defaults = std::mem::take(self.comments.revisioned.get_mut(&[][..]).unwrap()); let mut delayed_fallthrough = Vec::new(); let mut fallthrough_to = None; // The line that a `|` will refer to. @@ -316,7 +327,7 @@ impl Comments { col_start: NonZeroUsize::new(1).unwrap(), col_end: NonZeroUsize::new(line.chars().count() + 1).unwrap(), }; - match parser.parse_checked_line(fallthrough_to, Spanned::new(line, span)) { + match self.parse_checked_line(fallthrough_to, Spanned::new(line, span)) { Ok(ParsePatternResult::Other) => { fallthrough_to = None; } @@ -328,14 +339,14 @@ impl Comments { } Ok(ParsePatternResult::ErrorBelow { span, match_line }) => { if fallthrough_to.is_some() { - parser.error( + self.error( span, "`//~v` comment immediately following a `//~^` comment chain", ); } for (span, line, idx) in delayed_fallthrough.drain(..) { - if let Some(rev) = parser + if let Some(rev) = self .comments .revisioned .values_mut() @@ -343,18 +354,18 @@ impl Comments { { rev.error_matches[idx].line = match_line; } else { - parser.error(span, "`//~|` comment not attached to anchoring matcher"); + self.error(span, "`//~|` comment not attached to anchoring matcher"); } } } - Err(e) => parser.error(e.span, format!("Comment is not utf8: {:?}", e.content)), + Err(e) => self.error(e.span, format!("Comment is not utf8: {:?}", e.content)), } } - if let Some(revisions) = &parser.comments.revisions { - for (key, revisioned) in &parser.comments.revisioned { + if let Some(revisions) = &self.comments.revisions { + for (key, revisioned) in &self.comments.revisioned { for rev in key { if !revisions.contains(rev) { - parser.errors.push(Error::InvalidComment { + self.errors.push(Error::InvalidComment { msg: format!("the revision `{rev}` is not known"), span: revisioned.span.clone(), }) @@ -362,9 +373,9 @@ impl Comments { } } } else { - for (key, revisioned) in &parser.comments.revisioned { + for (key, revisioned) in &self.comments.revisioned { if !key.is_empty() { - parser.errors.push(Error::InvalidComment { + self.errors.push(Error::InvalidComment { msg: "there are no revisions in this test".into(), span: revisioned.span.clone(), }) @@ -372,14 +383,14 @@ impl Comments { } } - for revisioned in parser.comments.revisioned.values() { + for revisioned in self.comments.revisioned.values() { for m in &revisioned.error_matches { if m.line.get() > last_line { let span = match &m.kind { ErrorMatchKind::Pattern { pattern, .. } => pattern.span(), ErrorMatchKind::Code(code) => code.span(), }; - parser.errors.push(Error::InvalidComment { + self.errors.push(Error::InvalidComment { msg: format!( "//~v pattern is trying to refer to line {}, but the file only has {} lines", m.line.get(), @@ -392,7 +403,7 @@ impl Comments { } for (span, ..) in delayed_fallthrough { - parser.error(span, "`//~|` comment not attached to anchoring matcher"); + self.error(span, "`//~|` comment not attached to anchoring matcher"); } let Revisioned { @@ -408,12 +419,10 @@ impl Comments { error_matches, require_annotations_for_level, aux_builds, - edition, mode, - needs_asm_support, - no_rustfix, diagnostic_code_prefix, - } = parser.comments.base(); + custom, + } = self.comments.base(); if span.is_dummy() { *span = defaults.span; } @@ -430,24 +439,21 @@ impl Comments { if require_annotations_for_level.is_none() { *require_annotations_for_level = defaults.require_annotations_for_level; } - if edition.is_none() { - *edition = defaults.edition; - } if mode.is_none() { *mode = defaults.mode; } - if no_rustfix.is_none() { - *no_rustfix = defaults.no_rustfix; - } if diagnostic_code_prefix.is_none() { *diagnostic_code_prefix = defaults.diagnostic_code_prefix; } - *needs_asm_support |= defaults.needs_asm_support; - if parser.errors.is_empty() { - Ok(parser.comments) + for (k, v) in defaults.custom { + custom.entry(k).or_insert(v); + } + + if self.errors.is_empty() { + Ok(self.comments) } else { - Err(parser.errors) + Err(self.errors) } } } @@ -511,14 +517,14 @@ impl CommentParser { } impl CommentParser { - fn error(&mut self, span: Span, s: impl Into) { + pub fn error(&mut self, span: Span, s: impl Into) { self.errors.push(Error::InvalidComment { msg: s.into(), span, }); } - fn check(&mut self, span: Span, cond: bool, s: impl Into) { + pub fn check(&mut self, span: Span, cond: bool, s: impl Into) { if !cond { self.error(span, s); } @@ -638,7 +644,9 @@ impl CommentParser<&mut Revisioned> { let regex = self.parse_regex(from)?.content; Some((regex, to.as_bytes().to_owned())) } +} +impl CommentParser { fn commands() -> HashMap<&'static str, CommandParserFunc> { let mut commands = HashMap::<_, CommandParserFunc>::new(); macro_rules! commands { @@ -697,24 +705,6 @@ impl CommentParser<&mut Revisioned> { "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.clone()); - 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)) => { @@ -725,10 +715,6 @@ impl CommentParser<&mut Revisioned> { }; 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.clone()); // args are ignored (can be used as comment) @@ -738,22 +724,6 @@ impl CommentParser<&mut Revisioned> { "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.content.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.content.parse() { @@ -773,7 +743,9 @@ impl CommentParser<&mut Revisioned> { } commands } +} +impl CommentParser<&mut Revisioned> { 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()); diff --git a/src/parser/tests.rs b/src/parser/tests.rs index 2aaa345a..c3b69856 100644 --- a/src/parser/tests.rs +++ b/src/parser/tests.rs @@ -2,7 +2,7 @@ use std::path::Path; use crate::{ parser::{Condition, ErrorMatchKind, Pattern}, - Error, + Config, Error, }; use super::Comments; @@ -16,7 +16,7 @@ fn main() { let _x: &i32 = unsafe { mem::transmute(16usize) }; //~ ERROR: encountered a dangling reference (address $HEX is unallocated) } "; - let comments = Comments::parse(s, Comments::default(), Path::new("")).unwrap(); + let comments = Comments::parse(s, &Config::rustc(""), Path::new("")).unwrap(); println!("parsed comments: {:#?}", comments); assert_eq!(comments.revisioned.len(), 1); let revisioned = &comments.revisioned[&vec![]]; @@ -42,7 +42,7 @@ fn main() { let _x: i32 = 0u32; //~ E0308 } "; - let comments = Comments::parse(s, Comments::default(), Path::new("")).unwrap(); + let comments = Comments::parse(s, &Config::rustc(""), Path::new("")).unwrap(); println!("parsed comments: {:#?}", comments); assert_eq!(comments.revisioned.len(), 1); let revisioned = &comments.revisioned[&vec![]]; @@ -62,7 +62,7 @@ fn main() { let _x: &i32 = unsafe { mem::transmute(16usize) }; //~ encountered a dangling reference (address $HEX is unallocated) } "; - let errors = Comments::parse(s, Comments::default(), Path::new("")).unwrap_err(); + let errors = Comments::parse(s, &Config::rustc(""), Path::new("")).unwrap_err(); println!("parsed comments: {:#?}", errors); assert_eq!(errors.len(), 1); match &errors[0] { @@ -80,7 +80,7 @@ fn parse_slash_slash_at() { use std::mem; "; - let comments = Comments::parse(s, Comments::default(), Path::new("")).unwrap(); + let comments = Comments::parse(s, &Config::rustc(""), Path::new("")).unwrap(); println!("parsed comments: {:#?}", comments); assert_eq!(comments.revisioned.len(), 1); let revisioned = &comments.revisioned[&vec![]]; @@ -96,7 +96,7 @@ fn parse_regex_error_pattern() { use std::mem; "; - let comments = Comments::parse(s, Comments::default(), Path::new("")).unwrap(); + let comments = Comments::parse(s, &Config::rustc(""), Path::new("")).unwrap(); println!("parsed comments: {:#?}", comments); assert_eq!(comments.revisioned.len(), 1); let revisioned = &comments.revisioned[&vec![]]; @@ -112,7 +112,7 @@ fn parse_slash_slash_at_fail() { use std::mem; "; - let errors = Comments::parse(s, Comments::default(), Path::new("")).unwrap_err(); + let errors = Comments::parse(s, &Config::rustc(""), Path::new("")).unwrap_err(); println!("parsed comments: {:#?}", errors); assert_eq!(errors.len(), 2); match &errors[0] { @@ -136,7 +136,7 @@ fn missing_colon_fail() { use std::mem; "; - let errors = Comments::parse(s, Comments::default(), Path::new("")).unwrap_err(); + let errors = Comments::parse(s, &Config::rustc(""), Path::new("")).unwrap_err(); println!("parsed comments: {:#?}", errors); assert_eq!(errors.len(), 1); match &errors[0] { @@ -150,7 +150,7 @@ use std::mem; #[test] fn parse_x86_64() { let s = r"//@ only-target-x86_64-unknown-linux"; - let comments = Comments::parse(s, Comments::default(), Path::new("")).unwrap(); + let comments = Comments::parse(s, &Config::rustc(""), Path::new("")).unwrap(); println!("parsed comments: {:#?}", comments); assert_eq!(comments.revisioned.len(), 1); let revisioned = &comments.revisioned[&vec![]]; diff --git a/src/per_test_config.rs b/src/per_test_config.rs index 28c1e33e..d4e7c161 100644 --- a/src/per_test_config.rs +++ b/src/per_test_config.rs @@ -12,6 +12,7 @@ use std::process::{Command, Output}; use spanned::{Span, Spanned}; +use crate::core::Flag; use crate::dependencies::{Build, BuildManager}; pub use crate::parser::{Comments, Condition, Revisioned}; use crate::parser::{ErrorMatch, ErrorMatchKind, OptWithLine}; @@ -23,15 +24,18 @@ use crate::{ RustfixMode, }; -pub(crate) struct TestConfig<'a> { +/// All information needed to run a single test +pub struct TestConfig<'a> { + /// The generic config for all tests pub config: Config, - pub revision: &'a str, - pub comments: &'a Comments, + pub(crate) revision: &'a str, + pub(crate) comments: &'a Comments, + /// The path to the current file pub path: &'a Path, } impl TestConfig<'_> { - pub fn patch_out_dir(&mut self) { + pub(crate) fn patch_out_dir(&mut self) { // 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(self.path.parent().unwrap(), &self.config.out_dir); @@ -39,6 +43,7 @@ impl TestConfig<'_> { self.config.out_dir.extend(relative); } + /// Create a file extension that includes the current revision if necessary. pub fn extension(&self, extension: &str) -> String { if self.revision.is_empty() { extension.to_string() @@ -47,15 +52,12 @@ impl TestConfig<'_> { } } + /// The test's mode after applying all comments pub fn mode(&self) -> Result, Errored> { self.comments.mode(self.revision) } - pub fn edition(&self) -> Result>, Errored> { - self.comments.edition(self.revision) - } - - pub fn find_one<'a, T: 'a>( + pub(crate) fn find_one<'a, T: 'a>( &'a self, kind: &str, f: impl Fn(&'a Revisioned) -> OptWithLine, @@ -63,18 +65,19 @@ impl TestConfig<'_> { self.comments.find_one_for_revision(self.revision, kind, f) } + /// All comments that apply to the current test. pub fn comments(&self) -> impl Iterator { self.comments.for_revision(self.revision) } - pub fn collect<'a, T, I: Iterator, R: FromIterator>( + pub(crate) fn collect<'a, T, I: Iterator, R: FromIterator>( &'a self, f: impl Fn(&'a Revisioned) -> I, ) -> R { self.comments().flat_map(f).collect() } - pub fn build_command(&self) -> Result { + pub(crate) fn build_command(&self) -> Result { let TestConfig { config, revision, @@ -92,11 +95,8 @@ impl TestConfig<'_> { { cmd.arg(arg); } - let edition = comments.edition(revision)?; - if let Some(edition) = edition { - cmd.arg("--edition").arg(&*edition); - } + comments.apply_custom(revision, &mut cmd); if let Some(target) = &config.target { // Adding a `--target` arg to calls to Cargo will cause target folders @@ -120,7 +120,7 @@ impl TestConfig<'_> { Ok(cmd) } - pub fn output_path(&self, kind: &str) -> PathBuf { + pub(crate) fn output_path(&self, kind: &str) -> PathBuf { let ext = self.extension(kind); if self.comments().any(|r| r.stderr_per_bitwidth) { return self @@ -130,7 +130,7 @@ impl TestConfig<'_> { self.path.with_extension(ext) } - pub fn normalize(&self, text: &[u8], kind: &'static str) -> Vec { + pub(crate) fn normalize(&self, text: &[u8], kind: &'static str) -> Vec { let mut text = text.to_owned(); for (from, to) in self.comments().flat_map(|r| match kind { @@ -144,14 +144,19 @@ impl TestConfig<'_> { text } - pub fn check_test_output(&self, errors: &mut Errors, stdout: &[u8], stderr: &[u8]) { + pub(crate) fn check_test_output(&self, errors: &mut Errors, stdout: &[u8], stderr: &[u8]) { // Check output files (if any) // Check output files against actual output self.check_output(stderr, errors, "stderr"); self.check_output(stdout, errors, "stdout"); } - pub fn check_output(&self, output: &[u8], errors: &mut Errors, kind: &'static str) -> PathBuf { + pub(crate) fn check_output( + &self, + output: &[u8], + errors: &mut Errors, + kind: &'static str, + ) -> PathBuf { let output = self.normalize(output, kind); let path = self.output_path(kind); match &self.config.output_conflict_handling { @@ -178,14 +183,13 @@ impl TestConfig<'_> { path } - pub fn check_test_result( + fn check_test_result( &self, command: Command, - mode: Mode, output: Output, ) -> Result<(Command, Output), Errored> { let mut errors = vec![]; - errors.extend(mode.ok(output.status).err()); + errors.extend(self.mode()?.ok(output.status).err()); // Always remove annotation comments from stderr. let diagnostics = rustc_stderr::process(self.path, &output.stderr); self.check_test_output(&mut errors, &output.stdout, &diagnostics.rendered); @@ -207,7 +211,7 @@ impl TestConfig<'_> { } } - pub fn check_annotations( + pub(crate) fn check_annotations( &self, mut messages: Vec>, mut messages_from_unknown_file_or_line: Vec, @@ -368,7 +372,7 @@ impl TestConfig<'_> { Ok(()) } - pub fn build_aux_files( + pub(crate) fn build_aux_files( &self, aux_dir: &Path, build_manager: &BuildManager<'_>, @@ -426,7 +430,7 @@ impl TestConfig<'_> { Ok(extra_args) } - pub fn run_test(mut self, build_manager: &BuildManager<'_>) -> TestResult { + pub(crate) fn run_test(mut self, build_manager: &BuildManager<'_>) -> TestResult { let extra_args = self.build_aux_files( &self.path.parent().unwrap().join("auxiliary"), build_manager, @@ -434,8 +438,9 @@ impl TestConfig<'_> { self.patch_out_dir(); + self.config.program.args.extend(extra_args); + let mut cmd = self.build_command()?; - cmd.args(&extra_args); let stdin = self.path.with_extension(self.extension("stdin")); if stdin.exists() { cmd.stdin(std::fs::File::open(stdin).unwrap()); @@ -443,28 +448,24 @@ impl TestConfig<'_> { let (cmd, output) = crate::core::run_command(cmd)?; - let mode = self.mode()?; - let (cmd, output) = self.check_test_result( - cmd, - match *mode { - Mode::Run { .. } => Mode::Pass, - _ => *mode, - }, - output, - )?; + let (mut cmd, output) = self.check_test_result(cmd, output)?; - if let Mode::Run { .. } = *mode { - self.run_test_binary(mode, cmd) - } else { - self.run_rustfix(output, *mode, extra_args)?; - Ok(TestOk::Ok) + for rev in self.comments() { + for custom in rev.custom.values() { + if let Some(c) = custom.content.post_test_action(&self, cmd, &output)? { + cmd = c; + } else { + return Ok(TestOk::Ok); + } + } } + Ok(TestOk::Ok) } - fn run_test_binary(self, mode: Spanned, mut cmd: Command) -> TestResult { + fn run_test_binary(&self, mut cmd: Command, exit_code: i32) -> TestResult { let revision = self.extension("run"); let config = TestConfig { - config: self.config, + config: self.config.clone(), revision: &revision, comments: self.comments, path: self.path, @@ -491,7 +492,14 @@ impl TestConfig<'_> { config.check_test_output(&mut errors, &output.stdout, &output.stderr); - errors.extend(mode.ok(output.status).err()); + let status = output.status; + if status.code() != Some(exit_code) { + errors.push(Error::ExitStatus { + mode: format!("run({exit_code})"), + status, + expected: exit_code, + }) + } if errors.is_empty() { Ok(TestOk::Ok) } else { @@ -504,18 +512,12 @@ impl TestConfig<'_> { } } - fn run_rustfix( - self, + pub(crate) fn run_rustfix( + &self, output: Output, - mode: Mode, - extra_args: Vec, - ) -> Result<(), Errored> { - let no_run_rustfix = self.find_one("`no-rustfix` annotations", |r| r.no_rustfix.clone())?; - - let global_rustfix = match mode { - Mode::Pass | Mode::Run { .. } | Mode::Panic => RustfixMode::Disabled, - Mode::Fail { rustfix, .. } | Mode::Yolo { rustfix } => rustfix, - }; + global_rustfix: RustfixMode, + ) -> Result { + let no_run_rustfix = self.find_one_custom("no-rustfix")?; let fixed_code = (no_run_rustfix.is_none() && global_rustfix.enabled()) .then_some(()) @@ -566,7 +568,6 @@ impl TestConfig<'_> { stdout: output.stdout, })?; - let edition = self.edition()?.into(); let rustfix_comments = Comments { revisions: None, revisioned: std::iter::once(( @@ -584,17 +585,19 @@ impl TestConfig<'_> { error_matches: vec![], require_annotations_for_level: Default::default(), aux_builds: self.collect(|r| r.aux_builds.iter().cloned()), - edition, mode: OptWithLine::new(Mode::Pass, Span::default()), - no_rustfix: OptWithLine::new((), Span::default()), diagnostic_code_prefix: OptWithLine::new(String::new(), Span::default()), - needs_asm_support: false, + custom: self + .comments + .for_revision(self.revision) + .flat_map(|r| r.custom.clone()) + .collect(), }, )) .collect(), }; let config = TestConfig { - config: self.config, + config: self.config.clone(), revision: self.revision, comments: &rustfix_comments, path: self.path, @@ -634,15 +637,14 @@ impl TestConfig<'_> { } if !run { - return Ok(()); + return Ok(false); } let mut cmd = config.build_command()?; - cmd.args(extra_args); cmd.arg("--crate-name").arg(crate_name); let output = cmd.output().unwrap(); if output.status.success() { - Ok(()) + Ok(true) } else { Err(Errored { command: cmd, @@ -655,4 +657,28 @@ impl TestConfig<'_> { }) } } + + fn find_one_custom(&self, arg: &str) -> Result, Errored> { + self.find_one(arg, |r| r.custom.get(arg).map(|s| s.as_ref()).into()) + } +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct Run { + pub exit_code: i32, +} + +impl Flag for Run { + fn clone_inner(&self) -> Box { + Box::new(*self) + } + fn post_test_action( + &self, + config: &TestConfig<'_>, + cmd: Command, + _output: &Output, + ) -> Result, Errored> { + config.run_test_binary(cmd, self.exit_code)?; + Ok(None) + } } diff --git a/src/tests.rs b/src/tests.rs index 2375d3e5..52eb804a 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -18,7 +18,7 @@ fn config() -> Config { macro_rules! config { ($config:ident = $s:expr) => { let path = Path::new("moobar"); - let comments = Comments::parse($s, $config.comment_defaults.clone(), path).unwrap(); + let comments = Comments::parse($s, &$config, path).unwrap(); #[allow(unused_mut)] let mut $config = TestConfig { config: $config, diff --git a/tests/integration.rs b/tests/integration.rs index 1a8ec25f..783b306e 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -127,7 +127,7 @@ fn main() -> Result<()> { // multiple [[test]]s exist. If there's only one test, it returns // 1 on failure. Mode::Panic => fail, - Mode::Run { .. } | Mode::Yolo { .. } | Mode::Fail { .. } => unreachable!(), + Mode::Yolo { .. } | Mode::Fail { .. } => unreachable!(), } && default_any_file_filter(path, config), ) diff --git a/tests/integrations/basic-bin/Cargo.lock b/tests/integrations/basic-bin/Cargo.lock index 0cc23cc2..250c175d 100644 --- a/tests/integrations/basic-bin/Cargo.lock +++ b/tests/integrations/basic-bin/Cargo.lock @@ -719,7 +719,7 @@ dependencies = [ [[package]] name = "ui_test" -version = "0.22.3" +version = "0.23.0" dependencies = [ "annotate-snippets", "anyhow", diff --git a/tests/integrations/basic-fail-mode/Cargo.lock b/tests/integrations/basic-fail-mode/Cargo.lock index 3ec6f0f8..4bbb58f5 100644 --- a/tests/integrations/basic-fail-mode/Cargo.lock +++ b/tests/integrations/basic-fail-mode/Cargo.lock @@ -719,7 +719,7 @@ dependencies = [ [[package]] name = "ui_test" -version = "0.22.3" +version = "0.23.0" dependencies = [ "annotate-snippets", "anyhow", diff --git a/tests/integrations/basic-fail-mode/tests/ui_tests.rs b/tests/integrations/basic-fail-mode/tests/ui_tests.rs index ee67b3d6..0d8c7d4b 100644 --- a/tests/integrations/basic-fail-mode/tests/ui_tests.rs +++ b/tests/integrations/basic-fail-mode/tests/ui_tests.rs @@ -15,7 +15,6 @@ fn main() -> ui_test::color_eyre::Result<()> { }; config.comment_defaults.base().mode = Spanned::dummy(Mode::Fail { require_patterns: true, - rustfix: RustfixMode::MachineApplicable, }) .into(); config.stderr_filter("in ([0-9]m )?[0-9\\.]+s", ""); diff --git a/tests/integrations/basic-fail/Cargo.lock b/tests/integrations/basic-fail/Cargo.lock index 5cb22c7c..18f32b4a 100644 --- a/tests/integrations/basic-fail/Cargo.lock +++ b/tests/integrations/basic-fail/Cargo.lock @@ -719,7 +719,7 @@ dependencies = [ [[package]] name = "ui_test" -version = "0.22.3" +version = "0.23.0" dependencies = [ "annotate-snippets", "anyhow", diff --git a/tests/integrations/basic-fail/Cargo.stderr b/tests/integrations/basic-fail/Cargo.stderr index c46bc809..11f88adf 100644 --- a/tests/integrations/basic-fail/Cargo.stderr +++ b/tests/integrations/basic-fail/Cargo.stderr @@ -6,7 +6,7 @@ error: test failed, to rerun pass `--test ui_tests` Caused by: process didn't exit successfully: `$DIR/target/ui/tests/integrations/basic-fail/debug/deps/ui_tests-HASH` (exit status: 1) -thread 'main' panicked at tests/ui_tests_bless.rs:53:18: +thread 'main' panicked at tests/ui_tests_bless.rs:58:18: invalid mode/result combo: yolo: Err(tests failed Location: diff --git a/tests/integrations/basic-fail/Cargo.stdout b/tests/integrations/basic-fail/Cargo.stdout index 2d1899be..c6c3b6bb 100644 --- a/tests/integrations/basic-fail/Cargo.stdout +++ b/tests/integrations/basic-fail/Cargo.stdout @@ -519,7 +519,7 @@ full stdout: FAILED TEST: tests/actual_tests_bless/aux_proc_macro_no_main.rs -command: "rustc" "--error-format=json" "--crate-type=lib" "--extern" "basic_fail=$DIR/tests/integrations/basic-fail/../../../target/$TMP/$TRIPLE/debug/libbasic_fail.rlib" "--extern" "basic_fail=$DIR/tests/integrations/basic-fail/../../../target/$TMP/$TRIPLE/debug/libbasic_fail-$HASH.rmeta" "-L" "$DIR/tests/integrations/basic-fail/../../../target/$TMP/$TRIPLE/debug" "-L" "$DIR/tests/integrations/basic-fail/../../../target/$TMP/$TRIPLE/debug" "--out-dir" "$TMP "tests/actual_tests_bless/aux_proc_macro_no_main.rs" "--edition" "2021" "--extern" "the_proc_macro=$DIR/tests/integrations/basic-fail/../../../target/$TMP/tests/actual_tests_bless/auxiliary/libthe_proc_macro.so" "-L" "$DIR/tests/integrations/basic-fail/../../../target/$TMP/tests/actual_tests_bless/auxiliary" +command: "rustc" "--error-format=json" "--crate-type=lib" "--extern" "basic_fail=$DIR/tests/integrations/basic-fail/../../../target/$TMP/$TRIPLE/debug/libbasic_fail.rlib" "--extern" "basic_fail=$DIR/tests/integrations/basic-fail/../../../target/$TMP/$TRIPLE/debug/libbasic_fail-$HASH.rmeta" "-L" "$DIR/tests/integrations/basic-fail/../../../target/$TMP/$TRIPLE/debug" "-L" "$DIR/tests/integrations/basic-fail/../../../target/$TMP/$TRIPLE/debug" "--extern" "the_proc_macro=$DIR/tests/integrations/basic-fail/../../../target/$TMP/tests/actual_tests_bless/auxiliary/libthe_proc_macro.so" "-L" "$DIR/tests/integrations/basic-fail/../../../target/$TMP/tests/actual_tests_bless/auxiliary" "--out-dir" "$TMP "tests/actual_tests_bless/aux_proc_macro_no_main.rs" "--edition" "2021" error: there were 1 unmatched diagnostics --> tests/actual_tests_bless/aux_proc_macro_no_main.rs:7:8 diff --git a/tests/integrations/basic-fail/tests/ui_tests_bless.rs b/tests/integrations/basic-fail/tests/ui_tests_bless.rs index b52a3479..7abfcd3b 100644 --- a/tests/integrations/basic-fail/tests/ui_tests_bless.rs +++ b/tests/integrations/basic-fail/tests/ui_tests_bless.rs @@ -1,19 +1,19 @@ use ui_test::{spanned::Spanned, *}; fn main() -> ui_test::color_eyre::Result<()> { - for mode in [ - Mode::Fail { - require_patterns: true, - rustfix: RustfixMode::MachineApplicable, - }, - Mode::Yolo { - rustfix: RustfixMode::Everything, - }, + for (mode, rustfix) in [ + ( + Mode::Fail { + require_patterns: true, + }, + RustfixMode::MachineApplicable, + ), + (Mode::Yolo, RustfixMode::Everything), ] { let path = "../../../target"; let root_dir = match mode { - Mode::Yolo { .. } => "tests/actual_tests_bless_yolo", + Mode::Yolo => "tests/actual_tests_bless_yolo", Mode::Fail { .. } => "tests/actual_tests_bless", _ => unreachable!(), }; @@ -29,6 +29,11 @@ fn main() -> ui_test::color_eyre::Result<()> { ..Config::rustc(root_dir) }; config.comment_defaults.base().mode = Spanned::dummy(mode).into(); + config + .comment_defaults + .base() + .custom + .insert("rustfix", Spanned::dummy(Box::new(rustfix))); // hide binaries generated for successfully passing tests let tmp_dir = tempfile::tempdir_in(path)?; diff --git a/tests/integrations/basic/Cargo.lock b/tests/integrations/basic/Cargo.lock index 1f8d6262..9980068d 100644 --- a/tests/integrations/basic/Cargo.lock +++ b/tests/integrations/basic/Cargo.lock @@ -642,7 +642,7 @@ dependencies = [ [[package]] name = "ui_test" -version = "0.22.3" +version = "0.23.0" dependencies = [ "annotate-snippets", "anyhow", diff --git a/tests/integrations/basic/tests/run_file.rs b/tests/integrations/basic/tests/run_file.rs index 5dc7b12d..d5dfb79a 100644 --- a/tests/integrations/basic/tests/run_file.rs +++ b/tests/integrations/basic/tests/run_file.rs @@ -59,7 +59,7 @@ fn non_utf8() -> Result<()> { } let mut config = Config::rustc(PathBuf::new()); config.program = CommandBuilder::cmd("cat"); - config.comment_defaults.base().edition = Default::default(); + config.comment_defaults.base().custom.clear(); config.host = Some(String::new()); let mut result = ui_test::test_command(config, &path)?; diff --git a/tests/integrations/cargo-run/Cargo.lock b/tests/integrations/cargo-run/Cargo.lock index 0cc23cc2..250c175d 100644 --- a/tests/integrations/cargo-run/Cargo.lock +++ b/tests/integrations/cargo-run/Cargo.lock @@ -719,7 +719,7 @@ dependencies = [ [[package]] name = "ui_test" -version = "0.22.3" +version = "0.23.0" dependencies = [ "annotate-snippets", "anyhow",