Skip to main content

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}