test_dsl/
lib.rs

1#![deny(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use condition::TestCondition;
8use verb::TestVerb;
9
10pub mod arguments;
11pub mod condition;
12pub mod error;
13pub mod test_case;
14pub mod verb;
15pub use kdl;
16pub use miette;
17
18/// The main type of the crate
19///
20/// It contains all available verbs and conditions, and is used to derive
21/// [`TestCase`](test_case::TestCase)s.
22pub struct TestDsl<H> {
23    verbs: HashMap<String, Box<dyn TestVerb<H>>>,
24    conditions: HashMap<String, Box<dyn condition::TestCondition<H>>>,
25}
26
27impl<H> std::fmt::Debug for TestDsl<H> {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        f.debug_struct("TestDsl").finish_non_exhaustive()
30    }
31}
32
33impl<H: 'static> Default for TestDsl<H> {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl<H: 'static> TestDsl<H> {
40    /// Create an empty [`TestDsl`]
41    pub fn new() -> Self {
42        TestDsl {
43            verbs: HashMap::default(),
44            conditions: HashMap::default(),
45        }
46    }
47
48    /// Add a single verb
49    ///
50    /// The name is used as-is in your testcases, the arguments are up to each individual [`TestVerb`] implementation.
51    ///
52    /// See [`FunctionVerb`](verb::FunctionVerb) for an easy to use way of defining verbs.
53    pub fn add_verb(&mut self, name: impl AsRef<str>, verb: impl TestVerb<H>) {
54        let existing = self.verbs.insert(name.as_ref().to_string(), Box::new(verb));
55        assert!(existing.is_none());
56    }
57
58    /// Add a single condition
59    ///
60    /// The name is used as-is in your testcases, the arguments are up to each individual
61    /// [`TestCondition`] implementation.
62    ///
63    /// See [`Condition`](condition::Condition) for an easy to use way of defining conditions.
64    pub fn add_condition(
65        &mut self,
66        name: impl AsRef<str>,
67        condition: impl condition::TestCondition<H>,
68    ) {
69        let existing = self
70            .conditions
71            .insert(name.as_ref().to_string(), Box::new(condition));
72
73        assert!(existing.is_none());
74    }
75
76    /// Parse a given document as a [`KdlDocument`](kdl::KdlDocument) and generate a
77    /// [`TestCase`](test_case::TestCase) out of it.
78    pub fn parse_document(
79        &self,
80        input: miette::NamedSource<Arc<str>>,
81    ) -> Result<Vec<test_case::TestCase<H>>, error::TestParseError> {
82        let document = kdl::KdlDocument::parse(input.inner())?;
83
84        let mut cases = vec![];
85
86        let mut errors = vec![];
87
88        for testcase_node in document.nodes() {
89            if testcase_node.name().value() != "testcase" {
90                errors.push(error::TestErrorCase::NotTestcase {
91                    span: testcase_node.name().span(),
92                });
93
94                continue;
95            }
96
97            let mut testcase = test_case::TestCase::new(input.clone());
98
99            for verb_node in testcase_node.iter_children() {
100                match self.parse_verb(verb_node) {
101                    Ok(verb) => testcase.creators.push(verb),
102                    Err(e) => errors.push(e),
103                }
104            }
105
106            cases.push(testcase);
107        }
108
109        if !errors.is_empty() {
110            return Err(error::TestParseError {
111                errors,
112                source_code: Some(input.clone()),
113            });
114        }
115
116        Ok(cases)
117    }
118
119    fn parse_condition(
120        &self,
121        condition_node: &kdl::KdlNode,
122    ) -> Result<Box<dyn TestConditionCreator<H>>, error::TestErrorCase> {
123        self.conditions
124            .get(condition_node.name().value())
125            .ok_or_else(|| error::TestErrorCase::UnknownCondition {
126                condition: condition_node.name().span(),
127            })
128            .map(|cond| {
129                Box::new(DirectCondition {
130                    condition: cond.clone(),
131                    node: condition_node.clone(),
132                }) as Box<_>
133            })
134    }
135
136    fn parse_verb(
137        &self,
138        verb_node: &kdl::KdlNode,
139    ) -> Result<Box<dyn TestVerbCreator<H>>, error::TestErrorCase> {
140        match verb_node.name().value() {
141            "repeat" => {
142                let times = verb_node
143                    .get(0)
144                    .ok_or_else(|| error::TestErrorCase::MissingArgument {
145                        parent: verb_node.name().span(),
146                        missing: String::from("`repeat` takes one argument, the repetition count"),
147                    })?
148                    .as_integer()
149                    .ok_or_else(|| error::TestErrorCase::WrongArgumentType {
150                        parent: verb_node.name().span(),
151                        argument: verb_node.iter().next().unwrap().span(),
152                        expected: String::from("Expected an integer"),
153                    })? as usize;
154
155                let block = verb_node
156                    .iter_children()
157                    .map(|node| self.parse_verb(node))
158                    .collect::<Result<_, _>>()?;
159
160                Ok(Box::new(Repeat { times, block }))
161            }
162            "group" => Ok(Box::new(Group {
163                block: verb_node
164                    .iter_children()
165                    .map(|n| self.parse_verb(n))
166                    .collect::<Result<_, _>>()?,
167            })),
168            "assert" => Ok(Box::new(AssertConditions {
169                conditions: verb_node
170                    .iter_children()
171                    .map(|node| self.parse_condition(node))
172                    .collect::<Result<_, _>>()?,
173            })),
174            name => {
175                let verb = self
176                    .verbs
177                    .get(name)
178                    .ok_or_else(|| error::TestErrorCase::UnknownVerb {
179                        verb: verb_node.name().span(),
180                    })?
181                    .clone();
182
183                Ok(Box::new(DirectVerb {
184                    verb,
185                    node: verb_node.clone(),
186                }))
187            }
188        }
189    }
190}
191
192trait TestVerbCreator<H> {
193    fn get_test_verbs(&self) -> Box<dyn Iterator<Item = TestVerbInstance<'_, H>> + '_>;
194}
195
196trait TestConditionCreator<H> {
197    fn get_test_conditions(&self) -> Box<dyn Iterator<Item = TestConditionInstance<'_, H>> + '_>;
198}
199
200struct Group<H> {
201    block: Vec<Box<dyn TestVerbCreator<H>>>,
202}
203
204impl<H: 'static> TestVerbCreator<H> for Group<H> {
205    fn get_test_verbs(&self) -> Box<dyn Iterator<Item = TestVerbInstance<'_, H>> + '_> {
206        Box::new(self.block.iter().flat_map(|c| c.get_test_verbs()))
207    }
208}
209
210struct Repeat<H> {
211    times: usize,
212    block: Vec<Box<dyn TestVerbCreator<H>>>,
213}
214
215impl<H: 'static> TestVerbCreator<H> for Repeat<H> {
216    fn get_test_verbs(&self) -> Box<dyn Iterator<Item = TestVerbInstance<'_, H>> + '_> {
217        Box::new(
218            std::iter::repeat_with(|| self.block.iter().flat_map(|c| c.get_test_verbs()))
219                .take(self.times)
220                .flatten(),
221        )
222    }
223}
224
225struct DirectVerb<H> {
226    verb: Box<dyn TestVerb<H>>,
227    node: kdl::KdlNode,
228}
229
230impl<H: 'static> TestVerbCreator<H> for DirectVerb<H> {
231    fn get_test_verbs(&self) -> Box<dyn Iterator<Item = TestVerbInstance<'_, H>> + '_> {
232        Box::new(std::iter::once(TestVerbInstance {
233            verb: self.verb.clone(),
234            node: &self.node,
235        }))
236    }
237}
238
239struct DirectCondition<H> {
240    condition: Box<dyn TestCondition<H>>,
241    node: kdl::KdlNode,
242}
243
244impl<H: 'static> TestConditionCreator<H> for DirectCondition<H> {
245    fn get_test_conditions(&self) -> Box<dyn Iterator<Item = TestConditionInstance<'_, H>> + '_> {
246        Box::new(std::iter::once(TestConditionInstance {
247            condition: self.condition.clone(),
248            node: &self.node,
249        }))
250    }
251}
252
253struct AssertConditions<H> {
254    conditions: Vec<Box<dyn TestConditionCreator<H>>>,
255}
256
257impl<H: 'static> TestVerbCreator<H> for AssertConditions<H> {
258    fn get_test_verbs(&self) -> Box<dyn Iterator<Item = TestVerbInstance<'_, H>> + '_> {
259        Box::new(
260            self.conditions
261                .iter()
262                .flat_map(|cond| cond.get_test_conditions())
263                .map(|cond| TestVerbInstance {
264                    node: cond.node,
265                    verb: Box::new(AssertVerb {
266                        condition: cond.condition,
267                    }),
268                }),
269        )
270    }
271}
272
273struct AssertVerb<H> {
274    condition: Box<dyn TestCondition<H>>,
275}
276
277impl<H: 'static> TestVerb<H> for AssertVerb<H> {
278    fn run(&self, harness: &mut H, node: &kdl::KdlNode) -> Result<(), error::TestErrorCase> {
279        self.condition.check_now(harness, node).and_then(|res| {
280            res.then_some(())
281                .ok_or_else(|| error::TestErrorCase::Error {
282                    error: miette::miette!("Assert failed"),
283                    label: node.span(),
284                })
285        })
286    }
287
288    fn clone_box(&self) -> Box<dyn TestVerb<H>> {
289        Box::new(self.clone())
290    }
291}
292
293impl<H: 'static> Clone for AssertVerb<H> {
294    fn clone(&self) -> Self {
295        AssertVerb {
296            condition: self.condition.clone(),
297        }
298    }
299}
300
301struct TestConditionInstance<'p, H> {
302    condition: Box<dyn TestCondition<H>>,
303    node: &'p kdl::KdlNode,
304}
305
306struct TestVerbInstance<'p, H> {
307    verb: Box<dyn TestVerb<H>>,
308    node: &'p kdl::KdlNode,
309}
310
311impl<'p, H: 'static> TestVerbInstance<'p, H> {
312    fn run<'h>(&'p self, harness: &'h mut H) -> Result<(), error::TestErrorCase> {
313        self.verb.run(harness, self.node)
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use std::sync::Arc;
320    use std::sync::atomic::AtomicUsize;
321
322    use miette::NamedSource;
323
324    use crate::TestDsl;
325    use crate::verb::FunctionVerb;
326
327    struct ArithmeticHarness {
328        value: AtomicUsize,
329    }
330
331    #[test]
332    fn simple_test() {
333        let mut ts = TestDsl::<ArithmeticHarness>::new();
334        ts.add_verb(
335            "add_one",
336            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
337                ah.value.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
338
339                Ok(())
340            }),
341        );
342
343        ts.add_verb(
344            "mul_two",
345            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
346                let value = ah.value.load(std::sync::atomic::Ordering::SeqCst);
347                ah.value
348                    .store(value * 2, std::sync::atomic::Ordering::SeqCst);
349                Ok(())
350            }),
351        );
352
353        let tc = ts
354            .parse_document(NamedSource::new(
355                "test.kdl",
356                Arc::from(
357                    r#"
358            testcase {
359                add_one
360                add_one
361                mul_two
362            }
363            "#,
364                ),
365            ))
366            .unwrap();
367
368        let mut ah = ArithmeticHarness {
369            value: AtomicUsize::new(0),
370        };
371
372        tc[0].run(&mut ah).unwrap();
373
374        assert_eq!(ah.value.load(std::sync::atomic::Ordering::SeqCst), 4);
375    }
376
377    #[test]
378    fn repeat_test() {
379        let mut ts = TestDsl::<ArithmeticHarness>::new();
380        ts.add_verb(
381            "add_one",
382            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
383                ah.value.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
384
385                Ok(())
386            }),
387        );
388
389        ts.add_verb(
390            "mul_two",
391            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
392                let value = ah.value.load(std::sync::atomic::Ordering::SeqCst);
393                ah.value
394                    .store(value * 2, std::sync::atomic::Ordering::SeqCst);
395
396                Ok(())
397            }),
398        );
399
400        let tc = ts
401            .parse_document(NamedSource::new(
402                "test.kdl",
403                Arc::from(
404                    r#"
405            testcase {
406                repeat 2 {
407                    repeat 2 {
408                        add_one
409                        mul_two
410                    }
411                }
412            }
413            "#,
414                ),
415            ))
416            .unwrap();
417
418        let mut ah = ArithmeticHarness {
419            value: AtomicUsize::new(0),
420        };
421
422        tc[0].run(&mut ah).unwrap();
423
424        assert_eq!(ah.value.load(std::sync::atomic::Ordering::SeqCst), 30);
425    }
426
427    #[test]
428    fn check_arguments_work() {
429        let mut ts = TestDsl::<ArithmeticHarness>::new();
430        ts.add_verb(
431            "add_one",
432            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
433                ah.value.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
434
435                Ok(())
436            }),
437        );
438
439        ts.add_verb(
440            "add",
441            FunctionVerb::new(|ah: &mut ArithmeticHarness, num: usize| {
442                ah.value.fetch_add(num, std::sync::atomic::Ordering::SeqCst);
443                Ok(())
444            }),
445        );
446
447        ts.add_verb(
448            "mul_two",
449            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
450                let value = ah.value.load(std::sync::atomic::Ordering::SeqCst);
451                ah.value
452                    .store(value * 2, std::sync::atomic::Ordering::SeqCst);
453                Ok(())
454            }),
455        );
456
457        let tc = ts
458            .parse_document(NamedSource::new(
459                "test.kdl",
460                Arc::from(
461                    r#"
462            testcase {
463                repeat 2 {
464                    repeat 2 {
465                        group {
466                            add 2
467                            mul_two
468                        }
469                    }
470                }
471            }
472            "#,
473                ),
474            ))
475            .unwrap();
476
477        let mut ah = ArithmeticHarness {
478            value: AtomicUsize::new(0),
479        };
480
481        tc[0].run(&mut ah).unwrap();
482
483        assert_eq!(ah.value.load(std::sync::atomic::Ordering::SeqCst), 60);
484    }
485}