test_tag/lib.rs
1// Copyright (C) 2024-2026 Daniel Mueller <deso@posteo.net>
2// SPDX-License-Identifier: (Apache-2.0 OR MIT)
3
4//! Tagging functionality for tests, allowing for convenient grouping and later
5//! execution of certain groups.
6//!
7//! For example, a test can be associated with the tag `miri`, to
8//! indicate that it is suitable for being run under
9//! [Miri](https://github.com/rust-lang/miri):
10//! ```rust,ignore
11//! use test_tag::tag;
12//!
13//! #[tag(miri)]
14//! #[test]
15//! fn test1() {
16//! assert_eq!(2 + 2, 4);
17//! }
18//! ```
19//!
20//! Subsequently, it is possible to run only those tests under Miri:
21//! ```sh
22//! $ cargo miri test -- :miri:
23//! ```
24//!
25//! Please note that the usage of Miri is just an example (if the
26//! majority of tests is Miri-compatible you can use `#[cfg_attr(miri,
27//! ignore)]` instead and may not require a custom attribute). However,
28//! tagging can be useful for other properties, such as certain tests
29//! requiring alleviated rights (need to be run with administrator
30//! privileges).
31//!
32//! This crate provides the #[test_tag::[macro@tag]] attribute that allows for
33//! such tagging to happen.
34
35#![warn(missing_docs)]
36
37extern crate proc_macro;
38
39use proc_macro::TokenStream;
40use proc_macro2::TokenStream as Tokens;
41
42use quote::quote;
43use quote::quote_spanned;
44
45use syn::parse::Parse;
46use syn::parse::Parser as _;
47use syn::punctuated::Punctuated;
48use syn::spanned::Spanned as _;
49use syn::Attribute;
50use syn::Error;
51use syn::Ident;
52use syn::ItemFn;
53use syn::Meta;
54use syn::MetaNameValue;
55use syn::PathArguments;
56use syn::PathSegment;
57use syn::Result;
58use syn::Token;
59
60
61/// Our representation of a list of tags.
62type Tags = Punctuated<Ident, Token![,]>;
63
64
65/// A procedural macro for the `tag` attribute.
66///
67/// The attribute can be used to associate one or more tags with a test. The
68/// attribute should be placed before the eventual `#[test]` attribute.
69///
70/// # Example
71///
72/// Specify the attribute on a per-test basis:
73/// ```rust,ignore
74/// use test_tag::tag;
75///
76/// #[tag(tag1, tag2)]
77/// #[test]
78/// fn test1() {
79/// assert_eq!(2 + 2, 4);
80/// }
81/// ```
82#[proc_macro_attribute]
83pub fn tag(attrs: TokenStream, item: TokenStream) -> TokenStream {
84 try_tag(attrs, item)
85 .unwrap_or_else(Error::into_compile_error)
86 .into()
87}
88
89
90/// Handle the `#[test_tag::tag]` attribute.
91///
92/// The input to the function, for the following example:
93/// ```rust,ignore
94/// use test_tag::tag;
95///
96/// #[tag(tag1, tag2)]
97/// #[tag(tag3)]
98/// #[test]
99/// fn it_works() {
100/// assert_eq!(2 + 2, 4);
101/// }
102/// ```
103/// would be:
104/// - `attrs`: `tag1, tag2`
105/// - `item`: `#[tag(tag3)] #[test] fn it_works() { assert_eq!(2 + 2, 4); }`
106fn try_tag(attrs: TokenStream, item: TokenStream) -> Result<Tokens> {
107 // Parse the list of tags directly provided to *this* macro
108 // instantiation.
109 let mut tags = parse_tags(attrs)?;
110 let input = ItemFn::parse.parse(item)?;
111 let ItemFn {
112 attrs,
113 vis,
114 mut sig,
115 block,
116 } = input;
117
118 // Now also parse the attributes of the annotated function and filter
119 // out any additional `test_tag::tag` candidates, parsing their tags
120 // in the process.
121 let (more_tags, mut attrs) = parse_fn_attrs(attrs)?;
122 let () = tags.extend(more_tags);
123 let () = rewrite_test_attrs(&mut attrs);
124
125 let test_name = sig.ident.clone();
126 // Rename the test function to simply `test`. That's less confusing
127 // than re-using the original name, which we intend to use in the
128 // first module that we create.
129 sig.ident = Ident::new("test", sig.ident.span());
130
131 let mut result = quote! {
132 #(#attrs)*
133 pub #sig {
134 #block
135 }
136 };
137
138 let mut import = None;
139 for tag in tags.into_iter().rev() {
140 import = if let Some(import) = &import {
141 Some(quote! { #tag::#import })
142 } else {
143 Some(quote! { #tag })
144 };
145
146 result = quote! {
147 pub mod #tag {
148 use super::*;
149 #result
150 }
151 };
152 }
153
154 // Wrap everything in a module named after the test. In so doing we
155 // make sure that tags are always surrounded by `::` in the final test
156 // name that the testing infrastructure infers.
157 // NB: We need to import the standard prelude here so that some
158 // `#[test]` attribute is present. That is necessary because we
159 // rewrite #[test] attributes on tagged functions to
160 // `#[self::test]` and then rely on *a* `#[test]` attribute being
161 // in scope. We cannot, however, import `core::prelude::v1::test`
162 // directly, because that would conflict with potential user
163 // imports.
164 // TODO: We need to find an alternative solution to allowing the
165 // `ambiguous_panic_imports` lint here.
166 result = quote! {
167 use ::core::prelude::v1::*;
168 #[allow(unused_imports)]
169 #vis use #test_name::#import::test as #test_name;
170 #[doc(hidden)]
171 #[allow(ambiguous_panic_imports)]
172 pub mod #test_name {
173 use super::*;
174 #result
175 }
176 };
177 Ok(result)
178}
179
180
181/// Parse a list of tags (`tag1, tag2`).
182///
183/// This function will report an error if the list is empty.
184fn parse_tags(attrs: TokenStream) -> Result<Tags> {
185 let tags = Tags::parse_terminated.parse(attrs)?;
186 if !tags.is_empty() {
187 Ok(tags)
188 } else {
189 Err(Error::new_spanned(
190 &tags,
191 "at least one tag is required: #[test_tag::tag(<tags...>)]",
192 ))
193 }
194}
195
196
197/// Parse the list of attributes to a function.
198///
199/// In the process, this function filters out anything resembling a
200/// `tag` attribute and attempts to parsing its tags.
201fn parse_fn_attrs(attrs: Vec<Attribute>) -> Result<(Tags, Vec<Attribute>)> {
202 let mut tags = Tags::new();
203 let mut passthrough_attrs = Vec::new();
204
205 for attr in attrs {
206 if is_test_tag_attr(&attr) {
207 let tokens = match attr.meta {
208 Meta::Path(..) => {
209 // A path does not contain any tags. But leave error handling
210 // up to the `parse_tags` function for consistency.
211 quote_spanned!(attr.meta.span() => {})
212 },
213 Meta::List(list) => list.tokens,
214 Meta::NameValue(..) => {
215 return Err(Error::new_spanned(
216 &attr,
217 "encountered unexpected argument to `tag` attribute; expected list of tags",
218 ))
219 },
220 };
221
222 let attr_tags = parse_tags(tokens.into())?;
223 let () = tags.extend(attr_tags);
224 } else {
225 let () = passthrough_attrs.push(attr);
226 }
227 }
228
229 Ok((tags, passthrough_attrs))
230}
231
232
233/// Check whether given attribute is `#[tag]` or `#[test_tag::tag]`.
234fn is_test_tag_attr(attr: &Attribute) -> bool {
235 let path = match &attr.meta {
236 // We conservatively treat an attribute without arguments as a
237 // candidate as well, assuming it could just be wrong usage.
238 Meta::Path(path) => path,
239 Meta::List(list) => &list.path,
240 _ => return false,
241 };
242
243 let segments = ["test_tag", "tag"];
244 if path.leading_colon.is_none() && path.segments.len() == 1 && path.segments[0].ident == "tag" {
245 true
246 } else if path.segments.len() != segments.len() {
247 false
248 } else {
249 path
250 .segments
251 .iter()
252 .zip(segments)
253 .all(|(segment, path)| segment.ident == path)
254 }
255}
256
257
258/// Rewrite remaining `#[test]` attributes to use `#[self::test]` syntax.
259///
260/// This conversion is necessary in order to properly support custom `#[test]`
261/// attributes. These attributes are somewhat special and require custom
262/// treatment, because Rust's prelude also contains such an attribute
263/// and we run risk of ambiguities without this rewrite.
264fn rewrite_test_attrs(attrs: &mut [Attribute]) {
265 for attr in attrs.iter_mut() {
266 let span = attr.meta.span();
267 let path = match &mut attr.meta {
268 Meta::Path(path) => path,
269 Meta::List(list) => &mut list.path,
270 Meta::NameValue(MetaNameValue { path, .. }) => path,
271 };
272
273 if path.leading_colon.is_none() && path.segments.len() == 1 && path.segments[0].ident == "test"
274 {
275 let segment = PathSegment {
276 ident: Ident::new("self", span),
277 arguments: PathArguments::None,
278 };
279 let () = path.segments.insert(0, segment);
280 }
281 }
282}
283
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289
290 /// Check that we can identify the `test_tag::tag` in different shapes
291 /// and forms.
292 #[test]
293 fn test_tag_attr_recognition() {
294 #[track_caller]
295 fn test(func: Tokens) {
296 let attrs = ItemFn::parse.parse2(func).unwrap().attrs;
297 assert!(is_test_tag_attr(&attrs[0]));
298 assert!(!is_test_tag_attr(&attrs[1]));
299 }
300
301
302 let func = quote! {
303 #[tag(xxx)]
304 #[test]
305 fn foobar() {}
306 };
307 let () = test(func);
308
309 let func = quote! {
310 #[test_tag::tag(xxx)]
311 #[test]
312 fn foobar() {}
313 };
314 let () = test(func);
315
316 let func = quote! {
317 #[::test_tag::tag(xxx)]
318 #[test]
319 fn foobar() {}
320 };
321 let () = test(func);
322
323 let func = quote! {
324 #[::test_tag::tag]
325 #[test]
326 fn foobar() {}
327 };
328 let () = test(func);
329
330 let func = quote! {
331 #[test_tag::tag]
332 #[test]
333 fn foobar() {}
334 };
335 let () = test(func);
336 }
337}