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}