scrut/
expectation.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8use std::fmt::Display;
9
10use anyhow::Result;
11use serde::Serialize;
12
13use crate::escaping::Escaper;
14use crate::newline::StringNewline;
15use crate::rules::registry::RuleRegistry;
16use crate::rules::rule::Rule;
17
18/// An expectation about the content and / or form of one or multiple subsequent
19/// line(s) of output, that may be optional.
20#[derive(Debug, Clone)]
21pub struct Expectation {
22    /// Optional Expectations are lines that may or may not occur in the output
23    pub optional: bool,
24
25    /// Multiline Expectations (can) match multiple sequential lines of output
26    pub multiline: bool,
27
28    /// The actual algorithm that implements the Expectation
29    pub rule: Box<dyn Rule>,
30
31    /// The original expression as it was written in the test file
32    original: String,
33}
34
35impl Expectation {
36    /// Decompose the Expectation into the components from which it can be made (`Expectation::make`)
37    pub fn unmake(&self) -> (String, Vec<u8>, bool, bool) {
38        let (kind, expression) = self.rule.unmake();
39        (kind, expression, self.optional, self.multiline)
40    }
41
42    /// Whether the provided line matches this Expectation
43    pub fn matches(&self, line: &[u8]) -> bool {
44        self.rule.matches(line)
45    }
46
47    /// Renders the Expectation into an expression from which it can be parsed
48    pub fn to_expression_string(&self, escaper: &Escaper) -> String {
49        self.rule
50            .to_expression_string(self.optional, self.multiline, escaper)
51    }
52
53    /// The original string as it was written in the test file
54    pub fn original_string(&self) -> String {
55        self.original.clone()
56    }
57}
58
59impl Display for Expectation {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(f, "{}", self.to_expression_string(&Escaper::default()))
62    }
63}
64
65impl PartialEq for Expectation {
66    fn eq(&self, other: &Self) -> bool {
67        self.optional == other.optional
68            && self.multiline == other.multiline
69            && self.rule.to_string() == other.rule.to_string()
70    }
71}
72
73impl Serialize for Expectation {
74    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
75    where
76        S: serde::Serializer,
77    {
78        serializer.serialize_str(&self.original)
79    }
80}
81
82/// Facade for [`Expectation`] creation from either line encoded representation
83/// or from components
84pub struct ExpectationMaker(RuleRegistry);
85
86impl ExpectationMaker {
87    pub fn new(registry: RuleRegistry) -> Self {
88        Self(registry)
89    }
90
91    // TODO: T131320483 following sentence doesnt make sense
92    /// Create an [`Expectation`] that from it's text encoding, with the BNF form:
93    ///
94    /// ```bnf
95    ///  <expectation> ::= <expression> | <expression> (<kind>) | <expression> (<quantifier>) | <expression> (<kind><quantifier>)
96    ///   <expression> ::= "arbitrary text"
97    ///         <kind> ::= <equal-kind> | <no-eol-kind> | <escaped-kind> | <glob-kind> | <regex-kind>
98    ///   <equal-kind> ::= "equal" | "eq"
99    ///  <no-eol-kind> ::= "no-eol"
100    /// <escaped-kind> ::= "escaped" | "esc"
101    ///    <glob-kind> ::= "glob" | "gl"
102    ///   <regex-kind> ::= "regex" | "re"
103    ///   <quantifier> ::= "?" | "*" | "+"
104    /// ```
105    ///
106    /// ```
107    /// use scrut::expectation::ExpectationMaker;
108    /// use scrut::rules::registry::RuleRegistry;
109    ///
110    /// let maker = ExpectationMaker::new(RuleRegistry::default());
111    /// maker.parse("foo bar").expect("parses expectation");
112    /// maker
113    ///     .parse("^foo bar$ (regex)")
114    ///     .expect("parses expectation");
115    /// ```
116    pub fn parse(&self, line: &str) -> Result<Expectation> {
117        let (expression, kind, quantifier) = self.extract(line)?;
118        let multiline = quantifier == "*" || quantifier == "+";
119        let optional = quantifier == "*" || quantifier == "?";
120        self.make(
121            &kind,
122            &expression,
123            optional,
124            multiline,
125            &(&line).trim_newlines(),
126        )
127    }
128
129    /// Create an [`Expectation`] from the components that make it up
130    pub(crate) fn make(
131        &self,
132        kind: &str,
133        expression: &str,
134        optional: bool,
135        multiline: bool,
136        original: &str,
137    ) -> Result<Expectation> {
138        Ok(Expectation {
139            optional,
140            multiline,
141            rule: self.0.make(kind, expression)?,
142            original: original.into(),
143        })
144    }
145
146    // TODO: rename return type so that people can understand
147    fn extract(&self, line: &str) -> Result<(String, String, String)> {
148        let captures = self
149            .0
150            .to_expectation_regex()?
151            .captures(line)
152            .map_or(vec![], |captures| {
153                captures
154                    .iter()
155                    .skip(1)
156                    .filter_map(|m| m.map(|v| v.as_str()))
157                    .collect::<Vec<_>>()
158            });
159        if captures.len() == 1 {
160            Ok((line.to_string(), "equal".to_string(), "".to_string()))
161        } else if captures.len() == 2 {
162            Ok((
163                captures[0].to_string(),
164                captures[1].to_string(),
165                "".to_string(),
166            ))
167        } else {
168            Ok((
169                captures[0].to_string(),
170                match captures[1] {
171                    "" => "equal",
172                    v => v,
173                }
174                .to_string(),
175                captures[2].to_string(),
176            ))
177        }
178    }
179}
180
181#[cfg(test)]
182pub(crate) mod tests {
183    use super::ExpectationMaker;
184    use crate::escaping::Escaper;
185    use crate::rules::registry::RuleRegistry;
186
187    #[test]
188    fn test_expectation_extract() {
189        let tests = vec![
190            ("foo", ("foo", "equal", "")),
191            ("foo (?)", ("foo", "equal", "?")),
192            ("foo (*)", ("foo", "equal", "*")),
193            ("foo (+)", ("foo", "equal", "+")),
194            ("foo (eq+)", ("foo", "eq", "+")),
195            ("foo (equal+)", ("foo", "equal", "+")),
196            ("foo (no-eol)", ("foo", "no-eol", "")),
197            ("foo (no-eol?)", ("foo", "no-eol", "?")),
198            ("foo (no-eol*)", ("foo", "no-eol", "*")),
199            ("foo (no-eol+)", ("foo", "no-eol", "+")),
200            ("foo (esc)", ("foo", "esc", "")),
201            ("foo (esc*)", ("foo", "esc", "*")),
202            ("foo (escaped)", ("foo", "escaped", "")),
203            ("foo (escaped+)", ("foo", "escaped", "+")),
204            ("foo (re)", ("foo", "re", "")),
205            ("foo (re?)", ("foo", "re", "?")),
206            ("foo (regex*)", ("foo", "regex", "*")),
207            ("foo (regex+)", ("foo", "regex", "+")),
208            ("foo (glob)", ("foo", "glob", "")),
209            ("foo (glob?)", ("foo", "glob", "?")),
210            ("foo (glob*)", ("foo", "glob", "*")),
211            ("foo (glob+)", ("foo", "glob", "+")),
212            ("foo (glob+) (glob+)", ("foo (glob+)", "glob", "+")),
213        ];
214
215        tests.iter().for_each(
216            |(line, (expect_expression, expect_kind, expect_quantifier))| {
217                let (expression, kind, quantifier) = expectation_maker()
218                    .extract(line)
219                    .expect("extract expression from line");
220                assert_eq!(
221                    expect_expression.to_string(),
222                    expression,
223                    "expression from '{line}'"
224                );
225                assert_eq!(expect_kind.to_string(), kind, "kind from '{line}'");
226                assert_eq!(
227                    expect_quantifier.to_string(),
228                    quantifier,
229                    "quantifier from '{line}'"
230                );
231            },
232        );
233    }
234
235    #[test]
236    fn test_parse_to_expression_string() {
237        let tests = vec![
238            ("foo", "foo"),
239            ("foo (?)", "foo (?)"),
240            ("foo (equal)", "foo"),
241            ("foo (eq)", "foo"),
242            ("foo (equal*)", "foo (*)"),
243            ("foo (no-eol)", "foo (no-eol)"),
244            ("foo (escaped)", "foo (escaped)"),
245            ("foo (esc)", "foo (escaped)"),
246            ("foo (esc+)", "foo (escaped+)"),
247            ("foo (glob)", "foo (glob)"),
248            ("foo (gl)", "foo (glob)"),
249            ("foo (glob?)", "foo (glob?)"),
250            ("foo (regex)", "foo (regex)"),
251            ("foo (re)", "foo (regex)"),
252            ("foo (regex*)", "foo (regex*)"),
253        ];
254        for (from, to) in tests {
255            let expectation = expectation_maker()
256                .parse(from)
257                .unwrap_or_else(|_| panic!("parse `{from}`"));
258            let rendered = expectation.to_expression_string(&Escaper::default());
259            assert_eq!(rendered, *to, "`{from}` rendered back to `{to}`");
260        }
261    }
262
263    pub(crate) fn expectation_maker() -> ExpectationMaker {
264        ExpectationMaker::new(RuleRegistry::default())
265    }
266
267    #[macro_export]
268    macro_rules! test_expectation {
269        ($expression:expr) => {
270            $crate::expectation::tests::expectation_maker()
271                .make("equal", $expression, false, false, $expression)
272                .expect("create test expectation")
273        };
274        ($kind:expr, $expression:expr) => {
275            $crate::expectation::tests::expectation_maker()
276                .make(
277                    $kind,
278                    $expression,
279                    false,
280                    false,
281                    &format!("{} ({})", $expression, $kind),
282                )
283                .expect("create test expectation")
284        };
285        ($kind:expr, $expression:expr, $optional:expr, $multiline:expr) => {
286            $crate::expectation::tests::expectation_maker()
287                .make(
288                    $kind,
289                    $expression,
290                    $optional,
291                    $multiline,
292                    &format!("{} ({})", $expression, $kind),
293                )
294                .expect("create test expectation")
295        };
296        ($kind:expr, $expression:expr, $optional:expr, $multiline:expr, $original:expr) => {
297            $crate::expectation::tests::expectation_maker()
298                .make($kind, $expression, $optional, $multiline, $original)
299                .expect("create test expectation")
300        };
301    }
302}