1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
// Copyright (C) 2019-2023 Daniel Mueller <deso@posteo.net>
// SPDX-License-Identifier: (Apache-2.0 OR MIT)
#![deny(missing_docs)]
//! A crate providing a replacement #[[macro@test]] attribute that
//! initializes logging and/or tracing infrastructure before running
//! tests.
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as Tokens;
use quote::quote;
use syn::parse_macro_input;
use syn::ItemFn;
/// A procedural macro for the `test` attribute.
///
/// The attribute can be used to define a test that has the `env_logger`
/// and/or `tracing` crates initialized (depending on the features used).
///
/// # Example
///
/// Specify the attribute on a per-test basis:
/// ```rust
/// # // doctests seemingly run in a slightly different environment where
/// # // `super`, which is what our macro makes use of, is not available.
/// # // By having a fake module here we work around that problem.
/// # #[cfg(feature = "log")]
/// # mod fordoctest {
/// # use logging::info;
/// # // Note that no test would actually run, regardless of `no_run`,
/// # // because we do not invoke the function.
/// #[test_log::test]
/// fn it_works() {
/// info!("Checking whether it still works...");
/// assert_eq!(2 + 2, 4);
/// info!("Looks good!");
/// }
/// # }
/// ```
///
/// It can be very convenient to convert over all tests by overriding
/// the `#[test]` attribute on a per-module basis:
/// ```rust,no_run
/// # mod fordoctest {
/// use test_log::test;
///
/// #[test]
/// fn it_still_works() {
/// // ...
/// }
/// # }
/// ```
///
/// You can also wrap another attribute. For example, suppose you use
/// [`#[tokio::test]`](https://docs.rs/tokio/1.4.0/tokio/attr.test.html)
/// to run async tests:
/// ```
/// # mod fordoctest {
/// use test_log::test;
///
/// #[test(tokio::test)]
/// async fn it_still_works() {
/// // ...
/// }
/// # }
/// ```
#[proc_macro_attribute]
pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
let inner_test = if attr.is_empty() {
quote! { ::core::prelude::v1::test }
} else {
attr.into()
};
let input = parse_macro_input!(item as ItemFn);
let ItemFn {
attrs,
vis,
sig,
block,
} = input;
let logging_init = expand_logging_init();
let tracing_init = expand_tracing_init();
let result = quote! {
#[#inner_test]
#(#attrs)*
#vis #sig {
// We put all initialization code into a separate module here in
// order to prevent potential ambiguities that could result in
// compilation errors. E.g., client code could use traits that
// could have methods that interfere with ones we use as part of
// initialization; with a `Foo` trait that is implemented for T
// and that contains a `map` (or similarly common named) method
// that could cause an ambiguity with `Iterator::map`, for
// example.
// The alternative would be to use fully qualified call syntax in
// all initialization code, but that's much harder to control.
mod init {
pub fn init() {
#logging_init
#tracing_init
}
}
init::init();
#block
}
};
result.into()
}
/// Expand the initialization code for the `log` crate.
fn expand_logging_init() -> Tokens {
#[cfg(feature = "log")]
quote! {
{
let _ = ::env_logger::builder().is_test(true).try_init();
}
}
#[cfg(not(feature = "log"))]
quote! {}
}
/// Expand the initialization code for the `tracing` crate.
fn expand_tracing_init() -> Tokens {
#[cfg(feature = "trace")]
quote! {
{
let __internal_event_filter = {
use ::tracing_subscriber::fmt::format::FmtSpan;
match ::std::env::var_os("RUST_LOG_SPAN_EVENTS") {
Some(mut value) => {
value.make_ascii_lowercase();
let value = value.to_str().expect("test-log: RUST_LOG_SPAN_EVENTS must be valid UTF-8");
value
.split(",")
.map(|filter| match filter.trim() {
"new" => FmtSpan::NEW,
"enter" => FmtSpan::ENTER,
"exit" => FmtSpan::EXIT,
"close" => FmtSpan::CLOSE,
"active" => FmtSpan::ACTIVE,
"full" => FmtSpan::FULL,
_ => panic!("test-log: RUST_LOG_SPAN_EVENTS must contain filters separated by `,`.\n\t\
For example: `active` or `new,close`\n\t\
Supported filters: new, enter, exit, close, active, full\n\t\
Got: {}", value),
})
.fold(FmtSpan::NONE, |acc, filter| filter | acc)
},
None => FmtSpan::NONE,
}
};
let _ = ::tracing_subscriber::FmtSubscriber::builder()
.with_env_filter(::tracing_subscriber::EnvFilter::from_default_env())
.with_span_events(__internal_event_filter)
.with_test_writer()
.try_init();
}
}
#[cfg(not(feature = "trace"))]
quote! {}
}