use crate::{
errors::{FiltersetParseErrors, ParseSingleError},
parsing::{
DisplayParsedRegex, DisplayParsedString, ExprResult, GenericGlob, ParsedExpr, ParsedLeaf,
new_span, parse,
},
};
use guppy::{
PackageId,
graph::{BuildTargetId, PackageGraph, PackageMetadata, cargo::BuildPlatform},
};
use miette::SourceSpan;
use nextest_metadata::{RustBinaryId, RustTestBinaryKind, TestCaseName};
use recursion::{Collapsible, CollapsibleExt, MappableFrame, PartiallyApplied};
use smol_str::SmolStr;
use std::{collections::HashSet, fmt, sync::OnceLock};
#[derive(Debug, Clone)]
pub enum NameMatcher {
Equal { value: String, implicit: bool },
Contains { value: String, implicit: bool },
Glob { glob: GenericGlob, implicit: bool },
Regex(regex::Regex),
}
impl NameMatcher {
pub(crate) fn implicit_equal(value: String) -> Self {
Self::Equal {
value,
implicit: true,
}
}
pub(crate) fn implicit_contains(value: String) -> Self {
Self::Contains {
value,
implicit: true,
}
}
}
impl PartialEq for NameMatcher {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
Self::Contains {
value: s1,
implicit: default1,
},
Self::Contains {
value: s2,
implicit: default2,
},
) => s1 == s2 && default1 == default2,
(
Self::Equal {
value: s1,
implicit: default1,
},
Self::Equal {
value: s2,
implicit: default2,
},
) => s1 == s2 && default1 == default2,
(Self::Regex(r1), Self::Regex(r2)) => r1.as_str() == r2.as_str(),
(Self::Glob { glob: g1, .. }, Self::Glob { glob: g2, .. }) => {
g1.regex().as_str() == g2.regex().as_str()
}
_ => false,
}
}
}
impl Eq for NameMatcher {}
impl fmt::Display for NameMatcher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Equal { value, implicit } => write!(
f,
"{}{}",
if *implicit { "" } else { "=" },
DisplayParsedString(value)
),
Self::Contains { value, implicit } => write!(
f,
"{}{}",
if *implicit { "" } else { "~" },
DisplayParsedString(value)
),
Self::Glob { glob, implicit } => write!(
f,
"{}{}",
if *implicit { "" } else { "#" },
DisplayParsedString(glob.as_str())
),
Self::Regex(r) => write!(f, "/{}/", DisplayParsedRegex(r)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FiltersetLeaf {
Packages(HashSet<PackageId>),
Kind(NameMatcher, SourceSpan),
Platform(BuildPlatform, SourceSpan),
Binary(NameMatcher, SourceSpan),
BinaryId(NameMatcher, SourceSpan),
Test(NameMatcher, SourceSpan),
Group(NameMatcher, SourceSpan),
Default,
All,
None,
}
impl FiltersetLeaf {
pub fn is_runtime_only(&self) -> bool {
matches!(self, Self::Test(_, _) | Self::Group(_, _) | Self::Default)
}
}
pub trait GroupLookup: fmt::Debug {
fn is_member_test(&self, test: &TestQuery<'_>, matcher: &NameMatcher) -> bool;
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct BinaryQuery<'a> {
pub package_id: &'a PackageId,
pub binary_id: &'a RustBinaryId,
pub binary_name: &'a str,
pub kind: &'a RustTestBinaryKind,
pub platform: BuildPlatform,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct TestQuery<'a> {
pub binary_query: BinaryQuery<'a>,
pub test_name: &'a TestCaseName,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Filterset {
pub input: String,
pub parsed: ParsedExpr,
pub compiled: CompiledExpr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompiledExpr {
Not(Box<CompiledExpr>),
Union(Box<CompiledExpr>, Box<CompiledExpr>),
Intersection(Box<CompiledExpr>, Box<CompiledExpr>),
Set(FiltersetLeaf),
}
impl CompiledExpr {
pub const ALL: Self = CompiledExpr::Set(FiltersetLeaf::All);
pub fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext<'_>) -> Option<bool> {
use ExprFrame::*;
Wrapped(self).collapse_frames(|layer: ExprFrame<&FiltersetLeaf, Option<bool>>| {
match layer {
Set(set) => set.matches_binary(query, cx),
Not(a) => a.logic_not(),
Union(a, b) => a.logic_or(b),
Intersection(a, b) => a.logic_and(b),
Difference(a, b) => a.logic_and(b.logic_not()),
Parens(a) => a,
}
})
}
pub fn matches_test(&self, query: &TestQuery<'_>, cx: &EvalContext<'_>) -> bool {
self.matches_test_impl(query, cx, &NoGroups)
}
pub fn matches_test_with_groups(
&self,
query: &TestQuery<'_>,
cx: &EvalContext<'_>,
groups: &dyn GroupLookup,
) -> bool {
self.matches_test_impl(query, cx, &WithGroups(groups))
}
fn matches_test_impl(
&self,
query: &TestQuery<'_>,
cx: &EvalContext<'_>,
groups: &impl GroupResolver,
) -> bool {
use ExprFrame::*;
Wrapped(self).collapse_frames(|layer: ExprFrame<&FiltersetLeaf, bool>| match layer {
Set(set) => set.matches_test_impl(query, cx, groups),
Not(a) => !a,
Union(a, b) => a || b,
Intersection(a, b) => a && b,
Difference(a, b) => a && !b,
Parens(a) => a,
})
}
pub fn has_group_matchers(&self) -> bool {
let mut found = false;
Wrapped(self).collapse_frames(|layer: ExprFrame<&FiltersetLeaf, ()>| {
if matches!(layer, ExprFrame::Set(FiltersetLeaf::Group(_, _))) {
found = true;
}
});
found
}
}
impl NameMatcher {
pub fn is_match(&self, input: &str) -> bool {
match self {
Self::Equal { value, .. } => value == input,
Self::Contains { value, .. } => input.contains(value),
Self::Glob { glob, .. } => glob.is_match(input),
Self::Regex(reg) => reg.is_match(input),
}
}
}
impl FiltersetLeaf {
fn matches_test_impl(
&self,
query: &TestQuery<'_>,
cx: &EvalContext,
groups: &impl GroupResolver,
) -> bool {
match self {
Self::All => true,
Self::None => false,
Self::Default => cx.default_filter.matches_test_impl(query, cx, &NoGroups),
Self::Test(matcher, _) => matcher.is_match(query.test_name.as_str()),
Self::Binary(matcher, _) => matcher.is_match(query.binary_query.binary_name),
Self::BinaryId(matcher, _) => matcher.is_match(query.binary_query.binary_id.as_str()),
Self::Platform(platform, _) => query.binary_query.platform == *platform,
Self::Kind(matcher, _) => matcher.is_match(query.binary_query.kind.as_str()),
Self::Packages(packages) => packages.contains(query.binary_query.package_id),
Self::Group(matcher, _) => groups.resolve_group(query, matcher),
}
}
fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext) -> Option<bool> {
match self {
Self::All => Logic::top(),
Self::None => Logic::bottom(),
Self::Default => cx.default_filter.matches_binary(query, cx),
Self::Test(_, _) => None,
Self::Binary(matcher, _) => Some(matcher.is_match(query.binary_name)),
Self::BinaryId(matcher, _) => Some(matcher.is_match(query.binary_id.as_str())),
Self::Platform(platform, _) => Some(query.platform == *platform),
Self::Kind(matcher, _) => Some(matcher.is_match(query.kind.as_str())),
Self::Packages(packages) => Some(packages.contains(query.package_id)),
Self::Group(_, _) => None,
}
}
}
#[derive(Debug)]
pub enum KnownGroups {
Known { custom_groups: HashSet<String> },
Unavailable,
}
impl KnownGroups {
pub(crate) fn matches(&self, matcher: &NameMatcher) -> bool {
let custom_groups = match self {
KnownGroups::Known { custom_groups } => custom_groups,
KnownGroups::Unavailable => panic!(
"group() validation data is unavailable; \
this is a nextest bug (group() should have been banned \
during compilation for this filterset kind)"
),
};
if matcher.is_match(nextest_metadata::GLOBAL_TEST_GROUP) {
return true;
}
match matcher {
NameMatcher::Equal { value, .. } => custom_groups.contains(value.as_str()),
_ => {
custom_groups.iter().any(|g| matcher.is_match(g))
}
}
}
}
#[derive(Debug)]
pub struct ParseContext<'g> {
graph: &'g PackageGraph,
cache: OnceLock<ParseContextCache<'g>>,
}
impl<'g> ParseContext<'g> {
#[inline]
pub fn new(graph: &'g PackageGraph) -> Self {
Self {
graph,
cache: OnceLock::new(),
}
}
#[inline]
pub fn graph(&self) -> &'g PackageGraph {
self.graph
}
pub(crate) fn make_cache(&self) -> &ParseContextCache<'g> {
self.cache
.get_or_init(|| ParseContextCache::new(self.graph))
}
}
#[derive(Debug)]
pub(crate) struct ParseContextCache<'g> {
pub(crate) workspace_packages: Vec<PackageMetadata<'g>>,
pub(crate) binary_ids: HashSet<SmolStr>,
pub(crate) binary_names: HashSet<&'g str>,
}
impl<'g> ParseContextCache<'g> {
fn new(graph: &'g PackageGraph) -> Self {
let workspace_packages: Vec<_> = graph
.resolve_workspace()
.packages(guppy::graph::DependencyDirection::Forward)
.collect();
let (binary_ids, binary_names) = workspace_packages
.iter()
.flat_map(|pkg| {
pkg.build_targets().filter_map(|bt| {
let kind = compute_kind(&bt.id())?;
let binary_id = RustBinaryId::from_parts(pkg.name(), &kind, bt.name());
Some((SmolStr::new(binary_id.as_str()), bt.name()))
})
})
.unzip();
Self {
workspace_packages,
binary_ids,
binary_names,
}
}
}
fn compute_kind(id: &BuildTargetId<'_>) -> Option<RustTestBinaryKind> {
match id {
BuildTargetId::Library => Some(RustTestBinaryKind::LIB),
BuildTargetId::Benchmark(_) => Some(RustTestBinaryKind::BENCH),
BuildTargetId::Example(_) => Some(RustTestBinaryKind::EXAMPLE),
BuildTargetId::BuildScript => {
None
}
BuildTargetId::Binary(_) => Some(RustTestBinaryKind::BIN),
BuildTargetId::Test(_) => Some(RustTestBinaryKind::TEST),
_ => panic!("unknown build target id: {id:?}"),
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FiltersetKind {
Test,
TestArchive,
OverrideFilter,
DefaultFilter,
}
impl fmt::Display for FiltersetKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Test => write!(f, "test"),
Self::OverrideFilter => write!(f, "override-filter"),
Self::TestArchive => write!(f, "archive-filter"),
Self::DefaultFilter => write!(f, "default-filter"),
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct EvalContext<'a> {
pub default_filter: &'a CompiledExpr,
}
trait GroupResolver {
fn resolve_group(&self, query: &TestQuery<'_>, matcher: &NameMatcher) -> bool;
}
struct NoGroups;
impl GroupResolver for NoGroups {
fn resolve_group(&self, _query: &TestQuery<'_>, _matcher: &NameMatcher) -> bool {
panic!(
"group() predicate in expression where groups are not expected; \
this is a nextest bug (group() should be banned during compilation \
for this filterset kind)"
)
}
}
struct WithGroups<'a>(&'a dyn GroupLookup);
impl GroupResolver for WithGroups<'_> {
fn resolve_group(&self, query: &TestQuery<'_>, matcher: &NameMatcher) -> bool {
self.0.is_member_test(query, matcher)
}
}
impl Filterset {
pub fn parse(
input: String,
cx: &ParseContext<'_>,
kind: FiltersetKind,
known_groups: &KnownGroups,
) -> Result<Self, FiltersetParseErrors> {
let mut errors = Vec::new();
match parse(new_span(&input, &mut errors)) {
Ok(parsed_expr) => {
if !errors.is_empty() {
return Err(FiltersetParseErrors::new(input.clone(), errors));
}
match parsed_expr {
ExprResult::Valid(parsed) => {
let compiled = crate::compile::compile(&parsed, cx, kind, known_groups)
.map_err(|errors| FiltersetParseErrors::new(input.clone(), errors))?;
Ok(Self {
input,
parsed,
compiled,
})
}
_ => {
Err(FiltersetParseErrors::new(
input,
vec![ParseSingleError::Unknown],
))
}
}
}
Err(_) => {
Err(FiltersetParseErrors::new(
input,
vec![ParseSingleError::Unknown],
))
}
}
}
pub fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext<'_>) -> Option<bool> {
self.compiled.matches_binary(query, cx)
}
pub fn matches_test(&self, query: &TestQuery<'_>, cx: &EvalContext<'_>) -> bool {
self.compiled.matches_test(query, cx)
}
pub fn matches_test_with_groups(
&self,
query: &TestQuery<'_>,
cx: &EvalContext<'_>,
groups: &dyn GroupLookup,
) -> bool {
self.compiled.matches_test_with_groups(query, cx, groups)
}
pub fn needs_deps(raw_expr: &str) -> bool {
raw_expr.contains("deps")
}
}
trait Logic {
fn top() -> Self;
fn bottom() -> Self;
fn logic_and(self, other: Self) -> Self;
fn logic_or(self, other: Self) -> Self;
fn logic_not(self) -> Self;
}
impl Logic for bool {
#[inline]
fn top() -> Self {
true
}
#[inline]
fn bottom() -> Self {
false
}
#[inline]
fn logic_and(self, other: Self) -> Self {
self && other
}
#[inline]
fn logic_or(self, other: Self) -> Self {
self || other
}
#[inline]
fn logic_not(self) -> Self {
!self
}
}
impl Logic for Option<bool> {
#[inline]
fn top() -> Self {
Some(true)
}
#[inline]
fn bottom() -> Self {
Some(false)
}
#[inline]
fn logic_and(self, other: Self) -> Self {
match (self, other) {
(Some(false), _) | (_, Some(false)) => Some(false),
(Some(true), Some(true)) => Some(true),
_ => None,
}
}
#[inline]
fn logic_or(self, other: Self) -> Self {
match (self, other) {
(Some(true), _) | (_, Some(true)) => Some(true),
(Some(false), Some(false)) => Some(false),
_ => None,
}
}
#[inline]
fn logic_not(self) -> Self {
self.map(|v| !v)
}
}
pub(crate) enum ExprFrame<Set, A> {
Not(A),
Union(A, A),
Intersection(A, A),
Difference(A, A),
Parens(A),
Set(Set),
}
impl<Set> MappableFrame for ExprFrame<Set, PartiallyApplied> {
type Frame<Next> = ExprFrame<Set, Next>;
fn map_frame<A, B>(input: Self::Frame<A>, mut f: impl FnMut(A) -> B) -> Self::Frame<B> {
use ExprFrame::*;
match input {
Not(a) => Not(f(a)),
Union(a, b) => {
let b = f(b);
let a = f(a);
Union(a, b)
}
Intersection(a, b) => {
let b = f(b);
let a = f(a);
Intersection(a, b)
}
Difference(a, b) => {
let b = f(b);
let a = f(a);
Difference(a, b)
}
Parens(a) => Parens(f(a)),
Set(f) => Set(f),
}
}
}
pub(crate) struct Wrapped<T>(pub(crate) T);
impl<'a> Collapsible for Wrapped<&'a CompiledExpr> {
type FrameToken = ExprFrame<&'a FiltersetLeaf, PartiallyApplied>;
fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
match self.0 {
CompiledExpr::Not(a) => ExprFrame::Not(Wrapped(a.as_ref())),
CompiledExpr::Union(a, b) => ExprFrame::Union(Wrapped(a.as_ref()), Wrapped(b.as_ref())),
CompiledExpr::Intersection(a, b) => {
ExprFrame::Intersection(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
}
CompiledExpr::Set(f) => ExprFrame::Set(f),
}
}
}
impl<'a> Collapsible for Wrapped<&'a ParsedExpr> {
type FrameToken = ExprFrame<&'a ParsedLeaf, PartiallyApplied>;
fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
match self.0 {
ParsedExpr::Not(_, a) => ExprFrame::Not(Wrapped(a.as_ref())),
ParsedExpr::Union(_, a, b) => {
ExprFrame::Union(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
}
ParsedExpr::Intersection(_, a, b) => {
ExprFrame::Intersection(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
}
ParsedExpr::Difference(_, a, b) => {
ExprFrame::Difference(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
}
ParsedExpr::Parens(a) => ExprFrame::Parens(Wrapped(a.as_ref())),
ParsedExpr::Set(f) => ExprFrame::Set(f),
}
}
}