tytanic_filter/lib.rs
1//! A functional set-based DSL for filtering tests in the `tytanic` test runner.
2//! See the language [reference] and [guide] for more info.
3//!
4//! Note that this is generic over the test type because it is also used in
5//! internal projects of the author. This means that contribution is welcome,
6//! but may be rejected for various reasons not apparent in `tytanic` only.
7//!
8//! This library is still unstable to some degree, the inner test set types are
9//! very opaque and not easily printed or inspected because of this, this may
10//! change in the future.
11//!
12//! [reference]: https://typst-community.github.io/tytanic/reference/test-sets/index.html
13//! [guide]: https://typst-community.github.io/tytanic/guides/test-sets.html
14
15use ecow::EcoString;
16use eval::Value;
17use thiserror::Error;
18
19use crate::eval::Eval;
20use crate::eval::Test;
21
22pub mod ast;
23pub mod eval;
24
25/// A generic test set expression filter, this filter checks whether a test
26/// should be filtered out by checking it against the inner test set within its
27/// evaluation context.
28///
29/// This also includes extra parsing logic for the special `all:` modifier
30/// prefix, which is not part of the test set grammar, but can be used by the
31/// caller to handle instances where multiple tests match but only one is
32/// usually expected.
33#[derive(Debug, Clone)]
34pub struct ExpressionFilter<T: 'static> {
35 input: EcoString,
36 all: bool,
37 ctx: eval::Context<T>,
38 set: eval::Set<T>,
39}
40
41impl<T: Test> ExpressionFilter<T> {
42 /// Parse and evaluate a string into a test set with the given context.
43 pub fn new<S: Into<EcoString>>(ctx: eval::Context<T>, input: S) -> Result<Self, Error> {
44 let input = input.into();
45
46 let (all, expr) = input
47 .strip_prefix("all:")
48 .map(|rest| (true, rest))
49 .unwrap_or((false, &input));
50
51 let set = ast::parse(expr)?.eval(&ctx).and_then(Value::expect_type)?;
52
53 Ok(Self {
54 input,
55 all,
56 ctx,
57 set,
58 })
59 }
60}
61
62impl<T> ExpressionFilter<T> {
63 /// The input expression the inner test set was parsed from.
64 pub fn input(&self) -> &str {
65 &self.input
66 }
67
68 /// Whether this test set expression has the special `all:` modifier.
69 ///
70 /// Handling this is up to the caller and has no impact on the inner test
71 /// set.
72 pub fn all(&self) -> bool {
73 self.all
74 }
75
76 /// The context used to evaluate the inner test set.
77 pub fn ctx(&self) -> &eval::Context<T> {
78 &self.ctx
79 }
80
81 /// The inner test set.
82 pub fn set(&self) -> &eval::Set<T> {
83 &self.set
84 }
85}
86
87impl<T> ExpressionFilter<T> {
88 /// Applies a function to the inner test set, this is useful for
89 /// optimization or for adding implicit test sets like wrapping a test set
90 /// in `(...) ~ skip()`.
91 pub fn map<F>(self, f: F) -> Self
92 where
93 F: FnOnce(eval::Set<T>) -> eval::Set<T>,
94 {
95 Self {
96 set: f(self.set),
97 ..self
98 }
99 }
100}
101
102impl<T> ExpressionFilter<T> {
103 /// Whether the given test is contained in this test set. Note that this
104 /// means a return value of `true` should _not_ be filtered out, but
105 /// included in the set of tests to operate on.
106 pub fn contains(&self, test: &T) -> Result<bool, eval::Error> {
107 self.set.contains(&self.ctx, test)
108 }
109}
110
111/// Returned by [`ExpressionFilter::new`].
112#[derive(Debug, Error)]
113pub enum Error {
114 /// An error occurred during parsing.
115 #[error(transparent)]
116 Parse(#[from] ast::Error),
117
118 /// An error occurred during evaluation.
119 #[error(transparent)]
120 Eval(#[from] eval::Error),
121}