1use crate::{
5 errors::{FiltersetParseErrors, ParseSingleError},
6 parsing::{
7 DisplayParsedRegex, DisplayParsedString, ExprResult, GenericGlob, ParsedExpr, ParsedLeaf,
8 new_span, parse,
9 },
10};
11use guppy::{
12 PackageId,
13 graph::{BuildTargetId, PackageGraph, PackageMetadata, cargo::BuildPlatform},
14};
15use miette::SourceSpan;
16use nextest_metadata::{RustBinaryId, RustTestBinaryKind, TestCaseName};
17use recursion::{Collapsible, CollapsibleExt, MappableFrame, PartiallyApplied};
18use smol_str::SmolStr;
19use std::{collections::HashSet, fmt, sync::OnceLock};
20
21#[derive(Debug, Clone)]
25pub enum NameMatcher {
26 Equal { value: String, implicit: bool },
28 Contains { value: String, implicit: bool },
30 Glob { glob: GenericGlob, implicit: bool },
32 Regex(regex::Regex),
34}
35
36impl NameMatcher {
37 pub(crate) fn implicit_equal(value: String) -> Self {
38 Self::Equal {
39 value,
40 implicit: true,
41 }
42 }
43
44 pub(crate) fn implicit_contains(value: String) -> Self {
45 Self::Contains {
46 value,
47 implicit: true,
48 }
49 }
50}
51
52impl PartialEq for NameMatcher {
53 fn eq(&self, other: &Self) -> bool {
54 match (self, other) {
55 (
56 Self::Contains {
57 value: s1,
58 implicit: default1,
59 },
60 Self::Contains {
61 value: s2,
62 implicit: default2,
63 },
64 ) => s1 == s2 && default1 == default2,
65 (
66 Self::Equal {
67 value: s1,
68 implicit: default1,
69 },
70 Self::Equal {
71 value: s2,
72 implicit: default2,
73 },
74 ) => s1 == s2 && default1 == default2,
75 (Self::Regex(r1), Self::Regex(r2)) => r1.as_str() == r2.as_str(),
76 (Self::Glob { glob: g1, .. }, Self::Glob { glob: g2, .. }) => {
77 g1.regex().as_str() == g2.regex().as_str()
78 }
79 _ => false,
80 }
81 }
82}
83
84impl Eq for NameMatcher {}
85
86impl fmt::Display for NameMatcher {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 Self::Equal { value, implicit } => write!(
90 f,
91 "{}{}",
92 if *implicit { "" } else { "=" },
93 DisplayParsedString(value)
94 ),
95 Self::Contains { value, implicit } => write!(
96 f,
97 "{}{}",
98 if *implicit { "" } else { "~" },
99 DisplayParsedString(value)
100 ),
101 Self::Glob { glob, implicit } => write!(
102 f,
103 "{}{}",
104 if *implicit { "" } else { "#" },
105 DisplayParsedString(glob.as_str())
106 ),
107 Self::Regex(r) => write!(f, "/{}/", DisplayParsedRegex(r)),
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum FiltersetLeaf {
115 Packages(HashSet<PackageId>),
117 Kind(NameMatcher, SourceSpan),
119 Platform(BuildPlatform, SourceSpan),
121 Binary(NameMatcher, SourceSpan),
123 BinaryId(NameMatcher, SourceSpan),
125 Test(NameMatcher, SourceSpan),
127 Group(NameMatcher, SourceSpan),
129 Default,
131 All,
133 None,
135}
136
137impl FiltersetLeaf {
138 pub fn is_runtime_only(&self) -> bool {
144 matches!(self, Self::Test(_, _) | Self::Group(_, _) | Self::Default)
145 }
146}
147
148pub trait GroupLookup: fmt::Debug {
154 fn is_member_test(&self, test: &TestQuery<'_>, matcher: &NameMatcher) -> bool;
157}
158
159#[derive(Copy, Clone, Debug, Eq, PartialEq)]
161pub struct BinaryQuery<'a> {
162 pub package_id: &'a PackageId,
164
165 pub binary_id: &'a RustBinaryId,
167
168 pub binary_name: &'a str,
170
171 pub kind: &'a RustTestBinaryKind,
173
174 pub platform: BuildPlatform,
176}
177
178#[derive(Copy, Clone, Debug, Eq, PartialEq)]
180pub struct TestQuery<'a> {
181 pub binary_query: BinaryQuery<'a>,
183
184 pub test_name: &'a TestCaseName,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct Filterset {
193 pub input: String,
195
196 pub parsed: ParsedExpr,
198
199 pub compiled: CompiledExpr,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub enum CompiledExpr {
205 Not(Box<CompiledExpr>),
207 Union(Box<CompiledExpr>, Box<CompiledExpr>),
209 Intersection(Box<CompiledExpr>, Box<CompiledExpr>),
211 Set(FiltersetLeaf),
213}
214
215impl CompiledExpr {
216 pub const ALL: Self = CompiledExpr::Set(FiltersetLeaf::All);
218
219 pub fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext<'_>) -> Option<bool> {
226 use ExprFrame::*;
227 Wrapped(self).collapse_frames(|layer: ExprFrame<&FiltersetLeaf, Option<bool>>| {
228 match layer {
229 Set(set) => set.matches_binary(query, cx),
230 Not(a) => a.logic_not(),
231 Union(a, b) => a.logic_or(b),
233 Intersection(a, b) => a.logic_and(b),
234 Difference(a, b) => a.logic_and(b.logic_not()),
235 Parens(a) => a,
236 }
237 })
238 }
239
240 pub fn matches_test(&self, query: &TestQuery<'_>, cx: &EvalContext<'_>) -> bool {
248 self.matches_test_impl(query, cx, &NoGroups)
249 }
250
251 pub fn matches_test_with_groups(
257 &self,
258 query: &TestQuery<'_>,
259 cx: &EvalContext<'_>,
260 groups: &dyn GroupLookup,
261 ) -> bool {
262 self.matches_test_impl(query, cx, &WithGroups(groups))
263 }
264
265 fn matches_test_impl(
266 &self,
267 query: &TestQuery<'_>,
268 cx: &EvalContext<'_>,
269 groups: &impl GroupResolver,
270 ) -> bool {
271 use ExprFrame::*;
272 Wrapped(self).collapse_frames(|layer: ExprFrame<&FiltersetLeaf, bool>| match layer {
273 Set(set) => set.matches_test_impl(query, cx, groups),
274 Not(a) => !a,
275 Union(a, b) => a || b,
276 Intersection(a, b) => a && b,
277 Difference(a, b) => a && !b,
278 Parens(a) => a,
279 })
280 }
281
282 pub fn has_group_matchers(&self) -> bool {
284 let mut found = false;
285 Wrapped(self).collapse_frames(|layer: ExprFrame<&FiltersetLeaf, ()>| {
286 if matches!(layer, ExprFrame::Set(FiltersetLeaf::Group(_, _))) {
287 found = true;
288 }
289 });
290 found
291 }
292}
293
294impl NameMatcher {
295 pub fn is_match(&self, input: &str) -> bool {
297 match self {
298 Self::Equal { value, .. } => value == input,
299 Self::Contains { value, .. } => input.contains(value),
300 Self::Glob { glob, .. } => glob.is_match(input),
301 Self::Regex(reg) => reg.is_match(input),
302 }
303 }
304}
305
306impl FiltersetLeaf {
307 fn matches_test_impl(
308 &self,
309 query: &TestQuery<'_>,
310 cx: &EvalContext,
311 groups: &impl GroupResolver,
312 ) -> bool {
313 match self {
314 Self::All => true,
315 Self::None => false,
316 Self::Default => cx.default_filter.matches_test_impl(query, cx, &NoGroups),
320 Self::Test(matcher, _) => matcher.is_match(query.test_name.as_str()),
321 Self::Binary(matcher, _) => matcher.is_match(query.binary_query.binary_name),
322 Self::BinaryId(matcher, _) => matcher.is_match(query.binary_query.binary_id.as_str()),
323 Self::Platform(platform, _) => query.binary_query.platform == *platform,
324 Self::Kind(matcher, _) => matcher.is_match(query.binary_query.kind.as_str()),
325 Self::Packages(packages) => packages.contains(query.binary_query.package_id),
326 Self::Group(matcher, _) => groups.resolve_group(query, matcher),
327 }
328 }
329
330 fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext) -> Option<bool> {
331 match self {
332 Self::All => Logic::top(),
333 Self::None => Logic::bottom(),
334 Self::Default => cx.default_filter.matches_binary(query, cx),
335 Self::Test(_, _) => None,
336 Self::Binary(matcher, _) => Some(matcher.is_match(query.binary_name)),
337 Self::BinaryId(matcher, _) => Some(matcher.is_match(query.binary_id.as_str())),
338 Self::Platform(platform, _) => Some(query.platform == *platform),
339 Self::Kind(matcher, _) => Some(matcher.is_match(query.kind.as_str())),
340 Self::Packages(packages) => Some(packages.contains(query.package_id)),
341 Self::Group(_, _) => None,
343 }
344 }
345}
346
347#[derive(Debug)]
352pub enum KnownGroups {
353 Known { custom_groups: HashSet<String> },
360
361 Unavailable,
365}
366
367impl KnownGroups {
368 pub(crate) fn matches(&self, matcher: &NameMatcher) -> bool {
376 let custom_groups = match self {
377 KnownGroups::Known { custom_groups } => custom_groups,
378 KnownGroups::Unavailable => panic!(
379 "group() validation data is unavailable; \
380 this is a nextest bug (group() should have been banned \
381 during compilation for this filterset kind)"
382 ),
383 };
384
385 if matcher.is_match(nextest_metadata::GLOBAL_TEST_GROUP) {
387 return true;
388 }
389
390 match matcher {
391 NameMatcher::Equal { value, .. } => custom_groups.contains(value.as_str()),
392 _ => {
393 custom_groups.iter().any(|g| matcher.is_match(g))
396 }
397 }
398 }
399}
400
401#[derive(Debug)]
403pub struct ParseContext<'g> {
404 graph: &'g PackageGraph,
406
407 cache: OnceLock<ParseContextCache<'g>>,
409}
410
411impl<'g> ParseContext<'g> {
412 #[inline]
414 pub fn new(graph: &'g PackageGraph) -> Self {
415 Self {
416 graph,
417 cache: OnceLock::new(),
418 }
419 }
420
421 #[inline]
423 pub fn graph(&self) -> &'g PackageGraph {
424 self.graph
425 }
426
427 pub(crate) fn make_cache(&self) -> &ParseContextCache<'g> {
428 self.cache
429 .get_or_init(|| ParseContextCache::new(self.graph))
430 }
431}
432
433#[derive(Debug)]
434pub(crate) struct ParseContextCache<'g> {
435 pub(crate) workspace_packages: Vec<PackageMetadata<'g>>,
436 pub(crate) binary_ids: HashSet<SmolStr>,
439 pub(crate) binary_names: HashSet<&'g str>,
440}
441
442impl<'g> ParseContextCache<'g> {
443 fn new(graph: &'g PackageGraph) -> Self {
444 let workspace_packages: Vec<_> = graph
445 .resolve_workspace()
446 .packages(guppy::graph::DependencyDirection::Forward)
447 .collect();
448 let (binary_ids, binary_names) = workspace_packages
449 .iter()
450 .flat_map(|pkg| {
451 pkg.build_targets().filter_map(|bt| {
452 let kind = compute_kind(&bt.id())?;
453 let binary_id = RustBinaryId::from_parts(pkg.name(), &kind, bt.name());
454 Some((SmolStr::new(binary_id.as_str()), bt.name()))
455 })
456 })
457 .unzip();
458
459 Self {
460 workspace_packages,
461 binary_ids,
462 binary_names,
463 }
464 }
465}
466
467fn compute_kind(id: &BuildTargetId<'_>) -> Option<RustTestBinaryKind> {
468 match id {
469 BuildTargetId::Library => Some(RustTestBinaryKind::LIB),
473 BuildTargetId::Benchmark(_) => Some(RustTestBinaryKind::BENCH),
474 BuildTargetId::Example(_) => Some(RustTestBinaryKind::EXAMPLE),
475 BuildTargetId::BuildScript => {
476 None
478 }
479 BuildTargetId::Binary(_) => Some(RustTestBinaryKind::BIN),
480 BuildTargetId::Test(_) => Some(RustTestBinaryKind::TEST),
481 _ => panic!("unknown build target id: {id:?}"),
482 }
483}
484
485#[derive(Copy, Clone, Debug, PartialEq, Eq)]
487pub enum FiltersetKind {
488 Test,
490
491 TestArchive,
493
494 OverrideFilter,
500
501 DefaultFilter,
506}
507
508impl fmt::Display for FiltersetKind {
509 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510 match self {
511 Self::Test => write!(f, "test"),
512 Self::OverrideFilter => write!(f, "override-filter"),
513 Self::TestArchive => write!(f, "archive-filter"),
514 Self::DefaultFilter => write!(f, "default-filter"),
515 }
516 }
517}
518
519#[derive(Copy, Clone, Debug)]
521pub struct EvalContext<'a> {
522 pub default_filter: &'a CompiledExpr,
524}
525
526trait GroupResolver {
530 fn resolve_group(&self, query: &TestQuery<'_>, matcher: &NameMatcher) -> bool;
531}
532
533struct NoGroups;
537
538impl GroupResolver for NoGroups {
539 fn resolve_group(&self, _query: &TestQuery<'_>, _matcher: &NameMatcher) -> bool {
540 panic!(
541 "group() predicate in expression where groups are not expected; \
542 this is a nextest bug (group() should be banned during compilation \
543 for this filterset kind)"
544 )
545 }
546}
547
548struct WithGroups<'a>(&'a dyn GroupLookup);
551
552impl GroupResolver for WithGroups<'_> {
553 fn resolve_group(&self, query: &TestQuery<'_>, matcher: &NameMatcher) -> bool {
554 self.0.is_member_test(query, matcher)
555 }
556}
557
558impl Filterset {
559 pub fn parse(
561 input: String,
562 cx: &ParseContext<'_>,
563 kind: FiltersetKind,
564 known_groups: &KnownGroups,
565 ) -> Result<Self, FiltersetParseErrors> {
566 let mut errors = Vec::new();
567 match parse(new_span(&input, &mut errors)) {
568 Ok(parsed_expr) => {
569 if !errors.is_empty() {
570 return Err(FiltersetParseErrors::new(input.clone(), errors));
571 }
572
573 match parsed_expr {
574 ExprResult::Valid(parsed) => {
575 let compiled = crate::compile::compile(&parsed, cx, kind, known_groups)
576 .map_err(|errors| FiltersetParseErrors::new(input.clone(), errors))?;
577 Ok(Self {
578 input,
579 parsed,
580 compiled,
581 })
582 }
583 _ => {
584 Err(FiltersetParseErrors::new(
589 input,
590 vec![ParseSingleError::Unknown],
591 ))
592 }
593 }
594 }
595 Err(_) => {
596 Err(FiltersetParseErrors::new(
600 input,
601 vec![ParseSingleError::Unknown],
602 ))
603 }
604 }
605 }
606
607 pub fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext<'_>) -> Option<bool> {
614 self.compiled.matches_binary(query, cx)
615 }
616
617 pub fn matches_test(&self, query: &TestQuery<'_>, cx: &EvalContext<'_>) -> bool {
622 self.compiled.matches_test(query, cx)
623 }
624
625 pub fn matches_test_with_groups(
630 &self,
631 query: &TestQuery<'_>,
632 cx: &EvalContext<'_>,
633 groups: &dyn GroupLookup,
634 ) -> bool {
635 self.compiled.matches_test_with_groups(query, cx, groups)
636 }
637
638 pub fn needs_deps(raw_expr: &str) -> bool {
640 raw_expr.contains("deps")
642 }
643}
644
645trait Logic {
650 fn top() -> Self;
652
653 fn bottom() -> Self;
655
656 fn logic_and(self, other: Self) -> Self;
658
659 fn logic_or(self, other: Self) -> Self;
661
662 fn logic_not(self) -> Self;
664}
665
666impl Logic for bool {
668 #[inline]
669 fn top() -> Self {
670 true
671 }
672
673 #[inline]
674 fn bottom() -> Self {
675 false
676 }
677
678 #[inline]
679 fn logic_and(self, other: Self) -> Self {
680 self && other
681 }
682
683 #[inline]
684 fn logic_or(self, other: Self) -> Self {
685 self || other
686 }
687
688 #[inline]
689 fn logic_not(self) -> Self {
690 !self
691 }
692}
693
694impl Logic for Option<bool> {
699 #[inline]
700 fn top() -> Self {
701 Some(true)
702 }
703
704 #[inline]
705 fn bottom() -> Self {
706 Some(false)
707 }
708
709 #[inline]
710 fn logic_and(self, other: Self) -> Self {
711 match (self, other) {
712 (Some(false), _) | (_, Some(false)) => Some(false),
714 (Some(true), Some(true)) => Some(true),
716 _ => None,
718 }
719 }
720
721 #[inline]
722 fn logic_or(self, other: Self) -> Self {
723 match (self, other) {
724 (Some(true), _) | (_, Some(true)) => Some(true),
726 (Some(false), Some(false)) => Some(false),
728 _ => None,
730 }
731 }
732
733 #[inline]
734 fn logic_not(self) -> Self {
735 self.map(|v| !v)
736 }
737}
738
739pub(crate) enum ExprFrame<Set, A> {
740 Not(A),
741 Union(A, A),
742 Intersection(A, A),
743 Difference(A, A),
744 Parens(A),
745 Set(Set),
746}
747
748impl<Set> MappableFrame for ExprFrame<Set, PartiallyApplied> {
749 type Frame<Next> = ExprFrame<Set, Next>;
750
751 fn map_frame<A, B>(input: Self::Frame<A>, mut f: impl FnMut(A) -> B) -> Self::Frame<B> {
752 use ExprFrame::*;
753 match input {
754 Not(a) => Not(f(a)),
755 Union(a, b) => {
759 let b = f(b);
760 let a = f(a);
761 Union(a, b)
762 }
763 Intersection(a, b) => {
764 let b = f(b);
765 let a = f(a);
766 Intersection(a, b)
767 }
768 Difference(a, b) => {
769 let b = f(b);
770 let a = f(a);
771 Difference(a, b)
772 }
773 Parens(a) => Parens(f(a)),
774 Set(f) => Set(f),
775 }
776 }
777}
778
779pub(crate) struct Wrapped<T>(pub(crate) T);
781
782impl<'a> Collapsible for Wrapped<&'a CompiledExpr> {
783 type FrameToken = ExprFrame<&'a FiltersetLeaf, PartiallyApplied>;
784
785 fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
786 match self.0 {
787 CompiledExpr::Not(a) => ExprFrame::Not(Wrapped(a.as_ref())),
788 CompiledExpr::Union(a, b) => ExprFrame::Union(Wrapped(a.as_ref()), Wrapped(b.as_ref())),
789 CompiledExpr::Intersection(a, b) => {
790 ExprFrame::Intersection(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
791 }
792 CompiledExpr::Set(f) => ExprFrame::Set(f),
793 }
794 }
795}
796
797impl<'a> Collapsible for Wrapped<&'a ParsedExpr> {
798 type FrameToken = ExprFrame<&'a ParsedLeaf, PartiallyApplied>;
799
800 fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
801 match self.0 {
802 ParsedExpr::Not(_, a) => ExprFrame::Not(Wrapped(a.as_ref())),
803 ParsedExpr::Union(_, a, b) => {
804 ExprFrame::Union(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
805 }
806 ParsedExpr::Intersection(_, a, b) => {
807 ExprFrame::Intersection(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
808 }
809 ParsedExpr::Difference(_, a, b) => {
810 ExprFrame::Difference(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
811 }
812 ParsedExpr::Parens(a) => ExprFrame::Parens(Wrapped(a.as_ref())),
813 ParsedExpr::Set(f) => ExprFrame::Set(f),
814 }
815 }
816}