test_env_log/
lib.rs

1// Copyright (C) 2019-2021 Daniel Mueller <deso@posteo.net>
2// SPDX-License-Identifier: (Apache-2.0 OR MIT)
3
4extern crate proc_macro;
5
6use proc_macro::TokenStream;
7use proc_macro2::TokenStream as Tokens;
8
9use quote::quote;
10
11use syn::AttributeArgs;
12use syn::ItemFn;
13use syn::Meta;
14use syn::NestedMeta;
15use syn::parse_macro_input;
16use syn::parse_quote;
17use syn::Path;
18use syn::ReturnType;
19
20
21/// A procedural macro for the `test` attribute.
22///
23/// The attribute can be used to define a test that has the `env_logger`
24/// and/or `tracing` crates initialized (depending on the features used).
25///
26/// # Example
27///
28/// Specify the attribute on a per-test basis:
29/// ```rust
30/// # // doctests seemingly run in a slightly different environment where
31/// # // `super`, which is what our macro makes use of, is not available.
32/// # // By having a fake module here we work around that problem.
33/// # #[cfg(feature = "log")]
34/// # mod fordoctest {
35/// # use logging::info;
36/// # // Note that no test would actually run, regardless of `no_run`,
37/// # // because we do not invoke the function.
38/// #[test_env_log::test]
39/// fn it_works() {
40///   info!("Checking whether it still works...");
41///   assert_eq!(2 + 2, 4);
42///   info!("Looks good!");
43/// }
44/// # }
45/// ```
46///
47/// It can be very convenient to convert over all tests by overriding
48/// the `#[test]` attribute on a per-module basis:
49/// ```rust,no_run
50/// # mod fordoctest {
51/// use test_env_log::test;
52///
53/// #[test]
54/// fn it_still_works() {
55///   // ...
56/// }
57/// # }
58/// ```
59///
60/// You can also wrap another attribute. For example, suppose you use
61/// [`#[tokio::test]`](https://docs.rs/tokio/1.4.0/tokio/attr.test.html)
62/// to run async tests:
63/// ```
64/// # mod fordoctest {
65/// use test_env_log::test;
66///
67/// #[test(tokio::test)]
68/// async fn it_still_works() {
69///   // ...
70/// }
71/// # }
72/// ```
73#[deprecated(
74  since = "0.2.8",
75  note = "test-env-log has been renamed to test-log; use it instead"
76)]
77#[proc_macro_attribute]
78pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
79  let args = parse_macro_input!(attr as AttributeArgs);
80  let input = parse_macro_input!(item as ItemFn);
81
82  let inner_test = match args.as_slice() {
83    [] => parse_quote! { ::core::prelude::v1::test },
84    [NestedMeta::Meta(Meta::Path(path))] => path.clone(),
85    _ => panic!("unsupported attributes supplied: {}", quote! { args }),
86  };
87
88  expand_wrapper(&inner_test, &input)
89}
90
91
92/// Expand the initialization code for the `log` crate.
93fn expand_logging_init() -> Tokens {
94  #[cfg(feature = "log")]
95  quote! {
96    {
97      let _ = ::env_logger::builder().is_test(true).try_init();
98    }
99  }
100  #[cfg(not(feature = "log"))]
101  quote! {}
102}
103
104
105/// Expand the initialization code for the `tracing` crate.
106fn expand_tracing_init() -> Tokens {
107  #[cfg(feature = "trace")]
108  quote! {
109    {
110      let __internal_event_filter = {
111        use ::tracing_subscriber::fmt::format::FmtSpan;
112
113        match ::std::env::var("RUST_LOG_SPAN_EVENTS") {
114          Ok(value) => {
115            value
116              .to_ascii_lowercase()
117              .split(",")
118              .map(|filter| match filter.trim() {
119                "new" => FmtSpan::NEW,
120                "enter" => FmtSpan::ENTER,
121                "exit" => FmtSpan::EXIT,
122                "close" => FmtSpan::CLOSE,
123                "active" => FmtSpan::ACTIVE,
124                "full" => FmtSpan::FULL,
125                _ => panic!("test-env-log: RUST_LOG_SPAN_EVENTS must contain filters separated by `,`.\n\t\
126                  For example: `active` or `new,close`\n\t\
127                  Supported filters: new, enter, exit, close, active, full\n\t\
128                  Got: {}", value),
129              })
130              .fold(FmtSpan::NONE, |acc, filter| filter | acc)
131          },
132          Err(::std::env::VarError::NotUnicode(_)) =>
133            panic!("test-env-log: RUST_LOG_SPAN_EVENTS must contain a valid UTF-8 string"),
134          Err(::std::env::VarError::NotPresent) => FmtSpan::NONE,
135        }
136      };
137
138      let subscriber = ::tracing_subscriber::FmtSubscriber::builder()
139        .with_env_filter(::tracing_subscriber::EnvFilter::from_default_env())
140        .with_span_events(__internal_event_filter)
141        .with_test_writer()
142        .finish();
143      let _ = ::tracing::subscriber::set_global_default(subscriber);
144    }
145  }
146  #[cfg(not(feature = "trace"))]
147  quote! {}
148}
149
150
151/// Emit code for a wrapper function around a test function.
152fn expand_wrapper(inner_test: &Path, wrappee: &ItemFn) -> TokenStream {
153  let attrs = &wrappee.attrs;
154  let async_ = &wrappee.sig.asyncness;
155  let await_ = if async_.is_some() {
156    quote! {.await}
157  } else {
158    quote! {}
159  };
160  let body = &wrappee.block;
161  let test_name = &wrappee.sig.ident;
162
163  // Note that Rust does not allow us to have a test function with
164  // #[should_panic] that has a non-unit return value.
165  let ret = match &wrappee.sig.output {
166    ReturnType::Default => quote! {},
167    ReturnType::Type(_, type_) => quote! {-> #type_},
168  };
169
170  let logging_init = expand_logging_init();
171  let tracing_init = expand_tracing_init();
172
173  let result = quote! {
174    #[#inner_test]
175    #(#attrs)*
176    #async_ fn #test_name() #ret {
177      #async_ fn test_impl() #ret {
178        #body
179      }
180
181      #logging_init
182      #tracing_init
183
184      test_impl()#await_
185    }
186  };
187  result.into()
188}