datatest_stable/lib.rs
1// Copyright (c) The datatest-stable Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4#![forbid(unsafe_code)]
5
6//! `datatest-stable` is a test harness intended to write *file-driven* or *data-driven* tests,
7//! where individual test case fixtures are specified as files and not as code.
8//!
9//! Given:
10//!
11//! * a test `my_test` that accepts a path, and optionally the contents as input
12//! * a directory to look for files (test fixtures) in
13//! * a pattern to match files on
14//!
15//! `datatest-stable` will call the `my_test` function once per matching file in
16//! the directory. Directory traversals are recursive.
17//!
18//! `datatest-stable` works with [cargo nextest](https://nexte.st/), and is part
19//! of the [nextest-rs organization](https://github.com/nextest-rs/) on GitHub.
20//! With nextest, each test case is represented as a separate test, and is run
21//! as a separate process in parallel.
22//!
23//! # Usage
24//!
25//! 1. Configure the test target by setting `harness = false` in `Cargo.toml`:
26//!
27//! ```toml
28//! [[test]]
29//! name = "<test target name>"
30//! harness = false
31//! ```
32//!
33//! 2. Call the `datatest_stable::harness!` macro as:
34//!
35//! ```rust,ignore
36//! datatest_stable::harness! {
37//! { test = my_test, root = "path/to/fixtures", pattern = r".*" },
38//! }
39//! ```
40//!
41//! * `test` - The test function to be executed on each matching input. This function can be one
42//! of:
43//! * `fn(&Path) -> datatest_stable::Result<()>`
44//! * `fn(&Utf8Path) -> datatest_stable::Result<()>` ([`Utf8Path`](camino::Utf8Path) is part of the
45//! [`camino`] library, and is re-exported here for convenience.)
46//! * `fn(&P, String) -> datatest_stable::Result<()>` where `P` is `Path` or `Utf8Path`. If the
47//! extra `String` parameter is specified, the contents of the file will be loaded and passed in
48//! as a string (erroring out if that failed).
49//! * `fn(&P, Vec<u8>) -> datatest_stable::Result<()>` where `P` is `Path` or `Utf8Path`. If the
50//! extra `Vec<u8>` parameter is specified, the contents of the file will be loaded and passed
51//! in as a `Vec<u8>` (erroring out if that failed).
52//!
53//! * `root` - The path to the root directory where the input files (fixtures)
54//! live. Relative paths are resolved relative to the crate root (the directory where the crate's
55//! `Cargo.toml` is located).
56//!
57//! `root` is an arbitrary expression that implements
58//! [`Display`](std::fmt::Display), such as `&str`, or a function call that
59//! returns a [`Utf8PathBuf`](camino::Utf8PathBuf).
60//!
61//! * `pattern` - a regex used to match against and select each file to be tested. Extended regexes
62//! with lookaround and backtracking are supported via the [`fancy_regex`] crate.
63//!
64//! `pattern` is an arbitrary expression that implements [`Display`](std::fmt::Display), such as
65//! `&str`, or a function call that returns a `String`.
66//!
67//! `pattern` is optional, and defaults to `r".*"` (match all files).
68//!
69//! The three parameters can be repeated if you have multiple sets of data-driven tests to be run:
70//!
71//! ```rust,ignore
72//! datatest_stable::harness! {
73//! { test = testfn1, root = root1, pattern = pattern1 },
74//! { test = testfn2, root = root2 },
75//! }
76//! ```
77//!
78//! Trailing commas are optional.
79//!
80//! ## Relative and absolute paths
81//!
82//! The `pattern` argument is tested against the **relative** path of each file,
83//! **excluding** the `root` prefix -- not the absolute path.
84//!
85//! The `Path` and `Utf8Path` passed into the test functions are created by
86//! joining `root` to the relative path of the file.
87//!
88//! * If `root` is **relative**, the paths passed in are relative to the crate root.
89//! * If `root` is **absolute**, the paths passed in are absolute.
90//!
91//! For uniformity, all relative paths use `/` as the path separator,
92//! including on Windows, and all absolute paths use the platform's native
93//! separator throughout.
94//!
95//! ## Examples
96//!
97//! This is an example test. Use it with `harness = false`.
98//!
99//! ```rust
100//! use datatest_stable::Utf8Path;
101//! use std::path::Path;
102//!
103//! fn my_test(path: &Path) -> datatest_stable::Result<()> {
104//! // ... write test here
105//!
106//! Ok(())
107//! }
108//!
109//! fn my_test_utf8(path: &Utf8Path, contents: String) -> datatest_stable::Result<()> {
110//! // ... write test here
111//!
112//! Ok(())
113//! }
114//!
115//! datatest_stable::harness! {
116//! { test = my_test, root = "path/to/fixtures" },
117//! {
118//! test = my_test_utf8,
119//! root = "path/to/fixtures",
120//! pattern = r"^.*\.txt$",
121//! },
122//! }
123//! ```
124//!
125//! If `path/to/fixtures` contains a file `foo/bar.txt`, then:
126//!
127//! * The pattern `r"^.*/*"` will match `foo/bar.txt`.
128//! * `my_test` and `my_test_utf8` will be called with `"path/to/fixtures/foo/bar.txt"`.
129//!
130//! ## Embedding directories at compile time
131//!
132//! With the `include-dir` feature enabled, you can use the
133//! [`include_dir`](https://docs.rs/include_dir) crate's [`include_dir!`] macro.
134//! This allows you to embed directories into the binary at compile time.
135//!
136//! This is generally not recommended for rapidly-changing test data, since each
137//! change will force a rebuild. But it can be useful for relatively-unchanging
138//! data suites distributed separately, e.g. on crates.io.
139//!
140//! With the `include-dir` feature enabled, you can use:
141//!
142#![cfg_attr(feature = "include-dir", doc = "```rust")]
143#![cfg_attr(not(feature = "include-dir"), doc = "```rust,ignore")]
144//! // The `include_dir!` macro is re-exported for convenience.
145//! use datatest_stable::include_dir;
146//! use std::path::Path;
147//!
148//! fn my_test(path: &Path, contents: Vec<u8>) -> datatest_stable::Result<()> {
149//! // ... write test here
150//! Ok(())
151//! }
152//!
153//! datatest_stable::harness! {
154//! { test = my_test, root = include_dir!("tests/files"), pattern = r"^.*\.json$" },
155//! }
156//! ```
157//!
158//! You can also use directories published as `static` items in upstream crates:
159//!
160#![cfg_attr(feature = "include-dir", doc = "```rust")]
161#![cfg_attr(not(feature = "include-dir"), doc = "```rust,ignore")]
162//! use datatest_stable::{include_dir, Utf8Path};
163//!
164//! // In the upstream crate:
165//! pub static FIXTURES: include_dir::Dir<'static> =
166//! include_dir!("$CARGO_MANIFEST_DIR/tests/files");
167//!
168//! // In your test:
169//! fn my_test(path: &Utf8Path, contents: String) -> datatest_stable::Result<()> {
170//! // ... write test here
171//! Ok(())
172//! }
173//!
174//! datatest_stable::harness! {
175//! { test = my_test, root = &FIXTURES },
176//! }
177//! ```
178//!
179//! In this case, the passed-in `Path` and `Utf8Path` are always **relative** to
180//! the root of the included directory. Like elsewhere in `datatest-stable`,
181//! these relative paths always use forward slashes as separators, including on
182//! Windows.
183//!
184//! Because the files don't exist on disk, the test functions must accept their
185//! contents as either a `String` or a `Vec<u8>`. If the argument is not
186//! provided, the harness will panic at runtime.
187//!
188//! ## Conditionally embedding directories
189//!
190//! It is also possible to conditionally include directories at compile time via
191//! a feature flag. For example, you might have an internal-only `testing`
192//! feature that you turn on locally, but users don't on crates.io. In that
193//! case, you can use:
194//!
195#![cfg_attr(feature = "include-dir", doc = "```rust")]
196#![cfg_attr(not(feature = "include-dir"), doc = "```rust,ignore")]
197//! use datatest_stable::Utf8Path;
198//!
199//! // In the library itself:
200//! pub mod fixtures {
201//! #[cfg(feature = "testing")]
202//! pub static FIXTURES: &str = "tests/files";
203//!
204//! #[cfg(not(feature = "testing"))]
205//! pub static FIXTURES: include_dir::Dir<'static> =
206//! include_dir::include_dir!("$CARGO_MANIFEST_DIR/tests/files");
207//! }
208//!
209//! // In the test:
210//! fn my_test(path: &Utf8Path, contents: String) -> datatest_stable::Result<()> {
211//! // ... write test here
212//! Ok(())
213//! }
214//!
215//! datatest_stable::harness! {
216//! { test = my_test, root = &fixtures::FIXTURES, pattern = r"^inputs/.*$" },
217//! }
218//! ```
219//!
220//! In this case, note that `path` will be relative to the **crate directory**
221//! (e.g. `tests/files/foo/bar.txt`) if `FIXTURES` is a string, and relative to
222//! the **include directory** (e.g. `foo/bar.txt`) if `FIXTURES` is a
223//! [`Dir`](include_dir::Dir). Your test should be prepared to handle either
224//! case.
225//!
226//! # Features
227//!
228//! * `include-dir`: Enables the `include_dir!` macro, which allows embedding
229//! directories at compile time. This feature is disabled by default.
230//!
231//! # Minimum supported Rust version (MSRV)
232//!
233//! The minimum supported Rust version is **Rust 1.72**. MSRV bumps may be accompanied by a minor
234//! version update; at any time, Rust versions from at least the last 6 months are supported.
235//!
236//! # See also
237//!
238//! * [`datatest`](https://crates.io/crates/datatest): the original inspiration for this crate, with
239//! more features but targeting nightly Rust.
240//! * [Data-driven testing](https://en.wikipedia.org/wiki/Data-driven_testing)
241
242#![warn(missing_docs)]
243#![cfg_attr(doc_cfg, feature(doc_cfg, doc_auto_cfg))]
244
245mod data_source;
246mod macros;
247mod runner;
248
249/// The result type for `datatest-stable` tests.
250pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
251
252#[doc(hidden)]
253pub use self::data_source::{data_source_kinds, DataSource};
254/// Not part of the public API, just used for macros.
255#[doc(hidden)]
256pub use self::runner::{runner, test_kinds, Requirements, TestFn};
257/// A re-export of this type from the `camino` crate, since it forms part of function signatures.
258#[doc(no_inline)]
259pub use camino::Utf8Path;
260/// A re-export of `include_dir!` from the `include_dir` crate, for convenience.
261#[cfg(feature = "include-dir")]
262#[doc(no_inline)]
263pub use include_dir::include_dir;