Skip to main content

test_casing/
lib.rs

1//! Minimalistic testing framework for generating tests for a given set of test cases
2//! and decorating them to add retries, timeouts, sequential test processing etc. In other words,
3//! the framework implements:
4//!
5//! - Parameterized tests of reasonably low cardinality for the standard Rust test runner
6//! - Fully code-based, composable and extensible test decorators.
7//!
8//! # Overview
9//!
10//! ## Test cases
11//!
12//! [`test_casing`](macro@test_casing) attribute macro wraps a free-standing function
13//! with one or more arguments and transforms it into a collection of test cases.
14//! The arguments to the function are supplied by an iterator (more precisely,
15//! an expression implementing [`IntoIterator`]).
16//!
17//! For convenience, there is [`TestCases`], a lazy iterator wrapper that allows constructing
18//! test cases which cannot be constructed in compile time (e.g., ones requiring access to heap).
19//! [`TestCases`] can be instantiated using the [`cases!`] macro.
20//!
21//! Since a separate test wrapper is generated for each case, their number should be
22//! reasonably low (roughly speaking, no more than 20).
23//! Isolating each test case makes most sense if the cases involve some heavy lifting
24//! (spinning up a runtime, logging considerable amount of information, etc.).
25//!
26//! ## Test decorators
27//!
28//! [`decorate`] attribute macro can be placed on a test function to add generic functionality,
29//! such as retries, timeouts or running tests in a sequence.
30//!
31//! The [`decorators`] module defines some basic decorators and the
32//! [`DecorateTest`](decorators::DecorateTest) trait allowing to define custom decorators.
33//! Test decorators support async tests, tests returning `Result`s and test cases; see
34//! the module docs for more details.
35//!
36//! # Test cases structure
37//!
38//! The generated test cases are placed in a module with the same name as the target function
39//! near the function.
40//! This allows specifying the (potentially qualified) function name to restrict the test scope.
41//!
42//! If the [`nightly` crate feature](#nightly) is not enabled, names of particular test cases
43//! are not descriptive; they have the `case_NN` format, where `NN` is the 0-based case index.
44//! The values of arguments provided to the test are printed to the standard output
45//! at the test start. (The standard output is captured and thus may be not visible
46//! unless the `--nocapture` option is specified in the `cargo test` command.)
47//!
48//! If the `nightly` feature *is* enabled, the names are more descriptive, containing [`Debug`]
49//! presentation of all args together with their names. Here's an excerpt from the integration
50//! tests for this crate:
51//!
52//! ```text
53//! test number_can_be_converted_to_string::case_1 [number = 3, expected = "3"] ... ok
54//! test number_can_be_converted_to_string::case_2 [number = 5, expected = "5"] ... ok
55//! test numbers_are_large::case_0 [number = 2] ... ignored, testing that `#[ignore]` attr works
56//! test numbers_are_large::case_1 [number = 3] ... ignored, testing that `#[ignore]` attr works
57//! test string_conversion_fail::case_0 [bogus_str = "not a number"] - should panic ... ok
58//! test string_conversion_fail::case_1 [bogus_str = "-"] - should panic ... ok
59//! test string_conversion_fail::case_2 [bogus_str = ""] - should panic ... ok
60//! ```
61//!
62//! The names are fully considered when filtering tests, meaning that it's possible to run
63//! particular cases using a filter like `cargo test 'number = 5'`.
64//!
65//! # Alternatives and similar tools
66//!
67//! - The approach to test casing from this crate can be reproduced with some amount of copy-pasting
68//!   by manually feeding necessary inputs to a common parametric testing function.
69//!   Optionally, these tests may be collected in a module for better structuring.
70//!   The main downside of this approach is the amount of copy-pasting.
71//! - Alternatively, multiple test cases may be run in a single `#[test]` (e.g., in a loop).
72//!   This is fine for the large amount of small cases (e.g., mini-fuzzing), but may have downsides
73//!   such as overflowing or overlapping logs and increased test runtimes.
74//! - The [`test-case`] crate uses a similar approach to test case structuring, but differs
75//!   in how test case inputs are specified. Subjectively, the approach used by this crate
76//!   is more extensible and easier to read.
77//! - [Property testing] / [`quickcheck`]-like frameworks provide much more exhaustive approach
78//!   to parameterized testing, but they require significantly more setup effort.
79//! - [`rstest`] supports test casing and some of the test decorators (e.g., timeouts).
80//! - [`nextest`] is an alternative test runner that supports most of the test decorators
81//!   defined in the [`decorators`] module. It does not use code-based decorator config and
82//!   does not allow for custom decorator.
83//!
84//! [`test-case`]: https://docs.rs/test-case/
85//! [Property testing]: https://docs.rs/proptest/
86//! [`quickcheck`]: https://docs.rs/quickcheck/
87//! [`rstest`]: https://crates.io/crates/rstest
88//! [`nextest`]: https://nexte.st/
89//!
90//! # Crate features
91//!
92//! ## `nightly`
93//!
94//! *(Off by default)*
95//!
96//! Uses [custom test frameworks] APIs together with a generous spicing of hacks
97//! to include arguments in the names of the generated tests (see an excerpt above
98//! for an illustration). `test_casing` actually does not require a custom test runner,
99//! but rather hacks into the standard one; thus, the generated test cases can run alongside with
100//! ordinary / non-parameterized tests.
101//!
102//! Requires a nightly Rust toolchain and specifying `#![feature(test, custom_test_frameworks)]`
103//! in the using crate. Because `custom_test_frameworks` APIs may change between toolchain releases,
104//! the feature may break. See [the CI config] for the nightly toolchain version the crate
105//! is tested against.
106//!
107//! ## `tracing`
108//!
109//! *(Off by default)*
110//!
111//! Enables tracing / logging using [the `tracing` library](https://docs.rs/tracing/). This includes
112//! the [`Trace`](decorators::Trace) decorator.
113//!
114//! [custom test frameworks]: https://github.com/rust-lang/rust/issues/50297
115//! [the CI config]: https://github.com/slowli/test-casing/blob/main/.github/workflows/ci.yml
116
117#![cfg_attr(feature = "nightly", allow(unused_features))] // `custom_test_frameworks` is unused in newer nightly toolchains
118#![cfg_attr(feature = "nightly", feature(custom_test_frameworks, test))]
119// Documentation settings
120#![cfg_attr(docsrs, feature(doc_cfg))]
121#![doc(html_root_url = "https://docs.rs/test-casing/0.2.0-beta.1")]
122
123/// Wraps a tested function to add retries, timeouts etc.
124///
125/// # Inputs
126///
127/// This attribute must be placed on a test function (i.e., one decorated with `#[test]`,
128/// `#[tokio::test]`, etc.). The attribute must be invoked with a comma-separated list
129/// of one or more [test decorators](decorators::DecorateTest). Each decorator must
130/// be a constant expression (i.e., it should be usable as a definition of a `static` variable).
131///
132/// # Examples
133///
134/// ## Basic usage
135///
136/// ```no_run
137/// use test_casing::{decorate, decorators::Timeout};
138///
139/// #[test]
140/// #[decorate(Timeout::secs(1))]
141/// fn test_with_timeout() {
142///     // test logic
143/// }
144/// ```
145///
146/// ## Tests returning `Result`s
147///
148/// Decorators can be used on tests returning `Result`s, too:
149///
150/// ```no_run
151/// use test_casing::{decorate, decorators::{Retry, Timeout}};
152/// use std::error::Error;
153///
154/// #[test]
155/// #[decorate(Timeout::millis(200), Retry::times(2))]
156/// // ^ Decorators are applied in the order of their mention. In this case,
157/// // if the test times out, errors or panics, it will be retried up to 2 times.
158/// fn test_with_retries() -> Result<(), Box<dyn Error + Send>> {
159///     // test logic
160/// #   Ok(())
161/// }
162/// ```
163///
164/// ## Multiple `decorate` attributes
165///
166/// Multiple `decorate` attributes are allowed. Thus, the test above is equivalent to
167///
168/// ```no_run
169/// # use test_casing::{decorate, decorators::{Retry, Timeout}};
170/// # use std::error::Error;
171/// #[test]
172/// #[decorate(Timeout::millis(200))]
173/// #[decorate(Retry::times(2))]
174/// fn test_with_retries() -> Result<(), Box<dyn Error + Send>> {
175///     // test logic
176/// #   Ok(())
177/// }
178/// ```
179///
180/// ## Async tests
181///
182/// Decorators work on async tests as well, as long as the `decorate` macro is applied after
183/// the test macro:
184///
185/// ```no_run
186/// # use test_casing::{decorate, decorators::Retry};
187/// #[tokio::test]
188/// #[decorate(Retry::times(3))]
189/// async fn async_test() {
190///     // test logic
191/// }
192/// ```
193///
194/// ## Composability and reuse
195///
196/// Decorators can be extracted to a `const`ant or a `static` for readability, composability
197/// and/or reuse:
198///
199/// ```no_run
200/// # use test_casing::{decorate, decorators::*};
201/// # use std::time::Duration;
202/// const RETRY: RetryErrors<String> = Retry::times(2)
203///     .with_delay(Duration::from_secs(1))
204///     .on_error(|s| s.contains("oops"));
205///
206/// static SEQUENCE: Sequence = Sequence::new().abort_on_failure();
207///
208/// #[test]
209/// #[decorate(RETRY, &SEQUENCE)]
210/// fn test_with_error_retries() -> Result<(), String> {
211///     // test logic
212/// #   Ok(())
213/// }
214///
215/// #[test]
216/// #[decorate(&SEQUENCE)]
217/// fn other_test() {
218///     // test logic
219/// }
220/// ```
221///
222/// ## Use with `test_casing`
223///
224/// When used together with the [`test_casing`](macro@test_casing) macro, the decorators will apply
225/// to each generated case.
226///
227/// ```
228/// use test_casing::{decorate, test_casing, decorators::Timeout};
229///
230/// #[test_casing(3, [3, 5, 42])]
231/// #[decorate(Timeout::secs(1))]
232/// fn parameterized_test_with_timeout(input: u64) {
233///     // test logic
234/// }
235/// ```
236pub use test_casing_macro::decorate;
237/// Flattens a parameterized test into a collection of test cases.
238///
239/// # Inputs
240///
241/// This attribute must be placed on a freestanding function with 1..8 arguments.
242/// The attribute must be invoked with 2 values:
243///
244/// 1. Number of test cases, a number literal
245/// 2. A *case iterator* expression evaluating to an implementation of [`IntoIterator`]
246///    with [`Debug`]gable, `'static` items.
247///    If the target function has a single argument, the iterator item type must equal to
248///    the argument type. Otherwise, the iterator must return a tuple in which each item
249///    corresponds to the argument with the same index.
250///
251/// A case iterator expression may reference the environment (e.g., it can be a name of a constant).
252/// It doesn't need to be a constant expression (e.g., it may allocate in heap). It should
253/// return at least the number of items specified as the first attribute argument, and can
254/// return more items; these additional items will not be tested.
255///
256/// [`Debug`]: core::fmt::Debug
257///
258/// # Mapping arguments
259///
260/// To support more idiomatic signatures for parameterized test functions, it is possible
261/// to *map* from the type returned by the case iterator. The only supported kind of mapping
262/// so far is taking a shared reference (i.e., `T` → `&T`). The mapping is enabled by placing
263/// the `#[map(ref)]` attribute on the corresponding argument. Optionally, the reference `&T`
264/// can be further mapped with a function / method (e.g., `&String` → `&str` with
265/// [`String::as_str()`]). This is specified as `#[map(ref = path::to::method)]`, a la
266/// `serde` transforms.
267///
268/// # Examples
269///
270/// ## Basic usage
271///
272/// `test_casing` macro accepts 2 args: number of cases and the iterator expression.
273/// The latter can be any valid Rust expression.
274///
275/// ```
276/// # use test_casing::test_casing;
277/// #[test_casing(5, 0..5)]
278/// // #[test] attribute is optional and is added automatically
279/// // provided that the test function is not `async`.
280/// fn number_is_small(number: i32) {
281///     assert!(number < 10);
282/// }
283/// ```
284///
285/// Functions returning `Result`s are supported as well.
286///
287/// ```
288/// # use test_casing::test_casing;
289/// use std::error::Error;
290///
291/// #[test_casing(3, ["0", "42", "-3"])]
292/// fn parsing_numbers(s: &str) -> Result<(), Box<dyn Error>> {
293///     let number: i32 = s.parse()?;
294///     assert!(number.abs() < 100);
295///     Ok(())
296/// }
297/// ```
298///
299/// The function on which the `test_casing` attribute is placed can be accessed from other code
300/// (e.g., for more tests):
301///
302/// ```no_run
303/// # use test_casing::test_casing;
304/// # use std::error::Error;
305/// #[test_casing(3, ["0", "42", "-3"])]
306/// fn parsing_numbers(s: &str) -> Result<(), Box<dyn Error>> {
307///     // snipped...
308/// #   Ok(())
309/// }
310///
311/// #[test]
312/// fn parsing_number_error() {
313///     assert!(parsing_numbers("?").is_err());
314/// }
315/// ```
316///
317/// The number of test cases is asserted in a separate generated test. This ensures that no cases
318/// are skipped by "underreporting" this count (e.g., if the test cases expression is extended).
319///
320/// ## Case expressions
321///
322/// Case expressions can be extracted to a constant for reuse or better code structuring.
323///
324/// ```
325/// # use test_casing::{cases, test_casing, TestCases};
326/// const CASES: TestCases<(String, i32)> = cases! {
327///     [0, 42, -3].map(|i| (i.to_string(), i))
328/// };
329///
330/// #[test_casing(3, CASES)]
331/// fn parsing_numbers(s: String, expected: i32) {
332///     let parsed: i32 = s.parse().unwrap();
333///     assert_eq!(parsed, expected);
334/// }
335/// ```
336///
337/// This example also shows that semantics of args is up to the writer; some of the args may be
338/// expected values, etc.
339///
340/// ## Cartesian product
341///
342/// One of possible case expressions is a [`Product`]; it can be used to generate test cases
343/// as a Cartesian product of the expressions for separate args.
344///
345/// ```
346/// # use test_casing::{test_casing, Product};
347/// #[test_casing(6, Product((0_usize..3, ["foo", "bar"])))]
348/// fn numbers_and_strings(number: usize, s: &str) {
349///     assert!(s.len() <= number);
350/// }
351/// ```
352///
353/// ## Reference args
354///
355/// It is possible to go from a generated argument to its reference by adding
356/// a `#[map(ref)]` attribute on the argument. The attribute may optionally specify
357/// a path to the transform function from the reference to the desired type
358/// (similar to transform specifications in the [`serde`](https://docs.rs/serde/) attr).
359///
360/// ```
361/// # use test_casing::{cases, test_casing, TestCases};
362/// const CASES: TestCases<(String, i32)> = cases! {
363///     [0, 42, -3].map(|i| (i.to_string(), i))
364/// };
365///
366/// #[test_casing(3, CASES)]
367/// fn parsing_numbers(#[map(ref)] s: &str, expected: i32) {
368///     // Snipped...
369/// }
370///
371/// #[test_casing(3, CASES)]
372/// fn parsing_numbers_too(
373///     #[map(ref = String::as_str)] s: &str,
374///     expected: i32,
375/// ) {
376///     // Snipped...
377/// }
378/// ```
379///
380/// ## `ignore` and `should_panic` attributes
381///
382/// `ignore` or `should_panic` attributes can be specified below the `test_casing` attribute.
383/// They will apply to all generated tests.
384///
385/// ```
386/// # use test_casing::test_casing;
387/// #[test_casing(3, ["not", "implemented", "yet"])]
388/// #[ignore = "Promise this will work sometime"]
389/// fn future_test(s: &str) {
390///     unimplemented!()
391/// }
392///
393/// #[test_casing(3, ["not a number", "-", ""])]
394/// #[should_panic(expected = "ParseIntError")]
395/// fn string_conversion_fail(bogus_str: &str) {
396///     bogus_str.parse::<i32>().unwrap();
397/// }
398/// ```
399///
400/// ## Async tests
401///
402/// `test_casing` supports all kinds of async test wrappers, such as `async_std::test`,
403/// `tokio::test`, `actix::test` etc. The corresponding attribute just needs to be specified
404/// *below* the `test_casing` attribute.
405///
406/// ```
407/// # use test_casing::test_casing;
408/// # use std::error::Error;
409/// #[test_casing(3, ["0", "42", "-3"])]
410/// #[async_std::test]
411/// async fn parsing_numbers(s: &str) -> Result<(), Box<dyn Error>> {
412///     assert!(s.parse::<i32>()?.abs() < 100);
413///     Ok(())
414/// }
415/// ```
416pub use test_casing_macro::test_casing;
417
418#[cfg(any(feature = "nightly", not(feature = "tracing")))]
419mod arg_names;
420pub mod decorators;
421#[cfg(feature = "nightly")]
422#[doc(hidden)] // used by the `#[test_casing]` macro; logically private
423pub mod nightly;
424mod test_casing;
425
426pub use crate::test_casing::{Product, ProductIter, TestCases};
427
428#[doc(hidden)] // only should be used by proc macros
429pub mod _private {
430    #[cfg(feature = "tracing")]
431    pub use tracing::{info, info_span};
432
433    #[cfg(not(feature = "tracing"))]
434    pub use crate::arg_names::ArgNames;
435    pub use crate::test_casing::{assert_case_count, case};
436
437    #[cfg(not(feature = "tracing"))]
438    #[macro_export]
439    #[doc(hidden)] // Used in the proc macros; logically private
440    macro_rules! __describe_test_case {
441        ($name:tt, $index:tt, $arg:tt = $val:expr,) => {
442            ::std::println!(
443                "Testing case #{}: {}",
444                $index,
445                $crate::_private::ArgNames::print_with_args([$arg], $val)
446            );
447        };
448        ($name:tt, $index:tt, $($arg:tt = $val:expr,)+) => {
449            ::std::println!(
450                "Testing case #{}: {}",
451                $index,
452                $crate::_private::ArgNames::print_with_args([$($arg,)+], ($(&$val,)+))
453            );
454        };
455    }
456
457    /// Strips the repeating module name at the end of `path`.
458    #[cfg(feature = "tracing")]
459    pub const fn parent_module(path: &'static str, test_name: &'static str) -> &'static str {
460        const fn str_eq(x: &str, y: &str) -> bool {
461            if x.len() != y.len() {
462                return false;
463            }
464
465            let x = x.as_bytes();
466            let y = y.as_bytes();
467            let mut i = 0;
468            while i < x.len() {
469                if x[i] != y[i] {
470                    return false;
471                }
472                i += 1;
473            }
474
475            true
476        }
477
478        let Some((head, test_module)) = split_last(path) else {
479            return path;
480        };
481
482        if str_eq(test_module, test_name) {
483            head
484        } else {
485            path
486        }
487    }
488
489    #[cfg(feature = "tracing")]
490    const fn split_last(path: &str) -> Option<(&str, &str)> {
491        let path_bytes = path.as_bytes();
492        let mut i = path_bytes.len().checked_sub(1);
493        let mut found = None;
494        while let Some(pos) = i {
495            if path_bytes[pos] == b':' && path_bytes[pos + 1] == b':' {
496                found = Some(pos);
497                break;
498            }
499            i = pos.checked_sub(1);
500        }
501
502        if let Some(pos) = found {
503            let (head_bytes, tail_bytes) = path_bytes.split_at(pos);
504            let (_, tail_bytes) = tail_bytes.split_at(2); // remove `::` at the start
505            Some(unsafe {
506                // SAFETY: `path_bytes` is a valid UTF-8 string, and we split it at a char boundary.
507                (
508                    core::str::from_utf8_unchecked(head_bytes),
509                    core::str::from_utf8_unchecked(tail_bytes),
510                )
511            })
512        } else {
513            None
514        }
515    }
516
517    #[cfg(feature = "tracing")]
518    #[macro_export]
519    #[doc(hidden)] // Used in the proc macros; logically private
520    macro_rules! __describe_test_case {
521        ($name:tt, $index:tt, $($arg:tt = $val:expr,)+) => {{
522            const TARGET: &'static str = $crate::_private::parent_module(::core::module_path!(), ::core::stringify!($name));
523
524            let _guard = $crate::_private::info_span!(
525                target: TARGET,
526                ::core::stringify!($name),
527                case.index = $index,
528                $($arg = ?$val,)+
529            )
530            .entered();
531
532            $crate::_private::info!(target: TARGET, "started test");
533            _guard
534        }}
535    }
536}