generic_tests/macro.rs
1//! Support for generic test definitions with a procedural attribute macro.
2//!
3//! The `define` macro provided by this crate allows the test writer to
4//! reuse code between test cases or benchmarks that use the same test protocol
5//! with different types or constant values supplied to specific tests.
6//! As in general programming with Rust, this is achieved by using generic
7//! parameters and trait bounds. A module processed by the `define` macro
8//! contains generic test functions that are annotated with attributes consumed
9//! by the test framework, such as `test` or `bench`.
10//! The actual test cases can be instantiated in multiple submodules
11//! annotated with the `instantiate_tests` attribute providing specific
12//! argument types for the tests.
13
14#![warn(clippy::all)]
15#![warn(future_incompatible)]
16#![warn(missing_docs)]
17
18mod error;
19mod expand;
20mod extract;
21mod options;
22mod signature;
23
24use options::ParsedMacroOpts;
25use proc_macro::TokenStream;
26use syn::parse_macro_input;
27use syn::{meta, ItemMod};
28
29/// Populates a module tree with test cases parameterizing generic definitions.
30///
31/// This macro is used to annotate a module containing test case definitions.
32/// All functions defined directly in the module and marked with
33/// a [test attribute][test-attributes] must have the same number and order
34/// of generic type parameters.
35///
36/// Empty submodules defined inline at any depth under the module on which
37/// the macro is invoked can be annotated with the `instantiate_tests`
38/// attribute. The macro populates these submodules with functions having names,
39/// signatures, and test attributes mirroring the generic test functions at the
40/// macro invocation's root module. Each of the instantiated functions calls
41/// its generic namesake in the root module, parameterized with the arguments
42/// given in `instantiate_tests`.
43///
44/// # Basic example
45///
46/// ```
47/// #[generic_tests::define]
48/// mod tests {
49/// use std::borrow::Cow;
50/// use std::fmt::Display;
51///
52/// #[test]
53/// fn print<S>()
54/// where
55/// S: From<&'static str> + Display,
56/// {
57/// let s = S::from("Hello, world!");
58/// println!("{}", s);
59/// }
60///
61/// #[instantiate_tests(<String>)]
62/// mod string {}
63///
64/// #[instantiate_tests(<&'static str>)]
65/// mod str_slice {}
66///
67/// #[instantiate_tests(<Cow<'static, str>>)]
68/// mod cow {}
69/// }
70/// # fn main() {}
71/// ```
72///
73/// # Test attributes
74///
75/// [test-attributes]: #test-attributes
76///
77/// The macro checks attributes of the function items directly contained
78/// by the module against a customizable set of attribute paths that annotate
79/// test cases. Functions with at least one of the attributes found in this set
80/// are selected for instantiation.
81/// These attributes are replicated to the instantiated test case functions and
82/// erased from the original generic definitions. By default, the
83/// `test`, `bench`, `ignore`, and `should_panic` attributes get this
84/// treatment. To recognize other test attributes, their paths can be
85/// listed in the `attrs()` parameter of the `define` attribute. Use of the
86/// `attrs()` parameter overrides the default set.
87///
88/// ```
89/// #[generic_tests::define(attrs(tokio::test))]
90/// mod async_tests {
91/// use bytes::{Buf, Bytes};
92/// use tokio::io::{self, AsyncWriteExt};
93///
94/// #[tokio::test]
95/// async fn test_write_buf<T: Buf>() -> io::Result<()>
96/// where
97/// T: From<&'static str>,
98/// {
99/// let mut buf = T::from("Hello, world!");
100/// io::sink().write_buf(&mut buf).await?;
101/// Ok(())
102/// }
103///
104/// #[instantiate_tests(<Vec<u8>>)]
105/// mod test_vec {}
106///
107/// #[instantiate_tests(<Bytes>)]
108/// mod test_bytes {}
109/// }
110/// # fn main() {}
111/// ```
112///
113/// The `copy_attrs()` list parameter can be used to specify item attributes
114/// that are both copied to the instantiated test case functions and preserved
115/// on the generic functions. By default, this set consists of `cfg`,
116/// enabling consistent conditional compilation.
117///
118/// ```
119/// # struct Foo;
120/// #
121/// #[generic_tests::define(copy_attrs(cfg, cfg_attr))]
122/// mod tests {
123/// use super::Foo;
124///
125/// #[test]
126/// #[cfg(windows)]
127/// fn test_only_on_windows<T>() {
128/// // ...
129/// }
130///
131/// #[test]
132/// #[cfg_attr(feature = "my-fn-enhancer", bells_and_whistles)]
133/// fn test_with_optional_bells_and_whistles<T>() {
134/// // ...
135/// }
136///
137/// #[instantiate_tests(<Foo>)]
138/// mod foo {}
139/// }
140/// # fn main() {}
141/// ```
142///
143/// The attribute sets can be customized for an individual generic test
144/// function with the `generic_test` attribute.
145///
146/// ```
147/// # struct Foo;
148/// #
149/// #[generic_tests::define]
150/// mod tests {
151/// use super::Foo;
152///
153/// #[generic_test(attrs(test, cfg_attr), copy_attrs(allow))]
154/// #[test]
155/// #[cfg_attr(windows, ignore)]
156/// #[allow(dead_code)]
157/// fn works_everywhere_except_windows<T>() {
158/// // ...
159/// }
160///
161/// #[instantiate_tests(<Foo>)]
162/// mod foo {}
163/// }
164/// # fn main() {}
165/// ```
166///
167/// Finally, all function parameter attributes on the generic test functions
168/// are always copied into the signatures of the instantiated functions.
169///
170/// # Const generics
171///
172/// Since Rust 1.51, const generic parameters can be used to parameterize test
173/// cases in addition to type parameters.
174///
175/// ```
176/// #[generic_tests::define]
177/// mod tests {
178/// use std::iter;
179///
180/// #[test]
181/// fn test_fill_vec<const LEN: usize>() {
182/// let _v: Vec<u8> = iter::repeat(0xA5).take(LEN).collect();
183/// }
184///
185/// #[instantiate_tests(<16>)]
186/// mod small {}
187///
188/// #[instantiate_tests(<65536>)]
189/// mod large {}
190/// }
191/// ```
192///
193#[proc_macro_attribute]
194pub fn define(args: TokenStream, item: TokenStream) -> TokenStream {
195 let mut opts = ParsedMacroOpts::default();
196 let opts_parser = meta::parser(|meta| opts.parse(meta));
197 parse_macro_input!(args with opts_parser);
198 let ast = parse_macro_input!(item as ItemMod);
199 expand::expand(&opts.into_effective(), ast).into()
200}