test_dsl/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::collections::HashMap;
4use std::marker::PhantomData;
5use std::sync::Arc;
6
7use argument::BoxedArguments;
8use argument::ConditionChildren;
9use argument::VerbChildren;
10use condition::ErasedCondition;
11use error::TestError;
12use error::TestErrorCase;
13use verb::ErasedVerb;
14use verb::Verb;
15
16#[macro_use]
17mod macros;
18
19pub mod argument;
20pub mod condition;
21pub mod error;
22pub mod test_case;
23pub mod verb;
24pub use kdl;
25pub use miette;
26
27/// The main type of the crate
28///
29/// It contains all available verbs and conditions, and is used to derive
30/// [`TestCase`](test_case::TestCase)s.
31pub struct TestDsl<H> {
32    verbs: HashMap<String, ErasedVerb<H>>,
33    conditions: HashMap<String, ErasedCondition<H>>,
34}
35
36impl<H> std::fmt::Debug for TestDsl<H> {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        f.debug_struct("TestDsl").finish_non_exhaustive()
39    }
40}
41
42impl<H: 'static> Default for TestDsl<H> {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl<H: 'static> TestDsl<H> {
49    /// Create an empty [`TestDsl`]
50    pub fn new() -> Self {
51        let mut dsl = TestDsl {
52            verbs: HashMap::default(),
53            conditions: HashMap::default(),
54        };
55
56        dsl.add_verb("repeat", Repeat);
57        dsl.add_verb("group", Group);
58        dsl.add_verb("assert", AssertConditions);
59
60        dsl
61    }
62
63    /// Add a single verb
64    ///
65    /// The name is used as-is in your testcases, the arguments are up to each individual [`Verb`] implementation.
66    ///
67    /// See [`FunctionVerb`](verb::FunctionVerb) for an easy to use way of defining verbs.
68    pub fn add_verb(&mut self, name: impl AsRef<str>, verb: impl Verb<H>) {
69        let existing = self
70            .verbs
71            .insert(name.as_ref().to_string(), ErasedVerb::erase(verb));
72        assert!(existing.is_none());
73    }
74
75    /// Add a single condition
76    ///
77    /// The name is used as-is in your testcases, the arguments are up to each individual
78    /// [`Condition`](condition::Condition) implementation.
79    ///
80    /// See [`FunctionCondition`](condition::FunctionCondition) for an easy to use way of defining conditions.
81    pub fn add_condition(
82        &mut self,
83        name: impl AsRef<str>,
84        condition: impl condition::Condition<H>,
85    ) {
86        let existing = self
87            .conditions
88            .insert(name.as_ref().to_string(), ErasedCondition::erase(condition));
89
90        assert!(existing.is_none());
91    }
92
93    /// Parse a given document as a [`KdlDocument`](kdl::KdlDocument) and generate a
94    /// [`TestCase`](test_case::TestCase) out of it.
95    pub fn parse_testcase(
96        &self,
97        input: impl Into<TestCaseInput>,
98    ) -> Result<Vec<test_case::TestCase<H>>, error::TestParseError> {
99        let input = input.into();
100        let document = kdl::KdlDocument::parse(input.content())?;
101
102        let mut cases = vec![];
103
104        let mut errors = vec![];
105
106        for testcase_node in document.nodes() {
107            if testcase_node.name().value() != "testcase" {
108                errors.push(error::TestErrorCase::NotTestcase {
109                    span: testcase_node.name().span(),
110                });
111
112                continue;
113            }
114
115            let mut testcase = test_case::TestCase::new(input.clone());
116
117            for node in testcase_node.iter_children() {
118                match VerbInstance::with_test_dsl(self, node) {
119                    Ok(verb) => testcase.cases.push(verb),
120                    Err(e) => errors.push(e),
121                }
122            }
123
124            cases.push(testcase);
125        }
126
127        if !errors.is_empty() {
128            return Err(error::TestParseError {
129                errors,
130                source_code: Some(input.clone()),
131            });
132        }
133
134        Ok(cases)
135    }
136
137    fn get_condition_for_node(
138        &self,
139        condition_node: &kdl::KdlNode,
140    ) -> Result<ErasedCondition<H>, error::TestErrorCase> {
141        let condition = self
142            .conditions
143            .get(condition_node.name().value())
144            .ok_or_else(|| error::TestErrorCase::UnknownCondition {
145                condition: condition_node.name().span(),
146            })?
147            .clone();
148
149        Ok(condition)
150    }
151
152    fn get_verb_for_node(
153        &self,
154        verb_node: &kdl::KdlNode,
155    ) -> Result<ErasedVerb<H>, error::TestErrorCase> {
156        let verb = self
157            .verbs
158            .get(verb_node.name().value())
159            .ok_or_else(|| error::TestErrorCase::UnknownVerb {
160                verb: verb_node.name().span(),
161            })?
162            .clone();
163
164        Ok(verb)
165    }
166}
167
168#[derive(Debug, Clone)]
169/// The input to a [`TestCase`](test_case::TestCase)
170pub enum TestCaseInput {
171    /// Input that is not backed by a file
172    InMemory(Arc<str>),
173    /// Input that is backed by a file
174    FromFile {
175        /// The filepath of the file the contents are read from
176        filepath: Arc<str>,
177        /// The content of the file
178        contents: Arc<str>,
179    },
180}
181
182impl From<&str> for TestCaseInput {
183    fn from(value: &str) -> Self {
184        TestCaseInput::InMemory(Arc::from(value))
185    }
186}
187
188impl miette::SourceCode for TestCaseInput {
189    fn read_span<'a>(
190        &'a self,
191        span: &miette::SourceSpan,
192        context_lines_before: usize,
193        context_lines_after: usize,
194    ) -> Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
195        match self {
196            TestCaseInput::InMemory(content) => {
197                content.read_span(span, context_lines_before, context_lines_after)
198            }
199            TestCaseInput::FromFile {
200                filepath: filename,
201                contents,
202            } => {
203                let inner_contents =
204                    contents.read_span(span, context_lines_before, context_lines_after)?;
205                let mut contents = miette::MietteSpanContents::new_named(
206                    filename.to_string(),
207                    inner_contents.data(),
208                    *inner_contents.span(),
209                    inner_contents.line(),
210                    inner_contents.column(),
211                    inner_contents.line_count(),
212                );
213                contents = contents.with_language("kdl");
214                Ok(Box::new(contents))
215            }
216        }
217    }
218}
219
220impl TestCaseInput {
221    fn content(&self) -> &str {
222        match self {
223            TestCaseInput::InMemory(content) => content,
224            TestCaseInput::FromFile { contents, .. } => contents,
225        }
226    }
227}
228
229#[derive(Debug, Clone)]
230struct AssertConditions;
231
232impl<H: 'static> Verb<H> for AssertConditions {
233    type Arguments = ConditionChildren<H, ((),)>;
234    fn run(&self, harness: &mut H, arguments: &Self::Arguments) -> miette::Result<()> {
235        for child in arguments.children() {
236            child.run(harness)?;
237        }
238
239        Ok(())
240    }
241}
242
243#[derive(Debug, Clone)]
244struct Group;
245
246impl<H: 'static> Verb<H> for Group {
247    type Arguments = VerbChildren<H, ((),)>;
248    fn run(&self, harness: &mut H, arguments: &Self::Arguments) -> miette::Result<()> {
249        for child in arguments.children() {
250            child.run(harness)?;
251        }
252
253        Ok(())
254    }
255}
256
257#[derive(Debug, Clone)]
258struct Repeat;
259
260impl<H: 'static> Verb<H> for Repeat {
261    type Arguments = VerbChildren<H, (usize,)>;
262    fn run(&self, harness: &mut H, arguments: &Self::Arguments) -> miette::Result<()> {
263        let (times,) = *arguments.parameters();
264
265        for _ in 0..times {
266            for child in arguments.children() {
267                child.run(harness)?;
268            }
269        }
270
271        Ok(())
272    }
273}
274
275/// An instance of a [`Condition`](condition::Condition)
276pub struct ConditionInstance<H> {
277    _pd: PhantomData<fn(H)>,
278    condition: ErasedCondition<H>,
279    arguments: Box<dyn BoxedArguments<H>>,
280    node: kdl::KdlNode,
281}
282
283impl<H: 'static> Clone for ConditionInstance<H> {
284    fn clone(&self) -> Self {
285        Self {
286            _pd: self._pd,
287            condition: self.condition.clone(),
288            arguments: self.arguments.clone(),
289            node: self.node.clone(),
290        }
291    }
292}
293
294impl<H> std::fmt::Debug for ConditionInstance<H> {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        f.debug_struct("ConditionInstance")
297            .field("_pd", &self._pd)
298            .field("condition", &self.condition)
299            .field("arguments", &self.arguments)
300            .field("node", &self.node)
301            .finish()
302    }
303}
304
305impl<H: 'static> ConditionInstance<H> {
306    /// Create a new instance with the given node and [`TestDsl`]
307    pub fn with_test_dsl(
308        test_dsl: &TestDsl<H>,
309        node: &kdl::KdlNode,
310    ) -> Result<Self, TestErrorCase> {
311        let condition = test_dsl.get_condition_for_node(node)?;
312
313        let arguments = condition.parse_args(test_dsl, node)?;
314
315        Ok(ConditionInstance {
316            _pd: PhantomData,
317            condition,
318            arguments,
319            node: node.clone(),
320        })
321    }
322
323    /// Run the condition
324    ///
325    /// This returns an error if:
326    /// - The condition returns [`Ok(false)`](Ok)
327    /// - It returns an [`Err`]
328    /// - It [`panic`]s
329    pub fn run(&self, harness: &mut H) -> Result<(), TestError> {
330        let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
331            self.condition
332                .check_now(harness, self.arguments.as_dyn_any())
333        }));
334
335        match res {
336            Ok(Ok(true)) => Ok(()),
337            Ok(Ok(false)) => Err(TestError::ConditionFailed {
338                span: self.node.span(),
339            }),
340            Ok(Err(error)) => Err(TestError::Error {
341                error,
342                span: self.node.span(),
343            }),
344            Err(payload) => {
345                let mut message = "Something went wrong".to_string();
346
347                if let Some(msg) = payload.downcast_ref::<&str>() {
348                    message = msg.to_string();
349                }
350
351                if let Some(msg) = payload.downcast_ref::<String>() {
352                    message.clone_from(msg);
353                }
354
355                Err(TestError::Panic {
356                    error: miette::Report::msg(message),
357                    span: self.node.span(),
358                })
359            }
360        }
361    }
362}
363
364/// An instance of a [`Verb`]
365pub struct VerbInstance<H> {
366    _pd: PhantomData<fn(H)>,
367    verb: ErasedVerb<H>,
368    arguments: Box<dyn BoxedArguments<H>>,
369    node: kdl::KdlNode,
370}
371
372impl<H> std::fmt::Debug for VerbInstance<H> {
373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374        f.debug_struct("VerbInstance")
375            .field("_pd", &self._pd)
376            .field("verb", &self.verb)
377            .field("arguments", &self.arguments)
378            .field("node", &self.node)
379            .finish()
380    }
381}
382
383impl<H: 'static> Clone for VerbInstance<H> {
384    fn clone(&self) -> Self {
385        Self {
386            _pd: self._pd,
387            verb: self.verb.clone(),
388            arguments: self.arguments.clone(),
389            node: self.node.clone(),
390        }
391    }
392}
393
394impl<H: 'static> VerbInstance<H> {
395    /// Create a new instance with the given node and [`TestDsl`]
396    pub fn with_test_dsl(
397        test_dsl: &TestDsl<H>,
398        node: &kdl::KdlNode,
399    ) -> Result<Self, TestErrorCase> {
400        let verb = test_dsl.get_verb_for_node(node)?;
401
402        let arguments = verb.parse_args(test_dsl, node)?;
403
404        Ok(VerbInstance {
405            _pd: PhantomData,
406            verb,
407            arguments,
408            node: node.clone(),
409        })
410    }
411
412    /// Run the verb
413    ///
414    /// This returns an error if:
415    /// - It returns an [`Err`]
416    /// - It [`panic`]s
417    pub fn run(&self, harness: &mut H) -> Result<(), TestError> {
418        let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
419            self.verb.run(harness, self.arguments.as_dyn_any())
420        }));
421
422        match res {
423            Ok(Ok(())) => Ok(()),
424            Ok(Err(error)) => Err(TestError::Error {
425                error,
426                span: self.node.span(),
427            }),
428            Err(payload) => {
429                let mut message = "Something went wrong".to_string();
430
431                if let Some(msg) = payload.downcast_ref::<&str>() {
432                    message = msg.to_string();
433                }
434
435                if let Some(msg) = payload.downcast_ref::<String>() {
436                    message.clone_from(msg);
437                }
438
439                Err(TestError::Panic {
440                    error: miette::Report::msg(message),
441                    span: self.node.span(),
442                })
443            }
444        }
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use std::sync::atomic::AtomicUsize;
451
452    use crate::TestDsl;
453    use crate::verb::FunctionVerb;
454
455    struct ArithmeticHarness {
456        value: AtomicUsize,
457    }
458
459    #[test]
460    fn simple_test() {
461        let mut ts = TestDsl::<ArithmeticHarness>::new();
462        ts.add_verb(
463            "add_one",
464            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
465                ah.value.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
466
467                Ok(())
468            }),
469        );
470
471        ts.add_verb(
472            "mul_two",
473            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
474                let value = ah.value.load(std::sync::atomic::Ordering::SeqCst);
475                ah.value
476                    .store(value * 2, std::sync::atomic::Ordering::SeqCst);
477                Ok(())
478            }),
479        );
480
481        let tc = ts
482            .parse_testcase(
483                r#"
484            testcase {
485                add_one
486                add_one
487                mul_two
488            }
489            "#,
490            )
491            .unwrap();
492
493        let mut ah = ArithmeticHarness {
494            value: AtomicUsize::new(0),
495        };
496
497        tc[0].run(&mut ah).unwrap();
498
499        assert_eq!(ah.value.load(std::sync::atomic::Ordering::SeqCst), 4);
500    }
501
502    #[test]
503    fn repeat_test() {
504        let mut ts = TestDsl::<ArithmeticHarness>::new();
505        ts.add_verb(
506            "add_one",
507            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
508                ah.value.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
509
510                Ok(())
511            }),
512        );
513
514        ts.add_verb(
515            "mul_two",
516            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
517                let value = ah.value.load(std::sync::atomic::Ordering::SeqCst);
518                ah.value
519                    .store(value * 2, std::sync::atomic::Ordering::SeqCst);
520
521                Ok(())
522            }),
523        );
524
525        let tc = ts
526            .parse_testcase(
527                r#"
528            testcase {
529                repeat 2 {
530                    repeat 2 {
531                        add_one
532                        mul_two
533                    }
534                }
535            }
536            "#,
537            )
538            .unwrap();
539
540        let mut ah = ArithmeticHarness {
541            value: AtomicUsize::new(0),
542        };
543
544        tc[0].run(&mut ah).unwrap();
545
546        assert_eq!(ah.value.load(std::sync::atomic::Ordering::SeqCst), 30);
547    }
548
549    #[test]
550    fn check_arguments_work() {
551        let mut ts = TestDsl::<ArithmeticHarness>::new();
552        ts.add_verb(
553            "add_one",
554            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
555                ah.value.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
556
557                Ok(())
558            }),
559        );
560
561        ts.add_verb(
562            "add",
563            FunctionVerb::new(|ah: &mut ArithmeticHarness, num: usize| {
564                ah.value.fetch_add(num, std::sync::atomic::Ordering::SeqCst);
565                Ok(())
566            }),
567        );
568
569        ts.add_verb(
570            "mul_two",
571            FunctionVerb::new(|ah: &mut ArithmeticHarness| {
572                let value = ah.value.load(std::sync::atomic::Ordering::SeqCst);
573                ah.value
574                    .store(value * 2, std::sync::atomic::Ordering::SeqCst);
575                Ok(())
576            }),
577        );
578
579        let tc = ts
580            .parse_testcase(
581                r#"
582            testcase {
583                repeat 2 {
584                    repeat 2 {
585                        group {
586                            add 2
587                            mul_two
588                        }
589                    }
590                }
591            }
592            "#,
593            )
594            .unwrap();
595
596        let mut ah = ArithmeticHarness {
597            value: AtomicUsize::new(0),
598        };
599
600        tc[0].run(&mut ah).unwrap();
601
602        assert_eq!(ah.value.load(std::sync::atomic::Ordering::SeqCst), 60);
603    }
604}