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
27pub 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 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 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 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 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)]
169pub enum TestCaseInput {
171 InMemory(Arc<str>),
173 FromFile {
175 filepath: Arc<str>,
177 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
275pub 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 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 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
364pub 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 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 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}