pest_test/
lib.rs

1pub mod diff;
2pub mod model;
3mod parser;
4
5use crate::{
6    diff::ExpressionDiff,
7    model::{Expression, ModelError, TestCase},
8    parser::{ParserError, Rule, TestParser},
9};
10use pest::{error::Error as PestError, Parser, RuleType};
11use std::{
12    collections::HashSet, fs::read_to_string, io::Error as IOError, marker::PhantomData,
13    path::PathBuf,
14};
15use thiserror::Error;
16
17pub fn cargo_manifest_dir() -> PathBuf {
18    PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap().as_str())
19}
20
21pub fn default_test_dir() -> PathBuf {
22    cargo_manifest_dir().join("tests").join("pest")
23}
24
25#[derive(Error, Debug)]
26pub enum TestError<R> {
27    #[error("Error reading test case from file")]
28    IO { source: IOError },
29    #[error("Error parsing test case")]
30    Parser { source: ParserError<Rule> },
31    #[error("Error building model from test case parse tree")]
32    Model { source: ModelError },
33    #[error("Error parsing code with target parser")]
34    Target { source: Box<PestError<R>> },
35    #[error("Expected and actual parse trees are different:\n{diff}")]
36    Diff { diff: ExpressionDiff },
37}
38
39pub struct PestTester<R: RuleType, P: Parser<R>> {
40    test_dir: PathBuf,
41    test_ext: String,
42    rule: R,
43    skip_rules: HashSet<R>,
44    parser: PhantomData<P>,
45}
46
47impl<R: RuleType, P: Parser<R>> PestTester<R, P> {
48    /// Creates a new `PestTester` that looks for tests in `test_dir` and having file extension
49    /// `test_ext`. Code is parsed beinning at `rule` and the rules in `skip_rule` are ignored
50    /// when comparing to the expected expression.
51    pub fn new<D: Into<PathBuf>, S: AsRef<str>>(
52        test_dir: D,
53        test_ext: S,
54        rule: R,
55        skip_rules: HashSet<R>,
56    ) -> Self {
57        Self {
58            test_dir: test_dir.into(),
59            test_ext: test_ext.as_ref().to_owned(),
60            rule,
61            skip_rules,
62            parser: PhantomData::<P>,
63        }
64    }
65
66    /// Creates a new `PestTester` that looks for tests in `<crate root>/tests/pest` and having
67    /// file extension ".txt". Code is parsed beinning at `rule` and the rules in `skip_rule` are
68    /// ignored when comparing to the expected expression.
69    pub fn from_defaults(rule: R, skip_rules: HashSet<R>) -> Self {
70        Self::new(default_test_dir(), ".txt", rule, skip_rules)
71    }
72
73    /// Evaluates the test with the given name. If `ignore_missing_expected_values` is true, then
74    /// the test is not required to specify values for non-terminal nodes.
75    pub fn evaluate<N: AsRef<str>>(
76        &self,
77        name: N,
78        ignore_missing_expected_values: bool,
79    ) -> Result<(), TestError<R>> {
80        let path = self
81            .test_dir
82            .join(format!("{}.{}", name.as_ref(), self.test_ext));
83        let text = read_to_string(path).map_err(|source| TestError::IO { source })?;
84        let pair =
85            TestParser::parse(text.as_ref()).map_err(|source| TestError::Parser { source })?;
86        let test_case =
87            TestCase::try_from_pair(pair).map_err(|source| TestError::Model { source })?;
88        let code_pair =
89            parser::parse(test_case.code.as_ref(), self.rule, self.parser).map_err(|source| {
90                match source {
91                    ParserError::Empty => TestError::Parser {
92                        source: ParserError::Empty,
93                    },
94                    ParserError::Pest { source } => TestError::Target { source },
95                }
96            })?;
97        let code_expr = Expression::try_from_code(code_pair, &self.skip_rules)
98            .map_err(|source| TestError::Model { source })?;
99        match ExpressionDiff::from_expressions(
100            &test_case.expression,
101            &code_expr,
102            ignore_missing_expected_values,
103        ) {
104            ExpressionDiff::Equal(_) => Ok(()),
105            diff => Err(TestError::Diff { diff }),
106        }
107    }
108
109    /// Equivalent to `self.evaluate(name, true)
110    pub fn evaluate_strict<N: AsRef<str>>(&self, name: N) -> Result<(), TestError<R>> {
111        self.evaluate(name, false)
112    }
113}