Skip to main content

cedar_policy_core/
test_utils.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#![expect(
18    clippy::panic,
19    clippy::unwrap_used,
20    clippy::expect_used,
21    reason = "testing code"
22)]
23
24//! Shared test utilities.
25
26/// Describes the contents of an error message. Fields are based on the contents
27/// of `miette::Diagnostic`.
28#[derive(Debug)]
29pub struct ExpectedErrorMessage<'a> {
30    /// Expected contents of `Display`, or expected prefix of `Display` if `prefix` is `true`
31    error: &'a str,
32    /// Expected contents of `help()`, or `None` if no help, or expected prefix of `help()` if `prefix` is `true`
33    help: Option<&'a str>,
34    /// If `true`, then `error`, `help`, and `source` are interpreted as expected
35    /// prefixes of the relevant messages, and [`expect_err()`] will allow the
36    /// actual messages to have additional characters after the ones that are expected.
37    prefix: bool,
38    /// Expected text that is underlined by miette (text found at the error's
39    /// source location(s)) plus (optional) help text associated with the
40    /// underline.
41    /// If this is an empty vec, we expect the error to have no associated
42    /// source location.
43    /// If this is a vec with one or more elements, we expect the same number of
44    /// miette `labels` in the same order, and the vec elements represent the
45    /// expected contents of the labels.
46    underlines: Vec<(&'a str, Option<&'a str>)>,
47    /// A message describing the cause of this error
48    source: Option<&'a str>,
49}
50
51/// Builder struct for [`ExpectedErrorMessage`]
52#[derive(Debug)]
53pub struct ExpectedErrorMessageBuilder<'a> {
54    /// ExpectedErrorMessage::error
55    error: &'a str,
56    /// ExpectedErrorMessage::help
57    help: Option<&'a str>,
58    /// ExpectedErrorMessage::prefix
59    prefix: bool,
60    /// ExpectedErrorMessage::underlines
61    underlines: Vec<(&'a str, Option<&'a str>)>,
62    /// ExpectedErrorMessage::source
63    source: Option<&'a str>,
64}
65
66impl<'a> ExpectedErrorMessageBuilder<'a> {
67    /// Create a builder expecting the given main error message (contents of
68    /// `Display`)
69    pub fn error(msg: &'a str) -> Self {
70        Self {
71            error: msg,
72            help: None,
73            prefix: false,
74            underlines: vec![],
75            source: None,
76        }
77    }
78
79    /// Create a builder expecting the main error message (contents of
80    /// `Display`) to _start with_ the given text.
81    ///
82    /// (If you later add expected help text to this builder, that will
83    /// also be an expected prefix, not the entire expected help text.)
84    pub fn error_starts_with(msg: &'a str) -> Self {
85        Self {
86            error: msg,
87            help: None,
88            prefix: true,
89            underlines: vec![],
90            source: None,
91        }
92    }
93
94    /// Add expected contents of `help()`, or expected prefix of `help()` if
95    /// this builder was originally constructed with `error_starts_with()`
96    pub fn help(self, msg: &'a str) -> Self {
97        Self {
98            help: Some(msg),
99            ..self
100        }
101    }
102
103    /// Add expected underlined text. The error message will be expected to have
104    /// exactly one miette label, and the underlined portion should be `snippet`.
105    /// The underlined text should have no associated label text.
106    pub fn exactly_one_underline(self, snippet: &'a str) -> Self {
107        Self {
108            underlines: vec![(snippet, None)],
109            ..self
110        }
111    }
112
113    /// Add expected underlined text. The error message will be expected to have
114    /// exactly one miette label, and the underlined portion should be `snippet`.
115    /// The label text is expected to match `label`.
116    pub fn exactly_one_underline_with_label(self, snippet: &'a str, label: &'a str) -> Self {
117        Self {
118            underlines: vec![(snippet, Some(label))],
119            ..self
120        }
121    }
122
123    /// Add expected underlined text. The error message will be expected to have
124    /// exactly two miette labels, and the underlined portions should be `snippet1`
125    /// and `snippet2`, in that order.
126    /// Both labels should have no associated label text.
127    pub fn exactly_two_underlines(self, snippet1: &'a str, snippet2: &'a str) -> Self {
128        Self {
129            underlines: vec![(snippet1, None), (snippet2, None)],
130            ..self
131        }
132    }
133
134    /// Add expected underlined text. The error message will be expected to have
135    /// exactly this many miette labels, and for each label (a, b), the underlined
136    /// portion should be `a` and the label text should be `b` (or `None` for no
137    /// label text).
138    pub fn with_underlines_or_labels(
139        self,
140        labels: impl IntoIterator<Item = (&'a str, Option<&'a str>)>,
141    ) -> Self {
142        Self {
143            underlines: labels.into_iter().collect(),
144            ..self
145        }
146    }
147
148    /// Add expected contents of `source()`, or expected prefix of `source()` if
149    /// this builder was originally constructed with `error_starts_with()`
150    pub fn source(self, msg: &'a str) -> Self {
151        Self {
152            source: Some(msg),
153            ..self
154        }
155    }
156
157    /// Build the [`ExpectedErrorMessage`]
158    pub fn build(self) -> ExpectedErrorMessage<'a> {
159        ExpectedErrorMessage {
160            error: self.error,
161            help: self.help,
162            prefix: self.prefix,
163            underlines: self.underlines,
164            source: self.source,
165        }
166    }
167}
168
169impl<'a> ExpectedErrorMessage<'a> {
170    /// Return a boolean indicating whether a given error matches this expected message.
171    /// (If you want to assert that it matches, use [`expect_err()`] instead,
172    /// for much better assertion-failure messages.)
173    pub fn matches(&self, error: &impl miette::Diagnostic) -> bool {
174        self.matches_error(error)
175            && self.matches_help(error)
176            && self.matches_source(error)
177            && self.matches_underlines(error)
178    }
179
180    /// Internal helper: whether the main error message matches
181    fn matches_error(&self, error: &impl miette::Diagnostic) -> bool {
182        let e_string = error.to_string();
183        if self.prefix {
184            e_string.starts_with(self.error)
185        } else {
186            e_string == self.error
187        }
188    }
189
190    /// Internal helper: assert the main error message matches
191    #[track_caller]
192    fn expect_error_matches(&self, src: impl Into<OriginalInput<'a>>, error: &miette::Report) {
193        let e_string = error.to_string();
194        if self.prefix {
195            assert!(
196                e_string.starts_with(self.error),
197                "for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual error did not start with the expected prefix\n  actual error: {error}\n  expected prefix: {}", // the Debug representation of miette::Report is the pretty one
198                src.into(),
199                self.error,
200            );
201        } else {
202            assert_eq!(
203                &e_string,
204                self.error,
205                "for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual error did not match expected", // assert_eq! will print the actual and expected messages
206                src.into(),
207            );
208        }
209    }
210
211    /// Internal helper: whether the help message matches
212    fn matches_help(&self, error: &impl miette::Diagnostic) -> bool {
213        let h_string = error.help().map(|h| h.to_string());
214        if self.prefix {
215            match (h_string.as_deref(), self.help) {
216                (Some(actual), Some(expected)) => actual.starts_with(expected),
217                (None, None) => true,
218                _ => false,
219            }
220        } else {
221            h_string.as_deref() == self.help
222        }
223    }
224
225    /// Internal helper: whether the source message matches
226    fn matches_source(&self, error: &impl miette::Diagnostic) -> bool {
227        let s_string = error.source().map(|s| s.to_string());
228        if self.prefix {
229            match (s_string.as_deref(), self.source) {
230                (Some(actual), Some(expected)) => actual.starts_with(expected),
231                (None, None) => true,
232                _ => false,
233            }
234        } else {
235            s_string.as_deref() == self.source
236        }
237    }
238
239    /// Internal helper used to check help or source messages
240    #[track_caller]
241    fn expect_help_or_source_matches(
242        &self,
243        src: impl Into<OriginalInput<'a>>,
244        error: &miette::Report,
245        h_or_s: &str,
246        actual: Option<&str>,
247        expected: Option<&str>,
248    ) {
249        if self.prefix {
250            match (actual, expected) {
251                (Some(actual), Some(expected)) => {
252                    assert!(
253                        actual.starts_with(expected),
254                        "for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual {h_or_s} did not start with the expected prefix\n  actual {h_or_s}: {actual}\n  expected {h_or_s}: {expected}", // the Debug representation of miette::Report is the pretty one
255                        src.into(),
256                    )
257                }
258                (None, None) => (),
259                (Some(actual), None) => panic!(
260                    "for the following input:\n{}\nfor the following error:\n{error:?}\n\ndid not expect a {h_or_s} message, but found one: {actual}", // the Debug representation of miette::Report is the pretty one
261                    src.into(),
262                ),
263                (None, Some(expected)) => panic!(
264                    "for the following input:\n{}\nfor the following error:\n{error:?}\n\ndid not find a {h_or_s} message, but expected one: {expected}", // the Debug representation of miette::Report is the pretty one
265                    src.into(),
266                ),
267            }
268        } else {
269            assert_eq!(
270                actual,
271                expected,
272                "for the following input:\n{}\nfor the following error:\n{error:?}\n\nactual {h_or_s} did not match expected", // assert_eq! will print the actual and expected messages
273                src.into(),
274            );
275        }
276    }
277
278    /// Internal helper: assert the help message matches
279    #[track_caller]
280    fn expect_help_matches(&self, src: impl Into<OriginalInput<'a>>, error: &miette::Report) {
281        let h_string = error.help().map(|h| h.to_string());
282        self.expect_help_or_source_matches(src, error, "help", h_string.as_deref(), self.help);
283    }
284
285    /// Internal helper: assert the source message matches
286    #[track_caller]
287    fn expect_source_matches(&self, src: impl Into<OriginalInput<'a>>, error: &miette::Report) {
288        let s_string = error.source().map(|s| s.to_string());
289        self.expect_help_or_source_matches(src, error, "source", s_string.as_deref(), self.source);
290    }
291
292    /// Internal helper: whether the underlines match
293    fn matches_underlines(&self, err: &impl miette::Diagnostic) -> bool {
294        let expected_num_labels = self.underlines.len();
295        let actual_num_labels = err.labels().map(|iter| iter.count()).unwrap_or(0);
296        if expected_num_labels != actual_num_labels {
297            return false;
298        }
299        if expected_num_labels != 0 {
300            let src = err
301                .source_code()
302                .expect("err.source_code() should be `Some` if we are expecting underlines");
303            for (expected, actual) in self
304                .underlines
305                .iter()
306                .zip(err.labels().unwrap_or_else(|| Box::new(std::iter::empty())))
307            {
308                let (expected_snippet, expected_label) = expected;
309                let actual_snippet = {
310                    let span = actual.inner();
311                    std::str::from_utf8(src.read_span(span, 0, 0).expect("should read span").data())
312                        .expect("should be utf8 encoded")
313                };
314                let actual_label = actual.label();
315                if expected_snippet != &actual_snippet {
316                    return false;
317                }
318                if expected_label != &actual_label {
319                    return false;
320                }
321            }
322        }
323        true
324    }
325
326    /// Internal helper: assert the underlines match
327    #[track_caller]
328    fn expect_underlines_match(&self, err: &miette::Report) {
329        let expected_num_labels = self.underlines.len();
330        let actual_num_labels = err.labels().map(|iter| iter.count()).unwrap_or(0);
331        assert_eq!(expected_num_labels, actual_num_labels, "in the following error:\n{err:?}\n\nexpected {expected_num_labels} underlines but found {actual_num_labels}"); // the Debug representation of miette::Report is the pretty one
332        if expected_num_labels != 0 {
333            let src = err
334                .source_code()
335                .expect("err.source_code() should be `Some` if we are expecting underlines");
336            for (expected, actual) in self
337                .underlines
338                .iter()
339                .zip(err.labels().unwrap_or_else(|| Box::new(std::iter::empty())))
340            {
341                let (expected_snippet, expected_label) = expected;
342                let actual_snippet = {
343                    let span = actual.inner();
344                    std::str::from_utf8(src.read_span(span, 0, 0).expect("should read span").data())
345                        .expect("should be utf8 encoded")
346                };
347                let actual_label = actual.label();
348                assert_eq!(
349                    expected_snippet,
350                    &actual_snippet,
351                    "in the following error:\n{err:?}\n\nexpected underlined portion to be:\n  {expected_snippet}\nbut it was:\n  {actual_snippet}", // the Debug representation of miette::Report is the pretty one
352                );
353                assert_eq!(
354                    expected_label,
355                    &actual_label,
356                    "in the following error:\n{err:?}\n\nexpected underlined help text to be:\n  {expected_label:?}\nbut it was:\n  {actual_label:?}", // the Debug representation of miette::Report is the pretty one
357                );
358            }
359        }
360    }
361}
362
363impl std::fmt::Display for ExpectedErrorMessage<'_> {
364    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365        if self.prefix {
366            writeln!(f, "expected error to start with: {}", self.error)?;
367            match self.help {
368                Some(help) => writeln!(f, "expected help to start with: {help}")?,
369                None => writeln!(f, "  with no help message")?,
370            }
371        } else {
372            writeln!(f, "expected error: {}", self.error)?;
373            match self.help {
374                Some(help) => writeln!(f, "expected help: {help}")?,
375                None => writeln!(f, "  with no help message")?,
376            }
377        }
378        if self.underlines.is_empty() {
379            writeln!(f, "and expected no source locations / underlined segments.")?;
380        } else {
381            writeln!(f, "and expected the following underlined segments:")?;
382            for (underline, label) in &self.underlines {
383                writeln!(f, "  {underline}")?;
384                if let Some(label) = label {
385                    writeln!(f, "  with label {label}")?
386                }
387            }
388        }
389        Ok(())
390    }
391}
392
393/// Forms in which [`expect_err()`] accepts the original input text.
394/// See notes on [`expect_err()`].
395#[derive(Debug)]
396pub enum OriginalInput<'a> {
397    /// A plain string
398    String(&'a str),
399    /// A JSON value. We will not incur the cost of formatting this to
400    /// string unless it is actually needed.
401    Json(&'a serde_json::Value),
402}
403
404impl<'a> From<&'a str> for OriginalInput<'a> {
405    fn from(value: &'a str) -> Self {
406        Self::String(value)
407    }
408}
409
410impl<'a> From<&'a serde_json::Value> for OriginalInput<'a> {
411    fn from(value: &'a serde_json::Value) -> Self {
412        Self::Json(value)
413    }
414}
415
416impl std::fmt::Display for OriginalInput<'_> {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        match self {
419            Self::String(s) => write!(f, "{s}"),
420            Self::Json(val) => write!(f, "{}", serde_json::to_string_pretty(val).unwrap()),
421        }
422    }
423}
424
425/// Expect that the given `err` is an error with the given `ExpectedErrorMessage`.
426///
427/// `src` is the original input text, just for better assertion-failure messages.
428/// This function accepts any `impl Into<OriginalInput>` for `src`,
429/// including `&str` and `&serde_json::Value`.
430#[track_caller] // report the caller's location as the location of the panic, not the location in this function
431pub fn expect_err<'a>(
432    src: impl Into<OriginalInput<'a>> + Copy,
433    err: &miette::Report,
434    msg: &ExpectedErrorMessage<'a>,
435) {
436    msg.expect_error_matches(src, err);
437    msg.expect_help_matches(src, err);
438    msg.expect_source_matches(src, err);
439    msg.expect_underlines_match(err);
440}
441
442/// Assert equality of `Entities` using structural equality with the `deep_eq` method.
443#[macro_export]
444macro_rules! assert_deep_eq {
445    ( $self:expr , $other:expr ) => {
446        assert!(
447            $self.deep_eq(&$other),
448            "expected that {:?} would be structurally equal to {:?}",
449            $self,
450            $other
451        )
452    };
453}
454
455/// Assert inequality of `Entities` using structural equality with the `deep_eq` method.
456#[macro_export]
457macro_rules! assert_not_deep_eq {
458    ( $self:expr , $other:expr ) => {
459        assert!(
460            !$self.deep_eq(&$other),
461            "expected that {:?} would not be structurally equal to {:?}",
462            $self,
463            $other
464        )
465    };
466}