diff --git a/Cargo.lock b/Cargo.lock index ba2f2f58417..f941699c513 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bytecount" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f861d9ce359f56dbcb6e0c2a1cb84e52ad732cadb57b806adeb3c7668caccbd8" + +[[package]] +name = "bytecount" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" + [[package]] name = "byteorder" version = "1.4.3" @@ -252,7 +264,7 @@ checksum = "54ad70579325f1a38ea4c13412b82241c5900700a69785d73e2736bd65a33f86" dependencies = [ "async-trait", "lazy_static", - "nom", + "nom 7.1.1", "pathdiff", "serde", "toml", @@ -578,6 +590,19 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.120" @@ -614,6 +639,29 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miette" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ea7314b2a8dd373c2f2d2322e866ddea5d62ffd3d6cd7f2bb8c1467e56529f" +dependencies = [ + "miette-derive", + "once_cell", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c547b28d4f52cae473fb5a30ca087ed7fc5d1bac150bd6dfd9ec0a4562303aa3" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -665,7 +713,11 @@ dependencies = [ "indent_write", "indoc", "maplit", + "miette", "nextest-metadata", + "nom 7.1.1", + "nom-tracable", + "nom_locate 4.0.0", "num_cpus", "once_cell", "owo-colors", @@ -674,6 +726,7 @@ dependencies = [ "proptest-derive", "quick-junit", "rayon", + "regex", "serde", "serde_json", "strip-ansi-escapes", @@ -696,6 +749,17 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "nom" version = "7.1.1" @@ -706,6 +770,50 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom-tracable" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0d5ea9205a12f87381f7449d43a9ff1dd888af72ef01a858ecccd6d4182de" +dependencies = [ + "nom 7.1.1", + "nom-tracable-macros", + "nom_locate 1.0.0", + "nom_locate 4.0.0", +] + +[[package]] +name = "nom-tracable-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a6ae7ea2ecde0ee81e0eb45ce5415f2d3b0ef3c30688b0e85d127d43e0f6913" +dependencies = [ + "quote 1.0.15", + "syn 1.0.86", +] + +[[package]] +name = "nom_locate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f932834fd8e391fc7710e2ba17e8f9f8645d846b55aa63207e17e110a1e1ce35" +dependencies = [ + "bytecount 0.3.2", + "memchr", + "nom 5.1.2", +] + +[[package]] +name = "nom_locate" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37794436ca3029a3089e0b95d42da1f0b565ad271e4d3bb4bad0c7bb70b10605" +dependencies = [ + "bytecount 0.6.2", + "memchr", + "nom 7.1.1", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1241,6 +1349,26 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.15", + "syn 1.0.86", +] + [[package]] name = "time" version = "0.1.44" @@ -1271,6 +1399,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "unicode-xid" version = "0.1.0" diff --git a/nextest-runner/Cargo.toml b/nextest-runner/Cargo.toml index 71bae6959e5..db2cb127865 100644 --- a/nextest-runner/Cargo.toml +++ b/nextest-runner/Cargo.toml @@ -9,6 +9,11 @@ documentation = "https://docs.rs/nextest-runner" edition = "2018" rust-version = "1.54" +[features] +default = [] +# Must be called `trace` which is a bit infortunate +trace = ["nom-tracable/trace"] + [dependencies] aho-corasick = "0.7.18" camino = { version = "1.0.7", features = ["serde1"] } @@ -27,8 +32,13 @@ humantime-serde = "1.1.1" indent_write = "2.2.0" once_cell = "1.10.0" owo-colors = "3.2.0" +miette = "4.2.1" +nom = "7.1.1" +nom_locate = "4.0.0" +nom-tracable = "0.8.0" num_cpus = "1.13.1" rayon = "1.5.1" +regex = "1.5.5" serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" strip-ansi-escapes = "0.1.1" diff --git a/nextest-runner/src/test_filter.rs b/nextest-runner/src/test_filter.rs index 7845f106550..6d84220d672 100644 --- a/nextest-runner/src/test_filter.rs +++ b/nextest-runner/src/test_filter.rs @@ -18,6 +18,8 @@ use aho_corasick::AhoCorasick; use nextest_metadata::{FilterMatch, MismatchReason}; use std::{fmt, str::FromStr}; +mod expression; + /// Whether to run ignored tests. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum RunIgnored { @@ -197,6 +199,16 @@ impl<'filter> TestFilter<'filter> { _test_name: &str, ) -> Option { // TODO filter using self.expr + + // DSL ? + // - package(name) + // - deps(package_name) + // - rdeps(package_name) + // - test(partial_name) + // - not(expr) + // - && / and + // - || / or + // - () None } diff --git a/nextest-runner/src/test_filter/expression.rs b/nextest-runner/src/test_filter/expression.rs new file mode 100644 index 00000000000..d2260417155 --- /dev/null +++ b/nextest-runner/src/test_filter/expression.rs @@ -0,0 +1,375 @@ +use nom::{ + branch::alt, + bytes::complete::{tag, take_till1, take_while, take_while1}, + character::complete::char, + combinator::{eof, map, map_res, opt, recognize, success}, + multi::many0, + sequence::{delimited, pair, preceded, terminated, tuple}, +}; + +use nom_tracable::tracable_parser; + +type Span<'a> = nom_locate::LocatedSpan<&'a str, nom_tracable::TracableInfo>; +type IResult<'a, T> = nom::IResult, T>; + +/// Matcher for name +/// +/// Used both for package name and test name +#[derive(Debug)] +pub enum NameMatcher { + /// Exact value + Exact(String), + /// Simple contains test + Contains(String), + /// Test against a regex + Regex(regex::Regex), +} + +#[cfg(test)] +impl PartialEq for NameMatcher { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Contains(s1), Self::Contains(s2)) => s1 == s2, + (Self::Exact(s1), Self::Exact(s2)) => s1 == s2, + (Self::Regex(r1), Self::Regex(r2)) => r1.as_str() == r2.as_str(), + _ => false, + } + } + + fn ne(&self, other: &Self) -> bool { + match (self, other) { + (Self::Contains(s1), Self::Contains(s2)) => s1 != s2, + (Self::Exact(s1), Self::Exact(s2)) => s1 != s2, + (Self::Regex(r1), Self::Regex(r2)) => r1.as_str() != r2.as_str(), + _ => true, + } + } +} + +#[cfg(test)] +impl Eq for NameMatcher {} + +/// Filtering expression +/// +/// Used to filter tests to run. +#[cfg_attr(test, derive(PartialEq, Eq))] +#[derive(Debug)] +pub enum Expr { + /// Accepts every tests not in the given expression + Not(Box), + /// Accepts every tests in either given expression + Union(Box, Box), + /// Accepts every tests in both given expression + Intersection(Box, Box), + /// Accepts every tests in a package + Package(NameMatcher), + /// Accepts every tests in a package dependencies + Deps(NameMatcher), + /// Accepts every tests in a package reverse dependencies + Rdeps(NameMatcher), + /// Accepts every tests matching a name + Test(NameMatcher), + /// Accepts every tests + All, + /// Accepts no tests + None, +} + +fn ws<'a, T, P: FnMut(Span<'a>) -> IResult<'a, T>>( + inner: P, +) -> impl FnMut(Span<'a>) -> IResult<'a, T> { + preceded(many0(char(' ')), inner) +} + +fn parentheses<'a, T, P: FnMut(Span<'a>) -> IResult<'a, T>>( + inner: P, +) -> impl FnMut(Span<'a>) -> IResult<'a, T> { + delimited(ws(char('(')), inner, ws(char(')'))) +} + +// see www.unicode.org/reports/tr31/ +fn is_xid_start(c: char) -> bool { + // TODO for real + c.is_alphabetic() || c == '_' +} + +// see www.unicode.org/reports/tr31/ +fn is_xid_continue(c: char) -> bool { + // TODO for real + c.is_alphanumeric() || c == '_' +} + +#[tracable_parser] +fn identifier(input: Span) -> IResult { + map( + recognize(pair(take_while1(is_xid_start), take_while(is_xid_continue))), + |res: Span| res.fragment().to_string(), + )(input) +} + +fn regex(end: char) -> impl FnMut(Span) -> IResult { + move |input| { + map_res(recognize(take_till1(|c| c == end)), |res: Span| { + regex::Regex::new(res.fragment()).map(|r| NameMatcher::Regex(r)) + })(input) + } +} + +#[tracable_parser] +fn parse_name_matcher(input: Span) -> IResult { + ws(alt(( + map( + preceded(tag("contains:"), ws(identifier)), + NameMatcher::Contains, + ), + preceded(tag("re:"), regex(')')), + preceded( + tag("re:"), + ws(delimited(char('\''), regex('\''), char('\''))), + ), + map(preceded(tag("exact:"), ws(identifier)), NameMatcher::Exact), + map(identifier, NameMatcher::Contains), + )))(input) +} + +#[tracable_parser] +fn parse_expr_all(input: Span) -> IResult { + map(tuple((tag("all"), parentheses(success(())))), |_| Expr::All)(input) +} + +#[tracable_parser] +fn parse_expr_none(input: Span) -> IResult { + map(tuple((tag("none"), parentheses(success(())))), |_| { + Expr::None + })(input) +} + +#[tracable_parser] +fn parse_expr_package(input: Span) -> IResult { + map( + tuple((tag("package"), parentheses(parse_name_matcher))), + |name| Expr::Package(name.1), + )(input) +} + +#[tracable_parser] +fn parse_expr_deps(input: Span) -> IResult { + map( + tuple((tag("deps"), parentheses(parse_name_matcher))), + |name| Expr::Deps(name.1), + )(input) +} + +#[tracable_parser] +fn parse_expr_rdeps(input: Span) -> IResult { + map( + tuple((tag("rdeps"), parentheses(parse_name_matcher))), + |name| Expr::Rdeps(name.1), + )(input) +} + +#[tracable_parser] +fn parse_expr_test(input: Span) -> IResult { + map( + tuple((tag("test"), parentheses(parse_name_matcher))), + |name| Expr::Test(name.1), + )(input) +} + +#[tracable_parser] +fn parse_expr_not(input: Span) -> IResult { + let op = ws(alt((tag("not "), tag("!"), tag("-")))); + map(preceded(op, parse_expr), |expr| Expr::Not(Box::new(expr)))(input) +} + +#[tracable_parser] +fn parse_expr_union_second_half(input: Span) -> IResult { + let op = ws(alt((tag("and "), tag("&")))); + preceded(op, parse_expr)(input) +} + +#[tracable_parser] +fn parse_expr_difference_second_half(input: Span) -> IResult { + let op = ws(char('-')); + preceded(op, parse_expr)(input) +} + +#[tracable_parser] +fn parse_expr_intersection_second_half(input: Span) -> IResult { + let op = ws(alt((tag("or "), tag("|"), tag("+")))); + preceded(op, parse_expr)(input) +} + +#[tracable_parser] +fn parse_expr(input: Span) -> IResult { + let (input, expr) = ws(alt(( + parse_expr_all, + parse_expr_none, + parse_expr_not, + parse_expr_package, + parse_expr_deps, + parse_expr_rdeps, + parse_expr_test, + parentheses(parse_expr), + )))(input)?; + + enum Half { + Union(Expr), + Intersection(Expr), + Difference(Expr), + } + + let (input, half) = opt(alt(( + map(parse_expr_union_second_half, Half::Union), + map(parse_expr_intersection_second_half, Half::Intersection), + map(parse_expr_difference_second_half, Half::Difference), + )))(input)?; + + let expr = match half { + Some(Half::Union(expr_2)) => Expr::Union(Box::new(expr), Box::new(expr_2)), + Some(Half::Intersection(expr_2)) => Expr::Intersection(Box::new(expr), Box::new(expr_2)), + Some(Half::Difference(expr_2)) => { + Expr::Intersection(Box::new(expr), Box::new(Expr::Not(Box::new(expr_2)))) + } + None => expr, + }; + + Ok((input, expr)) +} + +#[allow(unused)] +pub fn parse_expression(input: Span) -> Result>> { + let (_, expr) = terminated(parse_expr, ws(eof))(input)?; + Ok(expr) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[track_caller] + fn parse(input: &str) -> Expr { + let info = nom_tracable::TracableInfo::new() + .forward(true) + .backward(true); + parse_expression(Span::new_extra(input, info)).unwrap() + } + + #[test] + fn parsing_all() { + let res = Expr::All; + assert_eq!(res, parse("all()")); + assert_eq!(res, parse(" all()")); + assert_eq!(res, parse("all() ")); + assert_eq!(res, parse(" all() ")); + } + + #[test] + fn parsing_none() { + let res = Expr::None; + assert_eq!(res, parse("none()")); + assert_eq!(res, parse(" none()")); + assert_eq!(res, parse("none() ")); + assert_eq!(res, parse(" none() ")); + } + + #[test] + fn parsing_not() { + let res = Expr::Not(Box::new(Expr::None)); + assert_eq!(res, parse("not none()")); + assert_eq!(res, parse("not none()")); + assert_eq!(res, parse("!none()")); + assert_eq!(res, parse("! none()")); + assert_eq!(res, parse("-none()")); + assert_eq!(res, parse("- none()")); + } + + #[test] + fn parsing_union() { + let res = Expr::Union(Box::new(Expr::All), Box::new(Expr::All)); + assert_eq!(res, parse("all()&all()")); + assert_eq!(res, parse("all() & all()")); + assert_eq!(res, parse("all() and all()")); + } + + #[test] + fn parsing_intersection() { + let res = Expr::Intersection(Box::new(Expr::All), Box::new(Expr::All)); + assert_eq!(res, parse("all()|all()")); + assert_eq!(res, parse("all() | all()")); + assert_eq!(res, parse("all() or all()")); + } + + #[test] + fn parsing_difference() { + let res = Expr::Intersection( + Box::new(Expr::All), + Box::new(Expr::Not(Box::new(Expr::All))), + ); + assert_eq!(res, parse("all()-all()")); + assert_eq!(res, parse("all() - all()")); + + assert_eq!(Expr::Not(Box::new(Expr::All)), parse("- all()")); + } + + #[test] + fn parsing_group() { + let expr = Expr::Union( + Box::new(Expr::Intersection(Box::new(Expr::All), Box::new(Expr::All))), + Box::new(Expr::All), + ); + assert_eq!(expr, parse("(all() | all()) & all()")); + assert_eq!(expr, parse(" ( all() | all()) & all()")); + assert_eq!(expr, parse("(all() | all() )& all()")); + + assert_eq!( + Expr::Intersection( + Box::new(Expr::All), + Box::new(Expr::Union(Box::new(Expr::All), Box::new(Expr::All))), + ), + parse("all() | (all() & all())") + ); + } + + #[test] + fn parsing_test() { + let expr = Expr::Test(NameMatcher::Contains("something".to_string())); + assert_eq!(expr, parse("test(something)")); + assert_eq!(expr, parse("test( something) ")); + assert_eq!(expr, parse("test(something )")); + assert_eq!(expr, parse("test(contains:something)")); + assert_eq!(expr, parse("test( contains: something )")); + + let expr = Expr::Test(NameMatcher::Exact("something".to_string())); + assert_eq!(expr, parse("test(exact:something)")); + assert_eq!(expr, parse("test( exact: something )")); + + let expr = Expr::Test(NameMatcher::Regex( + regex::Regex::new(r"something\\else").unwrap(), + )); + assert_eq!(expr, parse(r"test(re:something\\else)")); + + let expr = Expr::Test(NameMatcher::Regex( + regex::Regex::new(r"(some)thing\\else").unwrap(), + )); + assert_eq!(expr, parse(r"test(re:'(some)thing\\else')")); + } + + #[test] + fn parsing_package() { + let expr = Expr::Package(NameMatcher::Contains("something".to_string())); + assert_eq!(expr, parse("package(something)")); + } + + #[test] + fn parsing_deps() { + let expr = Expr::Deps(NameMatcher::Contains("something".to_string())); + assert_eq!(expr, parse("deps(something)")); + } + + #[test] + fn parsing_rdeps() { + let expr = Expr::Rdeps(NameMatcher::Contains("something".to_string())); + assert_eq!(expr, parse("rdeps(something)")); + } +}