tapconsooomer/
lib.rs

1extern crate pest;
2#[macro_use]
3extern crate pest_derive;
4
5use anyhow::{anyhow, Result};
6use pest::{
7    iterators::{Pair, Pairs},
8    Parser,
9};
10use serde::Serialize;
11
12#[derive(Parser)]
13#[grammar = "tap14.pest"]
14pub struct TAPParser;
15
16/// The TAP [`Preamble`] declares the start of a TAP document.
17#[derive(Debug, Serialize)]
18pub struct Preamble<'a> {
19    /// TAP specification version. Can be any semantic version string (e.g. `14` or `14.1.3`).
20    pub version: &'a str,
21}
22
23/// The [`Plan`] tells how many tests will be run, or how many tests have run.
24#[derive(Debug, Serialize)]
25pub struct Plan<'a> {
26    /// ID of first planned test. _Should_ always start with `1`.
27    pub first: i32,
28    /// ID of last planned test. A value of `0` _should_ indicate no tests were executed.
29    pub last: i32,
30    /// Arbitrary string which _should_ indicate why the certain tests were skipped.
31    pub reason: Option<&'a str>,
32}
33
34/// The body of the TAP document.
35#[derive(Debug, Serialize)]
36pub struct Body<'a> {
37    /// List of [`Statement`]s.
38    statements: Vec<Statement<'a>>,
39}
40
41/// A [`Pragma`] provides information to the document parser/interpreter.
42///
43/// # Note
44///
45/// Due to the PEG parsing approach, pragmas have no effect.
46#[derive(Debug, Serialize)]
47pub struct Pragma<'a> {
48    /// If present, declares if the given `option` should be enabled or disabled.
49    pub flag: Option<bool>,
50    /// Pragma option identifier.
51    pub option: &'a str,
52}
53
54/// Marks an emergency exit of the test procedure.
55#[derive(Debug, Serialize)]
56pub struct BailOut<'a> {
57    /// Optional reason for bailing out of the test procedure.
58    pub reason: Option<&'a str>,
59}
60
61/// Directive keys supported by [`Directive`].
62#[derive(Debug, Serialize)]
63pub enum Key {
64    /// Test was skipped
65    Skip,
66    /// Test has a to-do.
67    Todo,
68}
69
70/// A [`Directive`] gives some meta-data about the execution of a [`Test`].
71#[derive(Debug, Serialize)]
72pub struct Directive<'a> {
73    /// A directive key, declaring the nature of this [`Directive`].
74    pub key: Key,
75    /// A reason why this test was [`Key::Skip`]ped or why it is a [`Key::Todo`].
76    pub reason: Option<&'a str>,
77}
78
79/// A [`Test`] declaring the result of some test-case.
80#[derive(Debug, Serialize)]
81pub struct Test<'a> {
82    /// Result of the test.
83    pub result: bool,
84    /// Number of the test.
85    pub number: Option<i32>,
86    /// Description of the test.
87    pub description: Option<&'a str>,
88    /// Directive detailing this tests meta-execution.
89    pub directive: Option<Directive<'a>>,
90    /// List of YAML lines detailing the test execution.
91    pub yaml: Yaml<'a>,
92}
93
94/// [`Subtest`]s provide a way to nest one TAP14 stream inside another. This may be used in a variety of ways, depending on
95/// the test harness.
96#[derive(Debug, Serialize)]
97pub struct Subtest<'a> {
98    /// Name of the subtest, declared by a comment at the start of the [`Subtest`].
99    pub name: Option<&'a str>,
100    /// The [`Plan`] of the [`Subtest`].
101    pub plan: Plan<'a>,
102    /// Main [`Body`] of the [`Subtest`].
103    pub body: Vec<Statement<'a>>,
104}
105
106/// An enumeration of all possible TAP constructs that can be part of a [`Body`].
107#[derive(Debug, Serialize)]
108pub enum Statement<'a> {
109    /// Any text not captured by another [`Statement`] variant.
110    #[serde(rename = "anything")]
111    Anything(&'a str),
112    /// A [`BailOut`] statement.
113    #[serde(rename = "bail_out")]
114    BailOut(BailOut<'a>),
115    /// A [`Pragma`] statement.
116    #[serde(rename = "pragma")]
117    Pragma(Pragma<'a>),
118    /// A [`Subtest`] statement.
119    #[serde(rename = "subtest")]
120    Subtest(Subtest<'a>),
121    /// A [`Test`] statement.
122    #[serde(rename = "test")]
123    Test(Test<'a>),
124}
125
126/// A [`Document`] represents the root of any TAP document. It's the main point of interaction for users of this API.
127#[derive(Debug, Serialize)]
128pub struct Document<'a> {
129    /// The document's preamble.
130    pub preamble: Preamble<'a>,
131    /// The document's top-level plan declaration.
132    pub plan: Plan<'a>,
133    /// The document's top-level [`Body`] as a collection of [`Statement`]s. Some [`Statement`]s, like [`Subtest`] may
134    /// declare _nested_ [`Body`]s.
135    pub body: Vec<Statement<'a>>,
136}
137
138#[derive(Debug)]
139enum DocumentContent<'a> {
140    Plan(Plan<'a>),
141    Body(Vec<Statement<'a>>),
142}
143
144#[derive(Debug)]
145enum SubtestContent<'a> {
146    Plan(Plan<'a>),
147    Statement(Statement<'a>),
148}
149
150type Yaml<'a> = Vec<&'a str>;
151
152impl<'a> Preamble<'a> {
153    fn parse(mut pairs: Pairs<'a, Rule>) -> Self {
154        Self {
155            version: pairs.next().unwrap().as_str(),
156        }
157    }
158
159    /// Parse [`Preamble`] from a `&str`.
160    ///
161    /// # Examples
162    ///
163    /// Parsing a TAP version 14 preamble may look like this:
164    ///
165    /// ```
166    /// use tapconsooomer::Preamble;
167    ///
168    /// let content = "TAP version 14";
169    /// let preamble = Preamble::parse_from_str(content).expect("Parser error");
170    /// assert_eq!(preamble.version, "14");
171    /// ```
172    ///
173    /// Semantic versioning is supported aswell:
174    ///
175    /// ```
176    /// use tapconsooomer::Preamble;
177    ///
178    /// let content = "TAP version 13.1";
179    /// let preamble = Preamble::parse_from_str(content).expect("Parser error");
180    /// assert_eq!(preamble.version, "13.1");
181    /// ```
182    pub fn parse_from_str(content: &'a str) -> Result<Self> {
183        TAPParser::parse(Rule::preamble, content)?
184            .next()
185            .map(Pair::into_inner)
186            .map(Self::parse)
187            .ok_or_else(|| anyhow!("Can't parse '{}'", content))
188    }
189}
190
191impl<'a> Plan<'a> {
192    fn parse(mut pairs: Pairs<'a, Rule>) -> Result<Self> {
193        Ok(Self {
194            first: pairs.next().unwrap().as_str().parse()?,
195            last: pairs.next().unwrap().as_str().parse()?,
196            reason: pairs.next().map(|r| r.as_str()),
197        })
198    }
199
200    /// Parse [`Plan`] from a `&str`.
201    ///
202    /// # Examples
203    ///
204    /// Parsing a TAP plan may look like this:
205    ///
206    /// ```
207    /// use tapconsooomer::Plan;
208    ///
209    /// let content = "1..5 # TODO: Test 3 may fail";
210    /// let plan = Plan::parse_from_str(content).expect("Parser error");
211    /// assert_eq!(plan.first, 1);
212    /// assert_eq!(plan.last, 5);
213    /// assert_eq!(plan.reason, Some("TODO: Test 3 may fail"));
214    /// ```
215    ///
216    /// Note, [`Plan::reason`] is optional:
217    ///
218    /// ```
219    /// use tapconsooomer::Plan;
220    ///
221    /// let content = "1..5";
222    /// let plan = Plan::parse_from_str(content).expect("Parser error");
223    /// assert_eq!(plan.first, 1);
224    /// assert_eq!(plan.last, 5);
225    /// assert_eq!(plan.reason, None);
226    /// ```
227    pub fn parse_from_str(content: &'a str) -> Result<Self> {
228        TAPParser::parse(Rule::plan, content)?
229            .next()
230            .map(Pair::into_inner)
231            .map(Self::parse)
232            .ok_or_else(|| anyhow!("Can't parse '{}'", content))?
233    }
234}
235
236impl<'a> Directive<'a> {
237    fn parse(mut pairs: Pairs<'a, Rule>) -> Result<Self> {
238        let key = pairs.next().unwrap().as_str().to_lowercase();
239        Ok(Self {
240            key: match key.as_str() {
241                "skip" => Ok(Key::Skip),
242                "todo" => Ok(Key::Todo),
243                _ => Err(anyhow!("Directive key '{}' must be 'skip' or 'todo'", key)),
244            }?,
245            reason: pairs.next().map(|p| p.as_str()),
246        })
247    }
248
249    /// Parse [`Directive`] from a `&str`.
250    ///
251    /// # Examples
252    ///
253    /// Parsing a TAP directive may look like this:
254    ///
255    /// ```
256    /// use tapconsooomer::Directive;
257    /// use tapconsooomer::Key;
258    ///
259    /// let content = "# SKIP hardware requirements not met";
260    /// let directive = Directive::parse_from_str(content).expect("Parser error");
261    /// assert!(matches!(directive.key, Key::Skip));
262    /// assert_eq!(directive.reason, Some("hardware requirements not met"));
263    /// ```
264    ///
265    /// Note, [`Directive::reason`] is optional:
266    ///
267    /// ```
268    /// use tapconsooomer::Directive;
269    /// use tapconsooomer::Key;
270    ///
271    /// let content = "# TODO";
272    /// let directive = Directive::parse_from_str(content).expect("Parser error");
273    /// assert!(matches!(directive.key, Key::Todo));
274    /// assert_eq!(directive.reason, None);
275    /// ```
276    pub fn parse_from_str(content: &'a str) -> Result<Self> {
277        TAPParser::parse(Rule::directive, content)?
278            .next()
279            .map(Pair::into_inner)
280            .map(Self::parse)
281            .ok_or_else(|| anyhow!("Can't parse '{}'", content))?
282    }
283}
284
285impl<'a> Test<'a> {
286    fn parse(mut pairs: Pairs<'a, Rule>) -> Result<Self> {
287        let pair = pairs.next().unwrap();
288        let result = match pair.as_str().to_lowercase().as_str() {
289            "ok" => Ok(true),
290            "not ok" => Ok(false),
291            _ => Err(anyhow!(
292                "Result '{}' must be 'ok' or 'not ok'",
293                pair.as_str()
294            )),
295        }?;
296        let mut number: Option<i32> = None;
297        let mut description = None;
298        let mut directive = None;
299        let mut yaml = Vec::new();
300        for pair in pairs {
301            match pair.as_rule() {
302                Rule::number => number = pair.as_str().parse::<i32>().ok(),
303                Rule::description => description = Some(pair.as_str()),
304                Rule::directive => directive = Directive::parse(pair.into_inner()).ok(),
305                Rule::yaml_block => {
306                    yaml.append(&mut { pair.into_inner().map(|p| p.as_str()).collect() })
307                }
308                _ => unreachable!(),
309            };
310        }
311        Ok(Self {
312            result,
313            number,
314            description,
315            directive,
316            yaml,
317        })
318    }
319
320    /// Parse [`Test`] from a `&str`.
321    ///
322    /// # Examples
323    ///
324    /// Parsing a TAP test may look like this:
325    ///
326    /// ```
327    /// use tapconsooomer::Test;
328    ///
329    /// let content = "not ok 1 - foo()";
330    /// let test = Test::parse_from_str(content).expect("Parser error");
331    /// assert_eq!(test.result, false);
332    /// assert_eq!(test.number, Some(1));
333    /// assert_eq!(test.description, Some("foo()"));
334    /// assert_eq!(test.directive.is_none(), true);
335    /// assert_eq!(test.yaml.len(), 0);
336    /// ```
337    ///
338    /// Note, many attributes of a TAP test are optional. A minimal TAP test may look like this:
339    ///
340    /// ```
341    /// use tapconsooomer::Test;
342    ///
343    /// let content = "ok";
344    /// let test = Test::parse_from_str(content).expect("Parser error");
345    /// assert_eq!(test.result, true);
346    /// assert_eq!(test.number, None);
347    /// assert_eq!(test.description, None);
348    /// assert_eq!(test.directive.is_none(), true);
349    /// assert_eq!(test.yaml.len(), 0);
350    /// ```
351    ///
352    /// TAP tests may also optionally contain a YAML block. While no parsing of actual YAML syntax is performed, the
353    /// parser captures each line inside the YAML block in a [`Vec`]:
354    ///
355    /// ```
356    /// use tapconsooomer::Test;
357    ///
358    /// let content = concat!(
359    ///     "not ok 2 - bar()\n",
360    ///     "  ---\n",
361    ///     "  message: invalid input\n",
362    ///     "  status: failed\n",
363    ///     "  ...\n",
364    /// );
365    /// let test = Test::parse_from_str(content).expect("Parser error");
366    /// assert_eq!(test.result, false);
367    /// assert_eq!(test.number, Some(2));
368    /// assert_eq!(test.description, Some("bar()"));
369    /// assert_eq!(test.directive.is_none(), true);
370    /// assert_eq!(test.yaml.len(), 2);
371    /// assert_eq!(test.yaml, ["message: invalid input", "status: failed"])
372    /// ```
373    pub fn parse_from_str(content: &'a str) -> Result<Self> {
374        TAPParser::parse(Rule::test, content)?
375            .next()
376            .map(Pair::into_inner)
377            .map(Self::parse)
378            .ok_or_else(|| anyhow!("Can't parse '{}'", content))?
379    }
380}
381
382impl<'a> BailOut<'a> {
383    fn parse(mut pairs: Pairs<'a, Rule>) -> Result<Self> {
384        Ok(Self {
385            reason: pairs.next().map(|p| p.as_str()),
386        })
387    }
388
389    /// Parse [`BailOut`] from a `&str`.
390    ///
391    /// # Examples
392    ///
393    /// Parsing a TAP bail-out may look like this:
394    ///
395    /// ```
396    /// use tapconsooomer::BailOut;
397    ///
398    /// let content = "Bail out! Hardware overheating";
399    /// let bail_out = BailOut::parse_from_str(content).expect("Parser error");
400    /// assert_eq!(bail_out.reason, Some("Hardware overheating"));
401    /// ```
402    ///
403    /// Note, [`BailOut::reason`] is optional:
404    ///
405    /// ```
406    /// use tapconsooomer::BailOut;
407    ///
408    /// let content = "Bail out!";
409    /// let bail_out = BailOut::parse_from_str(content).expect("Parser error");
410    /// assert_eq!(bail_out.reason.is_none(), true);
411    /// ```
412    pub fn parse_from_str(content: &'a str) -> Result<Self> {
413        TAPParser::parse(Rule::bail_out, content)?
414            .next()
415            .map(Pair::into_inner)
416            .map(Self::parse)
417            .ok_or_else(|| anyhow!("Can't parse '{}'", content))?
418    }
419}
420
421impl<'a> Pragma<'a> {
422    pub fn parse(mut pairs: Pairs<'a, Rule>) -> Result<Self> {
423        let mut pair = pairs.next().unwrap();
424        let flag = match pair.as_rule() {
425            Rule::flag => {
426                println!("{}", pair.as_str());
427                let result = match pair.as_str() {
428                    "+" => Some(true),
429                    "-" => Some(false),
430                    _ => unreachable!(),
431                };
432                pair = pairs.next().unwrap();
433                result
434            }
435            _ => None,
436        };
437        Ok(Self {
438            flag,
439            option: pair.as_str(),
440        })
441    }
442
443    /// Parse [`Pragma`] from a `&str`.
444    ///
445    /// # Examples
446    ///
447    /// Parsing a TAP pragma may look like this:
448    ///
449    /// ```
450    /// use tapconsooomer::Pragma;
451    ///
452    /// let content = "pragma +strict";
453    /// let pragma = Pragma::parse_from_str(content).expect("Parser error");
454    /// assert_eq!(pragma.flag, Some(true));
455    /// assert_eq!(pragma.option, "strict");
456    /// ```
457    ///
458    /// Note, [`Pragma::flag`] is optional:
459    ///
460    /// ```
461    /// use tapconsooomer::Pragma;
462    ///
463    /// let content = "pragma foo";
464    /// let pragma = Pragma::parse_from_str(content).expect("Parser error");
465    /// assert_eq!(pragma.flag.is_none(), true);
466    /// assert_eq!(pragma.option, "foo");
467    /// ```
468    pub fn parse_from_str(content: &'a str) -> Result<Self> {
469        TAPParser::parse(Rule::pragma, content)?
470            .next()
471            .map(Pair::into_inner)
472            .map(Self::parse)
473            .ok_or_else(|| anyhow!("Can't parse '{}'", content))?
474    }
475}
476
477impl<'a> Subtest<'a> {
478    pub fn parse(mut pairs: Pairs<'a, Rule>) -> Result<Self> {
479        let pair = pairs.next().unwrap();
480        let name = match pair.as_rule() {
481            Rule::name => Some(pair.as_str()),
482            _ => None,
483        };
484
485        let mut contents = vec![]; /* FIXME: There has to be a better way to split plan and body! */
486
487        /* Consume first pair if not consumed by 'name'. */
488        if name.is_none() {
489            contents.push(match pair.as_rule() {
490                Rule::plan => Plan::parse(pair.into_inner()).map(SubtestContent::Plan),
491                _ => Statement::parse(pair).map(SubtestContent::Statement),
492            }?)
493        }
494
495        /* Now consume the rest of the pairs. */
496        let mut rest: Vec<SubtestContent> = pairs
497            .map(|p| match p.as_rule() {
498                Rule::plan => Plan::parse(p.into_inner()).map(SubtestContent::Plan),
499                _ => Statement::parse(p).map(SubtestContent::Statement),
500            })
501            .collect::<Result<Vec<SubtestContent>>>()?;
502        contents.append(&mut rest);
503
504        let mut p = None;
505        let mut i = 0;
506        while i < contents.len() {
507            /* TODO: Use drain_filter once stable. */
508            if matches!(contents[i], SubtestContent::Plan(_)) {
509                p = Some(match contents.remove(i) {
510                    SubtestContent::Plan(p) => p,
511                    SubtestContent::Statement(_) => unreachable!(),
512                })
513            }
514            i += 1;
515        }
516        let plan = p.unwrap();
517        let body = contents
518            .into_iter()
519            .filter_map(|s| match s {
520                SubtestContent::Plan(_) => None,
521                SubtestContent::Statement(s) => Some(s),
522            })
523            .collect();
524
525        Ok(Self { name, plan, body })
526    }
527
528    /// Parse [`Subtest`] from a `&str`.
529    ///
530    /// # Examples
531    ///
532    /// Parsing a TAP subtest may look like this:
533    ///
534    /// ```
535    /// use tapconsooomer::Subtest;
536    ///
537    /// let content = concat!(
538    ///     "# Subtest: foo\n",
539    ///     "  1..2\n",
540    ///     "  ok 1 - bar\n",
541    ///     "  ok 2 - tuna\n",
542    /// );
543    /// let subtest = Subtest::parse_from_str(content).expect("Parser error");
544    /// assert_eq!(subtest.name, Some("foo"));
545    /// assert_eq!(subtest.plan.first, 1);
546    /// assert_eq!(subtest.plan.last, 2);
547    /// assert_eq!(subtest.body.len(), 2);
548    /// ```
549    ///
550    /// Note, the comment declaring [`Subtest::name`] is optional:
551    ///
552    /// ```
553    /// use tapconsooomer::Subtest;
554    ///
555    /// let content = concat!(
556    ///     "  1..4\n",
557    ///     "  ok 1 - foo\n",
558    ///     "  ok 2 - bar\n",
559    ///     "  ok 3 - foobar\n",
560    ///     "  ok 4 - catfood\n",
561    /// );
562    /// let subtest = Subtest::parse_from_str(content).expect("Parser error");
563    /// assert_eq!(subtest.name.is_none(), true);
564    /// assert_eq!(subtest.plan.first, 1);
565    /// assert_eq!(subtest.plan.last, 4);
566    /// assert_eq!(subtest.body.len(), 4);
567    /// ```
568    ///
569    /// So is the order in which [`Plan`] and [`Body`] are declared:
570    ///
571    /// ```
572    /// use tapconsooomer::Subtest;
573    ///
574    /// let content = concat!(
575    ///     "  ok 1 - hello world\n",
576    ///     "  1..1\n",
577    /// );
578    /// let subtest = Subtest::parse_from_str(content).expect("Parser error");
579    /// assert_eq!(subtest.name.is_none(), true);
580    /// assert_eq!(subtest.plan.first, 1);
581    /// assert_eq!(subtest.plan.last, 1);
582    /// assert_eq!(subtest.body.len(), 1);
583    /// ```
584    pub fn parse_from_str(content: &'a str) -> Result<Self> {
585        TAPParser::parse(Rule::subtest, content)?
586            .next()
587            .map(Pair::into_inner)
588            .map(Self::parse)
589            .ok_or_else(|| anyhow!("Can't parse '{}'", content))?
590    }
591}
592
593impl<'a> Statement<'a> {
594    pub fn parse(pair: Pair<'a, Rule>) -> Result<Self> {
595        match pair.as_rule() {
596            Rule::test => Ok(Self::Test(Test::parse(pair.into_inner())?)),
597            Rule::bail_out => Ok(Self::BailOut(BailOut::parse(pair.into_inner())?)),
598            Rule::pragma => Ok(Self::Pragma(Pragma::parse(pair.into_inner())?)),
599            Rule::subtest => Ok(Self::Subtest(Subtest::parse(pair.into_inner())?)),
600            Rule::anything => Ok(Self::Anything(pair.as_str())),
601            _ => unreachable!(),
602        }
603    }
604
605    /// Parse [`Statement`] from a `&str`.
606    ///
607    /// # Examples
608    ///
609    /// Parsing a TAP statement may look like this:
610    ///
611    /// ```
612    /// use tapconsooomer::Statement;
613    ///
614    /// let test = "ok 1 - foo::bar()";
615    /// let stmt = Statement::parse_from_str(test).expect("Parser error");
616    /// assert!(matches!(stmt, Statement::Test(..)));
617    ///
618    /// let anything = "hello world";
619    /// let stmt = Statement::parse_from_str(anything).expect("Parser error");
620    /// assert!(matches!(stmt, Statement::Anything(..)));
621    ///
622    /// let bail_out = "Bail Out!";
623    /// let stmt = Statement::parse_from_str(bail_out).expect("Parser error");
624    /// assert!(matches!(stmt, Statement::BailOut(..)));
625    ///
626    /// let pragma = "pragma -strict";
627    /// let stmt = Statement::parse_from_str(pragma).expect("Parser error");
628    /// assert!(matches!(stmt, Statement::Pragma(..)));
629    ///
630    /// let subtest = concat!(
631    ///     "# Subtest: foo\n",
632    ///     "    1..0 # skipped\n",
633    /// );
634    /// let stmt = Statement::parse_from_str(subtest).expect("Parser error");
635    /// assert!(matches!(stmt, Statement::Subtest(..)));
636    /// ```
637    pub fn parse_from_str(content: &'a str) -> Result<Self> {
638        TAPParser::parse(Rule::statement, content)?
639            .next()
640            .map(Self::parse)
641            .ok_or_else(|| anyhow!("Can't parse '{}'", content))?
642    }
643}
644
645impl<'a> DocumentContent<'a> {
646    fn parse(pair: Pair<'a, Rule>) -> Result<Self> {
647        Ok(match pair.as_rule() {
648            Rule::plan => Self::Plan(Plan::parse(pair.into_inner())?),
649            Rule::body => {
650                let statements: Result<Vec<_>> = pair.into_inner().map(Statement::parse).collect();
651                Self::Body(statements?)
652            }
653            _ => unreachable!(),
654        })
655    }
656}
657
658impl<'a> Document<'a> {
659    pub fn parse(mut pairs: Pairs<'a, Rule>) -> Result<Self> {
660        let preamble = Preamble::parse(pairs.next().unwrap().into_inner());
661
662        let content1 = DocumentContent::parse(pairs.next().unwrap())?;
663        let content2 = DocumentContent::parse(pairs.next().unwrap())?;
664        let (plan, body) = match (content1, content2) {
665            (DocumentContent::Plan(p), DocumentContent::Body(b)) => (p, b),
666            (DocumentContent::Body(b), DocumentContent::Plan(p)) => (p, b),
667            _ => unreachable!(),
668        };
669
670        Ok(Self {
671            preamble,
672            plan,
673            body,
674        })
675    }
676
677    /// Parse [`Document`] from a `&str`.
678    ///
679    /// # Examples
680    ///
681    /// Parsing a TAP document may look like this:
682    ///
683    /// ```
684    /// use tapconsooomer::Document;
685    ///
686    /// let content = concat!(
687    ///     "TAP version 14\n",
688    ///     "1..1\n",
689    ///     "ok 1 - foo()\n",
690    /// );
691    /// let doc = Document::parse_from_str(content).expect("Parser error");
692    /// assert_eq!(doc.preamble.version, "14");
693    /// assert_eq!(doc.body.len(), 1);
694    /// ```
695    ///
696    /// The order in which [`Body`] and [`Plan`] are declared is unimportant:
697    ///
698    /// ```
699    /// use tapconsooomer::Document;
700    ///
701    /// let content = concat!(
702    ///     "TAP version 14\n",
703    ///     "ok 1 - foo()\n",
704    ///     "ok 2 - bar()\n",
705    ///     "1..2\n",
706    /// );
707    /// let doc = Document::parse_from_str(content).expect("Parser error");
708    /// assert_eq!(doc.preamble.version, "14");
709    /// assert_eq!(doc.body.len(), 2);
710    /// ```
711    pub fn parse_from_str(content: &'a str) -> Result<Self> {
712        TAPParser::parse(Rule::document, content)?
713            .next()
714            .map(Pair::into_inner)
715            .map(Self::parse)
716            .ok_or_else(|| anyhow!("Can't parse '{}'", content))?
717    }
718}
719
720#[cfg(test)]
721mod tests {
722    use pest::consumes_to;
723    use pest::parses_to;
724    use std::fs;
725
726    use super::*;
727
728    #[test]
729    fn test_version() {
730        parses_to! {
731            parser: TAPParser,
732            input : "TAP version 14",
733            rule: Rule::preamble,
734            tokens: [
735                preamble(0, 14, [
736                    version(12, 14)
737                ])
738            ]
739        }
740    }
741
742    #[test]
743    fn test_version_mixed_case() {
744        parses_to! {
745            parser: TAPParser,
746            input : "tAp VeRsIoN 123",
747            rule: Rule::preamble,
748            tokens: [
749                preamble(0, 15, [
750                    version(12, 15)
751                ])
752            ]
753        }
754    }
755
756    #[test]
757    fn test_plan() {
758        parses_to! {
759            parser: TAPParser,
760            input : "1..2",
761            rule: Rule::plan,
762            tokens: [
763                plan(0, 4, [
764                    first(0, 1), last(3, 4)
765                ])
766            ]
767        }
768    }
769
770    #[test]
771    fn test_plan_with_reason() {
772        parses_to! {
773            parser: TAPParser,
774            input : "1..20 # generated",
775            rule: Rule::plan,
776            tokens: [
777                plan(0, 17, [
778                    first(0, 1), last(3, 5), reason(8, 17)
779                ])
780            ]
781        }
782    }
783
784    #[test]
785    fn test_ok_plain() {
786        parses_to! {
787            parser: TAPParser,
788            input : "ok",
789            rule: Rule::test,
790            tokens: [
791                test(0, 2, [
792                    result(0, 2)
793                ])
794            ]
795        }
796    }
797
798    #[test]
799    fn test_not_ok_plain() {
800        parses_to! {
801            parser: TAPParser,
802            input : "not ok",
803            rule: Rule::test,
804            tokens: [
805                test(0, 6, [
806                    result(0, 6)
807                ])
808            ]
809        }
810    }
811
812    #[test]
813    fn test_not_ok_plain_mixed_case() {
814        parses_to! {
815            parser: TAPParser,
816            input : "nOt Ok",
817            rule: Rule::test,
818            tokens: [
819                test(0, 6, [
820                    result(0, 6)
821                ])
822            ]
823        }
824    }
825
826    #[test]
827    fn test_ok_with_number() {
828        parses_to! {
829            parser: TAPParser,
830            input : "ok 123",
831            rule: Rule::test,
832            tokens: [
833                test(0, 6, [
834                    result(0, 2), number(3, 6)
835                ])
836            ]
837        }
838    }
839
840    #[test]
841    fn test_ok_with_description() {
842        parses_to! {
843            parser: TAPParser,
844            input : "ok - hello world",
845            rule: Rule::test,
846            tokens: [
847                test(0, 16, [
848                    result(0, 2), description(5, 16)
849                ])
850            ]
851        }
852    }
853
854    #[test]
855    fn test_not_ok_with_description() {
856        parses_to! {
857            parser: TAPParser,
858            input : "not ok - hello world",
859            rule: Rule::test,
860            tokens: [
861                test(0, 20, [
862                    result(0, 6), description(9, 20)
863                ])
864            ]
865        }
866    }
867
868    #[test]
869    fn test_ok_with_description_no_dash() {
870        parses_to! {
871            parser: TAPParser,
872            input : "ok hello world",
873            rule: Rule::test,
874            tokens: [
875                test(0, 14, [
876                    result(0, 2), description(3, 14)
877                ])
878            ]
879        }
880    }
881
882    #[test]
883    fn test_ok_with_directive_skip() {
884        parses_to! {
885            parser: TAPParser,
886            input : "ok # skip",
887            rule: Rule::test,
888            tokens: [
889                test(0, 9, [
890                    result(0, 2), directive(3, 9, [
891                        key(5, 9)
892                    ])
893                ])
894            ]
895        }
896    }
897
898    #[test]
899    fn test_not_ok_with_directive_todo_reason() {
900        parses_to! {
901            parser: TAPParser,
902            input : "not ok # todo this is a reason",
903            rule: Rule::test,
904            tokens: [
905                test(0, 30, [
906                    result(0, 6), directive(7, 30, [
907                        key(9, 13), reason(14, 30)
908                    ])
909                ])
910            ]
911        }
912    }
913
914    #[test]
915    fn test_not_ok_with_number_directive_skip_reason() {
916        parses_to! {
917            parser: TAPParser,
918            input : "not ok 42 # todo this is a reason",
919            rule: Rule::test,
920            tokens: [
921                test(0, 33, [
922                    result(0, 6), number(7, 9), directive(10, 33, [
923                        key(12, 16), reason(17, 33)
924                    ])
925                ])
926            ]
927        }
928    }
929
930    #[test]
931    fn test_ok_with_number_description_directive_skip_reason() {
932        parses_to! {
933            parser: TAPParser,
934            input : "ok 1 - hello world # skip this is a reason",
935            rule: Rule::test,
936            tokens: [
937                test(0, 42, [
938                    result(0, 2), number(3, 4), description(7, 19), directive(19, 42, [
939                        key(21, 25),
940                        reason(26, 42)
941                    ])
942                ])
943            ]
944        }
945    }
946
947    #[test]
948    fn test_bail_out() {
949        parses_to! {
950            parser: TAPParser,
951            input : "bail out!",
952            rule: Rule::bail_out,
953            tokens: [
954                bail_out(0, 9)
955            ]
956        }
957    }
958
959    #[test]
960    fn test_bail_out_mixed_case() {
961        parses_to! {
962            parser: TAPParser,
963            input : "BaIl OuT!",
964            rule: Rule::bail_out,
965            tokens: [
966                bail_out(0, 9)
967            ]
968        }
969    }
970
971    #[test]
972    fn test_bail_out_with_reason() {
973        parses_to! {
974            parser: TAPParser,
975            input : "bail out! something went terribly wrong",
976            rule: Rule::bail_out,
977            tokens: [
978                bail_out(0, 39, [
979                    reason(10, 39)
980                ])
981            ]
982        }
983    }
984
985    #[test]
986    fn test_pragma() {
987        parses_to! {
988            parser: TAPParser,
989            input : "pragma -strict",
990            rule: Rule::pragma,
991            tokens: [
992                pragma(0, 14, [
993                    flag(7, 8), option(8, 14)
994                ])
995            ]
996        }
997    }
998
999    #[test]
1000    fn test_pragma_mixed_case() {
1001        parses_to! {
1002            parser: TAPParser,
1003            input : "pRaGmA +strict",
1004            rule: Rule::pragma,
1005            tokens: [
1006                pragma(0, 14, [
1007                    flag(7, 8), option(8, 14)
1008                ])
1009            ]
1010        }
1011    }
1012
1013    #[test]
1014    fn test_pragma_no_flag() {
1015        parses_to! {
1016            parser: TAPParser,
1017            input : "pragma allow_anything",
1018            rule: Rule::pragma,
1019            tokens: [
1020                pragma(0, 21, [
1021                    option(7, 21)
1022                ])
1023            ]
1024        }
1025    }
1026
1027    #[test]
1028    fn test_subtest_with_empty_declaration() {
1029        parses_to! {
1030            parser: TAPParser,
1031            input : r#"# subtest
1032        1..20"#,
1033            rule: Rule::subtest,
1034            tokens: [
1035                subtest(0, 23, [
1036                    plan(18, 23, [
1037                        first(18, 19), last(21, 23)
1038                    ])
1039                ])
1040            ]
1041        }
1042    }
1043
1044    #[test]
1045    fn test_subtest_with_declaration() {
1046        parses_to! {
1047            parser: TAPParser,
1048            input : concat!(
1049                "# Subtest: This is a subtest\n",
1050                "    1..20"
1051            ),
1052            rule: Rule::subtest,
1053            tokens: [
1054                subtest(0, 38, [
1055                    name(11, 28),
1056                    plan(33, 38, [
1057                        first(33, 34), last(36, 38)
1058                    ])
1059                ])
1060            ]
1061        }
1062    }
1063
1064    #[test]
1065    fn test_subtest_without_declaration() {
1066        parses_to! {
1067            parser: TAPParser,
1068            input : "    1..20",
1069            rule: Rule::subtest,
1070            tokens: [
1071                subtest(0, 9, [
1072                    plan(4, 9, [
1073                        first(4, 5), last(7, 9)
1074                    ])
1075                ])
1076            ]
1077        }
1078    }
1079
1080    #[test]
1081    fn test_subtest_with_plan_body() {
1082        parses_to! {
1083            parser: TAPParser,
1084            input : concat!(
1085                "    1..21\n",
1086                "    ok 1 - hello world\n",
1087            ),
1088            rule: Rule::subtest,
1089            tokens: [
1090                subtest(0, 33, [
1091                    plan(4, 9, [
1092                        first(4, 5), last(7, 9)
1093                    ]),
1094                    test(14, 32, [
1095                        result(14, 16), number(17, 18), description(21, 32)
1096                    ])
1097                ])
1098            ]
1099        }
1100    }
1101
1102    #[test]
1103    fn test_subtest_with_body_plan() {
1104        parses_to! {
1105            parser: TAPParser,
1106            input : concat!(
1107                "    ok 1 - hello world\n",
1108                "    1..21\n",
1109            ),
1110            rule: Rule::subtest,
1111            tokens: [
1112                subtest(0, 32, [
1113                    test(4, 22, [
1114                        result(4, 6), number(7, 8), description(11, 22)
1115                    ]),
1116                    plan(27, 32, [
1117                        first(27, 28), last(30, 32)
1118                    ])
1119                ])
1120            ]
1121        }
1122    }
1123
1124    #[test]
1125    fn test_comment() {
1126        parses_to! {
1127            parser: TAPParser,
1128            input : "# this is a comment",
1129            rule: Rule::COMMENT,
1130            tokens: [
1131            ]
1132        }
1133    }
1134
1135    #[test]
1136    fn test_yaml_block() {
1137        parses_to! {
1138            parser: TAPParser,
1139            input : concat!(
1140                "  ---\n",
1141                "  YAML line 1\n",
1142                "  YAML line 2\n",
1143                "  YAML line 3\n",
1144                "  ok 1 - this is considered YAML\n",
1145                "        \r\n",
1146                "  0..1 # even this is YAML\n",
1147                "  ...\n",
1148            ),
1149            rule: Rule::yaml_block,
1150            tokens: [
1151                yaml_block(0, 123, [
1152                    yaml(8, 19),
1153                    yaml(22, 33),
1154                    yaml(36, 47),
1155                    yaml(50, 80),
1156                    yaml(93, 117),
1157                ])
1158            ]
1159        }
1160    }
1161
1162    #[test]
1163    fn test_common() {
1164        let contents = fs::read_to_string("examples/common.tap").expect("Failed to read file");
1165
1166        parses_to! {
1167            parser: TAPParser,
1168            input : &contents,
1169            rule: Rule::document,
1170            tokens: [
1171                document(0, 318, [
1172                    preamble(0, 14, [
1173                        version(12, 14)
1174                    ]),
1175                    plan(15, 19, [
1176                        first(15, 16), last(18, 19)
1177                    ]),
1178                    body(19, 318, [
1179                        test(93, 131, [
1180                            result(93, 95),
1181                            number(96, 97),
1182                            description(100, 122),
1183                            directive(122, 131, [
1184                                key(123, 127), reason(128, 131)
1185                            ])
1186                        ]),
1187                        test(132, 171, [
1188                            result(132, 134),
1189                            number(135, 136),
1190                            description(139, 158),
1191                            directive(158, 171, [
1192                                key(160, 164), reason(165, 171)
1193                            ])
1194                        ]),
1195                        test(172, 209, [
1196                            result(172, 174), number(175, 176), description(179, 200)
1197                        ]),
1198                        test(210, 252, [
1199                            result(210, 212), number(213, 214), description(217, 252)
1200                        ]),
1201                        test(253, 294, [
1202                            result(253, 255), number(256, 257), description(260, 294)
1203                        ]),
1204                        test(295, 317, [
1205                            result(295, 297), number(298, 299), description(302, 317)
1206                        ]),
1207                    ])
1208                ])
1209            ]
1210        }
1211    }
1212
1213    #[test]
1214    fn test_cascading() {
1215        let contents = fs::read_to_string("examples/cascading.tap").expect("Failed to read file");
1216
1217        parses_to! {
1218            parser: TAPParser,
1219            input : &contents,
1220            rule: Rule::document,
1221            tokens: [
1222                document(0, 300, [
1223                    preamble(0, 14, [
1224                        version(12, 14)
1225                    ]),
1226                    plan(15, 26, [
1227                        first(15, 16), last(18, 19), reason(22, 26)
1228                    ]),
1229                    body(26, 300, [
1230                        test(27, 45, [
1231                            result(27, 29), number(30, 31), description(34, 45)
1232                        ]),
1233                        subtest(46, 295, [
1234                            name(57, 74),
1235                            plan(77, 89, [
1236                                first(77, 78), last(80, 81), reason(84, 89)
1237                            ]),
1238                            subtest(92, 273, [
1239                                plan(94, 106, [
1240                                    first(94, 95), last(97, 98), reason(101, 106)
1241                                ]),
1242                                test(111, 130, [
1243                                    result(111, 113), number(114, 115), description(118, 130)
1244                                ]),
1245                                anything(135, 236),
1246                                test(241, 272, [
1247                                    result(241, 247),
1248                                    number(248, 249),
1249                                    directive(250, 272, [
1250                                        key(252, 256),
1251                                        reason(257, 272)
1252                                    ])
1253                                ])
1254                            ]),
1255                            test(275, 294, [
1256                                result(275, 277), number(278, 279), description(282, 294)
1257                            ])
1258                        ]),
1259                        test(295, 299, [
1260                            result(295, 297), number(298, 299),
1261                        ]),
1262                    ])
1263                ])
1264            ]
1265        }
1266    }
1267
1268    #[test]
1269    fn test_yaml() {
1270        let contents = fs::read_to_string("examples/yaml.tap").expect("Failed to read file");
1271
1272        parses_to! {
1273            parser: TAPParser,
1274            input : &contents,
1275            rule: Rule::document,
1276            tokens: [
1277                document(0, 534, [
1278                    preamble(0, 14, [
1279                        version(12, 14)
1280                    ]),
1281                    plan(15, 31, [
1282                        first(15, 16), last(18, 19), reason(22, 31)
1283                    ]),
1284                    body(31, 534, [
1285                        test(32, 344, [
1286                            result(32, 38),
1287                            number(39, 40),
1288                            description(43, 58),
1289                            yaml_block(59, 344, [
1290                                yaml(67, 136),
1291                                yaml(139, 153),
1292                                yaml(156, 162),
1293                                yaml(165, 198),
1294                                yaml(201, 213),
1295                                yaml(217, 224),
1296                                yaml(227, 260),
1297                                yaml(263, 289),
1298                                yaml(292, 295),
1299                                yaml(298, 324),
1300                                yaml(327, 338),
1301                            ])
1302                        ]),
1303                        test(345, 366, [
1304                            result(345, 347), number(348, 349), description(352, 366)
1305                        ]),
1306                        subtest(367, 534, [
1307                            name(378, 402),
1308                            plan(405, 420, [
1309                                first(405, 406), last(408, 409), reason(412, 420)
1310                            ]),
1311                            test(423, 519, [
1312                                result(423, 425),
1313                                number(426, 427),
1314                                description(430, 444),
1315                                yaml_block(445, 519, [
1316                                    yaml(461, 509)
1317                                ]),
1318                            ]),
1319                            test(522, 533, [
1320                                result(522, 524), number(525, 526), description(529, 533)
1321                            ])
1322                        ])
1323                    ])
1324                ])
1325            ]
1326        }
1327    }
1328}