toml_test_harness/
lib.rs

1//! Verify Rust TOML parsers
2//!
3//! See [`DecoderHarness`] and [`EncoderHarness`]
4//!
5//! For TOML test cases, see [`toml-test-data`](https://docs.rs/toml-test-data).
6//!
7//! To read and write these test cases, see [`toml-test`](https://docs.rs/toml-test).
8
9#![cfg_attr(docsrs, feature(doc_cfg))]
10#![warn(clippy::print_stderr)]
11#![warn(clippy::print_stdout)]
12
13pub use toml_test::DecodedScalar;
14pub use toml_test::DecodedValue;
15pub use toml_test::Decoder;
16pub use toml_test::Encoder;
17pub use toml_test::Error;
18
19/// Run decoder compliance tests
20///
21/// # Example
22///
23/// In `Cargo.toml`:
24/// ```toml
25/// [[test]]
26/// name = "decoder_compliance"
27/// harness = false
28/// ```
29///
30/// `tests/decoder_compliance.rs`
31/// ```rust,no_run
32/// // mod decoder;
33/// # mod decoder {
34/// #   #[derive(Copy, Clone)]
35/// #   pub struct Decoder;
36/// #   impl toml_test_harness::Decoder for Decoder {
37/// #     fn name(&self) -> &'static str { "foo" }
38/// #     fn decode(&self, _: &[u8]) -> Result<toml_test_harness::DecodedValue, toml_test_harness::Error> { todo!() }
39/// #   }
40/// # }
41///
42/// fn main() {
43///     let decoder = decoder::Decoder;
44///     let mut harness = toml_test_harness::DecoderHarness::new(decoder);
45///     harness.version("1.0.0");
46///     harness.ignore([]).unwrap();
47///     harness.test();
48/// }
49/// ```
50pub struct DecoderHarness<D> {
51    decoder: D,
52    matches: Option<Matches>,
53    version: Option<String>,
54    custom_valid: Vec<toml_test_data::Valid<'static>>,
55    custom_invalid: Vec<toml_test_data::Invalid<'static>>,
56    #[cfg(feature = "snapshot")]
57    snapshot_root: Option<std::path::PathBuf>,
58}
59
60impl<D> DecoderHarness<D>
61where
62    D: Decoder + Copy + Send + Sync + 'static,
63{
64    pub fn new(decoder: D) -> Self {
65        Self {
66            decoder,
67            matches: None,
68            version: None,
69            custom_valid: Vec::new(),
70            custom_invalid: Vec::new(),
71            #[cfg(feature = "snapshot")]
72            snapshot_root: None,
73        }
74    }
75
76    pub fn ignore<'p>(
77        &mut self,
78        patterns: impl IntoIterator<Item = &'p str>,
79    ) -> Result<&mut Self, Error> {
80        self.matches = Some(Matches::new(patterns.into_iter())?);
81        Ok(self)
82    }
83
84    pub fn version(&mut self, version: impl Into<String>) -> &mut Self {
85        self.version = Some(version.into());
86        self
87    }
88
89    pub fn extend_valid(
90        &mut self,
91        cases: impl IntoIterator<Item = toml_test_data::Valid<'static>>,
92    ) -> &mut Self {
93        self.custom_valid.extend(cases);
94        self
95    }
96
97    pub fn extend_invalid(
98        &mut self,
99        cases: impl IntoIterator<Item = toml_test_data::Invalid<'static>>,
100    ) -> &mut Self {
101        self.custom_invalid.extend(cases);
102        self
103    }
104
105    #[cfg(feature = "snapshot")]
106    pub fn snapshot_root(&mut self, root: impl Into<std::path::PathBuf>) -> &mut Self {
107        self.snapshot_root = Some(root.into());
108        self
109    }
110
111    pub fn test(self) -> ! {
112        let harness = libtest2_mimic::Harness::with_env();
113
114        let versioned = self
115            .version
116            .as_deref()
117            .into_iter()
118            .flat_map(toml_test_data::version)
119            .collect::<std::collections::HashSet<_>>();
120
121        let mut tests = Vec::new();
122        let decoder = self.decoder;
123        #[cfg(feature = "snapshot")]
124        let snapshot_root = self.snapshot_root;
125        tests.extend(
126            toml_test_data::valid()
127                .map(|case| {
128                    let ignore = !versioned.contains(case.name());
129                    (case, ignore)
130                })
131                .chain(self.custom_valid.into_iter().map(|c| (c, false)))
132                .map(|(case, mut ignore)| {
133                    ignore |= self
134                        .matches
135                        .as_ref()
136                        .map(|m| !m.matched(case.name()))
137                        .unwrap_or_default();
138                    (case, ignore)
139                })
140                .map(move |(case, ignore)| {
141                    libtest2_mimic::Trial::test(case.name().display().to_string(), move |context| {
142                        if ignore {
143                            context.ignore()?;
144                        }
145                        decoder
146                            .verify_valid_case(case.fixture(), case.expected())
147                            .map_err(libtest2_mimic::RunError::fail)
148                    })
149                }),
150        );
151        tests.extend(
152            toml_test_data::invalid()
153                .map(|case| {
154                    let ignore = !versioned.contains(case.name());
155                    (case, ignore)
156                })
157                .chain(self.custom_invalid.into_iter().map(|c| (c, false)))
158                .map(|(case, mut ignore)| {
159                    ignore |= self
160                        .matches
161                        .as_ref()
162                        .map(|m| !m.matched(case.name()))
163                        .unwrap_or_default();
164                    (case, ignore)
165                })
166                .map(move |(case, ignore)| {
167                    #[cfg(feature = "snapshot")]
168                    let snapshot_root = snapshot_root.clone();
169                    libtest2_mimic::Trial::test(case.name().display().to_string(), move |context| {
170                        if ignore {
171                            context.ignore()?;
172                        }
173                        match decoder.verify_invalid_case(case.fixture()) {
174                            Ok(_err) => {
175                                #[cfg(feature = "snapshot")]
176                                if let Some(snapshot_root) = snapshot_root.as_deref() {
177                                    let snapshot_path =
178                                        snapshot_root.join(case.name().with_extension("stderr"));
179                                    snapbox::assert_data_eq!(
180                                        _err.to_string(),
181                                        snapbox::Data::read_from(&snapshot_path, None).raw()
182                                    );
183                                }
184                                Ok(())
185                            }
186                            Err(err) => Err(libtest2_mimic::RunError::fail(err)),
187                        }
188                    })
189                }),
190        );
191        harness.discover(tests).main()
192    }
193}
194
195/// Run encoder compliance tests
196///
197/// <div class="warning">
198///
199/// [`DecoderHarness`] must pass on your [`Decoder`] fixture for this to work
200///
201/// </div>
202///
203/// # Example
204///
205/// In `Cargo.toml`:
206/// ```toml
207/// [[test]]
208/// name = "encoder_compliance"
209/// harness = false
210/// ```
211///
212/// `tests/encoder_compliance.rs`
213/// ```rust,no_run
214/// // mod decoder;
215/// // mod encoder;
216/// # mod decoder {
217/// #   #[derive(Copy, Clone)]
218/// #   pub struct Decoder;
219/// #   impl toml_test_harness::Decoder for Decoder {
220/// #     fn name(&self) -> &'static str { "foo" }
221/// #     fn decode(&self, _: &[u8]) -> Result<toml_test_harness::DecodedValue, toml_test_harness::Error> { todo!() }
222/// #   }
223/// # }
224/// # mod encoder {
225/// #   #[derive(Copy, Clone)]
226/// #   pub struct Encoder;
227/// #   impl toml_test_harness::Encoder for Encoder {
228/// #     fn name(&self) -> &'static str { "foo" }
229/// #     fn encode(&self, _: toml_test_harness::DecodedValue) -> Result<String, toml_test_harness::Error> { todo!() }
230/// #   }
231/// # }
232///
233/// fn main() {
234///     let encoder = encoder::Encoder;
235///     let decoder = decoder::Decoder;
236///     let mut harness = toml_test_harness::EncoderHarness::new(encoder, decoder);
237///     harness.version("1.0.0");
238///     harness.test();
239/// }
240/// ```
241pub struct EncoderHarness<E, D> {
242    encoder: E,
243    fixture: D,
244    matches: Option<Matches>,
245    version: Option<String>,
246    custom_valid: Vec<toml_test_data::Valid<'static>>,
247}
248
249impl<E, D> EncoderHarness<E, D>
250where
251    E: Encoder + Copy + Send + Sync + 'static,
252    D: Decoder + Copy + Send + Sync + 'static,
253{
254    pub fn new(encoder: E, fixture: D) -> Self {
255        Self {
256            encoder,
257            fixture,
258            matches: None,
259            version: None,
260            custom_valid: Vec::new(),
261        }
262    }
263
264    pub fn ignore<'p>(
265        &mut self,
266        patterns: impl IntoIterator<Item = &'p str>,
267    ) -> Result<&mut Self, Error> {
268        self.matches = Some(Matches::new(patterns.into_iter())?);
269        Ok(self)
270    }
271
272    pub fn version(&mut self, version: impl Into<String>) -> &mut Self {
273        self.version = Some(version.into());
274        self
275    }
276
277    pub fn extend_valid(
278        &mut self,
279        cases: impl IntoIterator<Item = toml_test_data::Valid<'static>>,
280    ) -> &mut Self {
281        self.custom_valid.extend(cases);
282        self
283    }
284
285    pub fn test(self) -> ! {
286        let harness = libtest2_mimic::Harness::with_env();
287
288        let versioned = self
289            .version
290            .as_deref()
291            .into_iter()
292            .flat_map(toml_test_data::version)
293            .collect::<std::collections::HashSet<_>>();
294
295        let mut tests = Vec::new();
296        let encoder = self.encoder;
297        let fixture = self.fixture;
298        tests.extend(
299            toml_test_data::valid()
300                .map(|case| {
301                    let ignore = !versioned.contains(case.name());
302                    (case, ignore)
303                })
304                .chain(self.custom_valid.into_iter().map(|c| (c, false)))
305                .map(|(case, mut ignore)| {
306                    ignore |= self
307                        .matches
308                        .as_ref()
309                        .map(|m| !m.matched(case.name()))
310                        .unwrap_or_default();
311                    (case, ignore)
312                })
313                .map(move |(case, ignore)| {
314                    libtest2_mimic::Trial::test(case.name().display().to_string(), move |context| {
315                        if ignore {
316                            context.ignore()?;
317                        }
318                        encoder
319                            .verify_valid_case(case.expected(), &fixture)
320                            .map_err(libtest2_mimic::RunError::fail)
321                    })
322                }),
323        );
324        harness.discover(tests).main()
325    }
326}
327
328struct Matches {
329    ignores: ignore::gitignore::Gitignore,
330}
331
332impl Matches {
333    fn new<'p>(patterns: impl Iterator<Item = &'p str>) -> Result<Self, Error> {
334        let mut ignores = ignore::gitignore::GitignoreBuilder::new(".");
335        for line in patterns {
336            ignores.add_line(None, line).map_err(Error::new)?;
337        }
338        let ignores = ignores.build().map_err(Error::new)?;
339        Ok(Self { ignores })
340    }
341
342    fn matched(&self, path: &std::path::Path) -> bool {
343        match self.ignores.matched_path_or_any_parents(path, false) {
344            ignore::Match::None | ignore::Match::Whitelist(_) => true,
345            ignore::Match::Ignore(_) => false,
346        }
347    }
348}
349
350#[doc = include_str!("../README.md")]
351#[cfg(doctest)]
352pub struct ReadmeDoctests;