test_tag/
lib.rs

1// Copyright (C) 2024 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  result = quote! {
165    use ::core::prelude::v1::*;
166    #[allow(unused_imports)]
167    #vis use #test_name::#import::test as #test_name;
168    #[doc(hidden)]
169    pub mod #test_name {
170      use super::*;
171      #result
172    }
173  };
174  Ok(result)
175}
176
177
178/// Parse a list of tags (`tag1, tag2`).
179///
180/// This function will report an error if the list is empty.
181fn parse_tags(attrs: TokenStream) -> Result<Tags> {
182  let tags = Tags::parse_terminated.parse(attrs)?;
183  if !tags.is_empty() {
184    Ok(tags)
185  } else {
186    Err(Error::new_spanned(
187      &tags,
188      "at least one tag is required: #[test_tag::tag(<tags...>)]",
189    ))
190  }
191}
192
193
194/// Parse the list of attributes to a function.
195///
196/// In the process, this function filters out anything resembling a
197/// `tag` attribute and attempts to parsing its tags.
198fn parse_fn_attrs(attrs: Vec<Attribute>) -> Result<(Tags, Vec<Attribute>)> {
199  let mut tags = Tags::new();
200  let mut passthrough_attrs = Vec::new();
201
202  for attr in attrs {
203    if is_test_tag_attr(&attr) {
204      let tokens = match attr.meta {
205        Meta::Path(..) => {
206          // A path does not contain any tags. But leave error handling
207          // up to the `parse_tags` function for consistency.
208          quote_spanned!(attr.meta.span() => {})
209        },
210        Meta::List(list) => list.tokens,
211        Meta::NameValue(..) => {
212          return Err(Error::new_spanned(
213            &attr,
214            "encountered unexpected argument to `tag` attribute; expected list of tags",
215          ))
216        },
217      };
218
219      let attr_tags = parse_tags(tokens.into())?;
220      let () = tags.extend(attr_tags);
221    } else {
222      let () = passthrough_attrs.push(attr);
223    }
224  }
225
226  Ok((tags, passthrough_attrs))
227}
228
229
230/// Check whether given attribute is `#[tag]` or `#[test_tag::tag]`.
231fn is_test_tag_attr(attr: &Attribute) -> bool {
232  let path = match &attr.meta {
233    // We conservatively treat an attribute without arguments as a
234    // candidate as well, assuming it could just be wrong usage.
235    Meta::Path(path) => path,
236    Meta::List(list) => &list.path,
237    _ => return false,
238  };
239
240  let segments = ["test_tag", "tag"];
241  if path.leading_colon.is_none() && path.segments.len() == 1 && path.segments[0].ident == "tag" {
242    true
243  } else if path.segments.len() != segments.len() {
244    false
245  } else {
246    path
247      .segments
248      .iter()
249      .zip(segments)
250      .all(|(segment, path)| segment.ident == path)
251  }
252}
253
254
255/// Rewrite remaining `#[test]` attributes to use `#[self::test]` syntax.
256///
257/// This conversion is necessary in order to properly support custom `#[test]`
258/// attributes. These attributes are somewhat special and require custom
259/// treatment, because Rust's prelude also contains such an attribute
260/// and we run risk of ambiguities without this rewrite.
261fn rewrite_test_attrs(attrs: &mut [Attribute]) {
262  for attr in attrs.iter_mut() {
263    let span = attr.meta.span();
264    let path = match &mut attr.meta {
265      Meta::Path(path) => path,
266      Meta::List(list) => &mut list.path,
267      Meta::NameValue(MetaNameValue { path, .. }) => path,
268    };
269
270    if path.leading_colon.is_none() && path.segments.len() == 1 && path.segments[0].ident == "test"
271    {
272      let segment = PathSegment {
273        ident: Ident::new("self", span),
274        arguments: PathArguments::None,
275      };
276      let () = path.segments.insert(0, segment);
277    }
278  }
279}
280
281
282#[cfg(test)]
283mod tests {
284  use super::*;
285
286
287  /// Check that we can identify the `test_tag::tag` in different shapes
288  /// and forms.
289  #[test]
290  fn test_tag_attr_recognition() {
291    #[track_caller]
292    fn test(func: Tokens) {
293      let attrs = ItemFn::parse.parse2(func).unwrap().attrs;
294      assert!(is_test_tag_attr(&attrs[0]));
295      assert!(!is_test_tag_attr(&attrs[1]));
296    }
297
298
299    let func = quote! {
300      #[tag(xxx)]
301      #[test]
302      fn foobar() {}
303    };
304    let () = test(func);
305
306    let func = quote! {
307      #[test_tag::tag(xxx)]
308      #[test]
309      fn foobar() {}
310    };
311    let () = test(func);
312
313    let func = quote! {
314      #[::test_tag::tag(xxx)]
315      #[test]
316      fn foobar() {}
317    };
318    let () = test(func);
319
320    let func = quote! {
321      #[::test_tag::tag]
322      #[test]
323      fn foobar() {}
324    };
325    let () = test(func);
326
327    let func = quote! {
328      #[test_tag::tag]
329      #[test]
330      fn foobar() {}
331    };
332    let () = test(func);
333  }
334}