test_dsl/
condition.rs

1//! Conditions allow to tests for invariants or expected actions
2
3use std::any::Any;
4use std::marker::PhantomData;
5
6use crate::BoxedArguments;
7use crate::argument::ParseArguments;
8use crate::argument::VerbArgument;
9use crate::error::TestErrorCase;
10
11/// A condition check for a given property
12///
13/// Conditions allow to check for anything you would find useful. For example, in a HTTP library,
14/// you can check that your cache contains a valid entry from a previous request.
15pub trait Condition<H>: std::fmt::Debug + Clone + 'static {
16    /// The arguments for this condition
17    type Arguments: ParseArguments<H>;
18
19    /// Run the check now, may or may not actually be implemented
20    ///
21    /// This is only useful for non-transient properties. For example "is this connected". It is
22    /// not a way to check for events.
23    ///
24    /// If the condition cannot properly support the concept of 'checking now' it is ok to simply
25    /// return an error.
26    fn check_now(&self, harness: &H, arguments: &Self::Arguments) -> miette::Result<bool>;
27
28    /// Wait until a given condition evaluates to a meaningful value
29    ///
30    /// Some properties of a system are not meaningful other than 'that they happened'. This could
31    /// be an event or some value changing.
32    ///
33    /// If the condition cannot properly support the concept of 'waiting until it has a value', it
34    /// is ok to simply return an error.
35    fn wait_until(&self, harness: &H, arguments: &Self::Arguments) -> miette::Result<bool>;
36}
37
38pub(crate) struct ErasedCondition<H> {
39    condition: Box<dyn Any>,
40    fn_parse_args:
41        fn(&crate::TestDsl<H>, &kdl::KdlNode) -> Result<Box<dyn BoxedArguments<H>>, TestErrorCase>,
42    fn_check_now: fn(&dyn Any, &H, &dyn Any) -> miette::Result<bool>,
43    fn_wait_util: fn(&dyn Any, &H, &dyn Any) -> miette::Result<bool>,
44    fn_clone: fn(&dyn Any) -> Box<dyn Any>,
45}
46
47impl<H> std::fmt::Debug for ErasedCondition<H> {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("ErasedCondition")
50            .field("condition", &self.condition)
51            .field("fn_parse_args", &self.fn_parse_args)
52            .field("fn_check_now", &self.fn_check_now)
53            .field("fn_wait_util", &self.fn_wait_util)
54            .field("fn_clone", &self.fn_clone)
55            .finish()
56    }
57}
58
59impl<H> Clone for ErasedCondition<H> {
60    fn clone(&self) -> Self {
61        Self {
62            condition: (self.fn_clone)(&*self.condition),
63            fn_parse_args: self.fn_parse_args,
64            fn_check_now: self.fn_check_now,
65            fn_wait_util: self.fn_wait_util,
66            fn_clone: self.fn_clone,
67        }
68    }
69}
70
71impl<H> ErasedCondition<H> {
72    pub(crate) fn erase<C>(condition: C) -> Self
73    where
74        C: Condition<H>,
75    {
76        ErasedCondition {
77            condition: Box::new(condition),
78            fn_parse_args: |test_dsl, node| {
79                <C::Arguments as ParseArguments<H>>::parse(test_dsl, node).map(|a| {
80                    let args = Box::new(a);
81                    args as _
82                })
83            },
84            fn_check_now: |this, harness, arguments| {
85                let this: &C = this.downcast_ref().unwrap();
86                let arguments: &C::Arguments = arguments.downcast_ref().unwrap();
87
88                this.check_now(harness, arguments)
89            },
90            fn_wait_util: |this, harness, arguments| {
91                let this: &C = this.downcast_ref().unwrap();
92                let arguments: &C::Arguments = arguments.downcast_ref().unwrap();
93
94                this.wait_until(harness, arguments)
95            },
96            fn_clone: |this| {
97                let this: &C = this.downcast_ref().unwrap();
98
99                Box::new(this.clone())
100            },
101        }
102    }
103
104    pub(crate) fn parse_args(
105        &self,
106        test_dsl: &crate::TestDsl<H>,
107        node: &kdl::KdlNode,
108    ) -> Result<Box<dyn BoxedArguments<H>>, TestErrorCase> {
109        (self.fn_parse_args)(test_dsl, node)
110    }
111
112    pub(crate) fn check_now(&self, harness: &H, arguments: &dyn Any) -> miette::Result<bool> {
113        (self.fn_check_now)(&*self.condition, harness, arguments)
114    }
115}
116
117/// A [`Checker`] is the actual instance that executes when a condition evaluates.
118///
119/// It is mostly used with the [`FunctionCondition`] struct when given a closure/function.
120///
121/// It is implemented for closures of up to 16 arguments
122pub trait Checker<H, T>: Clone + 'static {
123    /// Execute the check with the given node
124    fn check(&self, harness: &H, arguments: &T) -> miette::Result<bool>;
125}
126
127struct BoxedChecker<H, T> {
128    checker: Box<dyn Any>,
129    check_fn: fn(&dyn Any, harness: &H, node: &T) -> miette::Result<bool>,
130    clone_fn: fn(&dyn Any) -> Box<dyn Any>,
131}
132
133impl<H, T> std::fmt::Debug for BoxedChecker<H, T> {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        f.debug_struct("BoxedChecker")
136            .field("checker", &self.checker)
137            .field("check_fn", &self.check_fn)
138            .field("clone_fn", &self.clone_fn)
139            .finish()
140    }
141}
142
143impl<H, T> BoxedChecker<H, T> {
144    fn new<C>(checker: C) -> Self
145    where
146        C: Checker<H, T>,
147    {
148        BoxedChecker {
149            checker: Box::new(checker),
150            check_fn: |this, harness, node| {
151                let this: &C = this.downcast_ref().unwrap();
152
153                this.check(harness, node)
154            },
155            clone_fn: |this| {
156                let this: &C = this.downcast_ref().unwrap();
157
158                Box::new(this.clone())
159            },
160        }
161    }
162
163    fn check(&self, harness: &H, node: &T) -> miette::Result<bool> {
164        (self.check_fn)(&*self.checker, harness, node)
165    }
166}
167
168impl<H, T> Clone for BoxedChecker<H, T> {
169    fn clone(&self) -> Self {
170        BoxedChecker {
171            checker: (self.clone_fn)(&*self.checker),
172            check_fn: self.check_fn,
173            clone_fn: self.clone_fn,
174        }
175    }
176}
177
178/// A condition that can be used in test-cases
179///
180/// Depending on how it is constructed, it may or may not be able to be used in direct or waiting
181/// contexts
182pub struct FunctionCondition<H, T> {
183    now: Option<BoxedChecker<H, T>>,
184    wait: Option<BoxedChecker<H, T>>,
185    _pd: PhantomData<fn(H)>,
186}
187
188impl<H, T> std::fmt::Debug for FunctionCondition<H, T> {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        f.debug_struct("FunctionCondition")
191            .field("now", &self.now)
192            .field("wait", &self.wait)
193            .field("_pd", &self._pd)
194            .finish()
195    }
196}
197
198impl<H, T> FunctionCondition<H, T> {
199    /// Create a new [`FunctionCondition`] that can be called in direct contexts
200    ///
201    /// For example the `assert` verb allows you to verify multiple [`Condition`]s (of which [`FunctionCondition`] is one way to create one).
202    pub fn new_now<C>(now: C) -> Self
203    where
204        C: Checker<H, T>,
205    {
206        FunctionCondition {
207            now: Some(BoxedChecker::new(now)),
208            wait: None,
209            _pd: PhantomData,
210        }
211    }
212
213    /// Create a new [`FunctionCondition`] that can be called in waiting contexts
214    pub fn new_wait<C>(wait: C) -> Self
215    where
216        C: Checker<H, T>,
217    {
218        FunctionCondition {
219            now: None,
220            wait: Some(BoxedChecker::new(wait)),
221            _pd: PhantomData,
222        }
223    }
224
225    /// Create a new [`FunctionCondition`] that can be called in both direct and waiting contexts
226    pub fn new_now_and_wait<C>(both: C) -> Self
227    where
228        C: Checker<H, T>,
229    {
230        FunctionCondition {
231            now: Some(BoxedChecker::new(both.clone())),
232            wait: Some(BoxedChecker::new(both)),
233            _pd: PhantomData,
234        }
235    }
236
237    /// Allow this condition to also be used in direct contexts
238    pub fn with_now<C>(mut self, now: C) -> Self
239    where
240        C: Checker<H, T>,
241    {
242        self.now = Some(BoxedChecker::new(now));
243        self
244    }
245
246    /// Allow this condition to also be used in waiting contexts
247    pub fn with_wait<C>(mut self, wait: C) -> Self
248    where
249        C: Checker<H, T>,
250    {
251        self.wait = Some(BoxedChecker::new(wait));
252        self
253    }
254}
255
256impl<H, T> Clone for FunctionCondition<H, T> {
257    fn clone(&self) -> Self {
258        FunctionCondition {
259            now: self.now.clone(),
260            wait: self.wait.clone(),
261            _pd: PhantomData,
262        }
263    }
264}
265
266impl<H, F> Checker<H, ((),)> for F
267where
268    F: Fn(&H) -> miette::Result<bool>,
269    F: Clone + 'static,
270{
271    fn check(&self, harness: &H, _arguments: &((),)) -> miette::Result<bool> {
272        self(harness)
273    }
274}
275
276macro_rules! impl_callable {
277    (
278        [$($ty:ident),*], $last:ident
279    ) => {
280        #[allow(non_snake_case, unused_mut)]
281        impl<H, F, $($ty,)* $last> Checker<H, ($($ty,)* $last,)> for F
282            where
283                F: Fn(&H, $($ty,)* $last,) -> miette::Result<bool>,
284                F: Clone + 'static,
285                $( $ty: VerbArgument, )*
286                $last: VerbArgument,
287        {
288            fn check(&self, harness: &H, node: &($($ty,)* $last,)) -> miette::Result<bool> {
289                let ($($ty,)* $last,) = node.clone();
290                self(harness, $($ty,)* $last,)
291            }
292        }
293    };
294}
295
296all_the_tuples!(impl_callable);
297
298impl<H, T> Condition<H> for FunctionCondition<H, T>
299where
300    H: 'static,
301    T: ParseArguments<H>,
302{
303    type Arguments = T;
304    fn check_now(&self, harness: &H, arguments: &T) -> miette::Result<bool> {
305        let Some(check) = self.now.as_ref().map(|now| now.check(harness, arguments)) else {
306            return Err(TestErrorCase::InvalidCondition {
307                error: miette::miette!("FunctionCondition does not implement checking now"),
308            }
309            .into());
310        };
311
312        check
313    }
314
315    fn wait_until(&self, harness: &H, node: &T) -> miette::Result<bool> {
316        let Some(check) = self.wait.as_ref().map(|wait| wait.check(harness, node)) else {
317            return Err(TestErrorCase::InvalidCondition {
318                error: miette::miette!("FunctionCondition does not implement checking now"),
319            }
320            .into());
321        };
322
323        check
324    }
325}