Skip to content

Commit

Permalink
Add #[test_log(default_log_filter = "___")]
Browse files Browse the repository at this point in the history
Users can now specify a default_log_filter via #[test_log(default_log_filter = "foo")]
which will be used when RUST_LOG is not specified.

Please note that because env_logger is initialized globally, it is
possible that this value will be ignored if the logger is already
initialized.

Fixes: d-e-s-o#25
  • Loading branch information
DarrenTsung committed Oct 23, 2023
1 parent f979a7d commit 0f0aae0
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 11 deletions.
11 changes: 11 additions & 0 deletions examples/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! Use this file for iterating on the derive code. You can view the expanded code for any
//! given configuration by updating this file and running:
//!
//! cargo expand --tests --example main

#[test_log::test]
fn works() {
assert_eq!(2 + 2, 4);
}

fn main() {}
98 changes: 87 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ use proc_macro2::TokenStream as Tokens;

use quote::quote;

use syn::parse_macro_input;
use syn::ItemFn;

use syn::{parse::Parse, parse_macro_input, Attribute, Expr, ItemFn, Lit, Meta};

/// A procedural macro for the `test` attribute.
///
Expand Down Expand Up @@ -72,12 +70,19 @@ use syn::ItemFn;
/// ```
#[proc_macro_attribute]
pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
let item = parse_macro_input!(item as ItemFn);
try_test(attr, item)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}

fn try_test(attr: TokenStream, input: ItemFn) -> syn::Result<Tokens> {
let inner_test = if attr.is_empty() {
quote! { ::core::prelude::v1::test }
} else {
attr.into()
};
let input = parse_macro_input!(item as ItemFn);
let mut attribute_args = AttributeArgs::default();

let ItemFn {
attrs,
Expand All @@ -86,12 +91,21 @@ pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
block,
} = input;

let logging_init = expand_logging_init();
let mut non_test_log_attrs = vec![];
for attr in attrs {
let matched = populate_attribute_args(&mut attribute_args, &attr)?;
// Keep only attrs that didn't match the #[test_log(_)] syntax.
if !matched {
non_test_log_attrs.push(attr);
}
}

let logging_init = expand_logging_init(&attribute_args);
let tracing_init = expand_tracing_init();

let result = quote! {
#[#inner_test]
#(#attrs)*
#(#non_test_log_attrs)*
#vis #sig {
// We put all initialization code into a separate module here in
// order to prevent potential ambiguities that could result in
Expand All @@ -115,23 +129,29 @@ pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
#block
}
};
result.into()
Ok(result)
}


/// Expand the initialization code for the `log` crate.
fn expand_logging_init() -> Tokens {
fn expand_logging_init(attribute_args: &AttributeArgs) -> Tokens {
let add_default_log_filter = if let Some(default_log_filter) = &attribute_args.default_log_filter
{
quote! { let env_logger_builder = env_logger_builder.parse_env(env_logger::Env::default().default_filter_or(#default_log_filter)); }
} else {
quote! {}
};
#[cfg(feature = "log")]
quote! {
{
let _ = ::env_logger::builder().is_test(true).try_init();
let mut env_logger_builder = ::env_logger::builder();
#add_default_log_filter
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")]
Expand Down Expand Up @@ -174,3 +194,59 @@ fn expand_tracing_init() -> Tokens {
#[cfg(not(feature = "trace"))]
quote! {}
}

#[derive(Debug, Default)]
struct AttributeArgs {
default_log_filter: Option<String>,
}

fn populate_attribute_args(
attribute_args: &mut AttributeArgs,
attr: &Attribute,
) -> syn::Result<bool> {
if !attr.path().is_ident("test_log") {
return Ok(false);
}

let nested_meta = attr.parse_args_with(Meta::parse)?;
let Meta::NameValue(name_value) = nested_meta else {
return Err(syn::Error::new_spanned(
&nested_meta,
"Expected NameValue syntax, e.g. 'default_log_filter = \"debug\"'.",
));
};

let Some(ident) = name_value.path.get_ident() else {
return Err(syn::Error::new_spanned(
&name_value.path,
"Expected NameValue syntax, e.g. 'default_log_filter = \"debug\"'.",
));
};

let arg_ref = match ident.to_string().as_ref() {
"default_log_filter" => &mut attribute_args.default_log_filter,
_ => {
return Err(syn::Error::new_spanned(
&name_value.path,
"Unrecognized attribute, see documentation for details.",
));
},
};

if let Expr::Lit(lit) = &name_value.value {
if let Lit::Str(lit_str) = &lit.lit {
*arg_ref = Some(lit_str.value());
}
}

// If we couldn't parse the value on the right-hand side because it was some
// unexpected type, e.g. #[test_log::log(default_log_filter=10)], return an error.
if arg_ref.is_none() {
return Err(syn::Error::new_spanned(
&name_value.value,
"Failed to parse value, expected a string!",
));
}

Ok(true)
}
12 changes: 12 additions & 0 deletions tests/default_log_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! This test needs to be defined in a separate file because it depends on global state
//! (logger) and can't be run in parallel / in the same process with other tests to avoid
//! flakiness (other tests initializing the env_logger first).

#[test_log::test(tokio::test)]
#[test_log(default_log_filter = "debug")]
async fn with_inner_test_attribute_and_default_log_filter_defined() {
// Check that RUST_LOG isn't set, because that could affect the outcome of this
// test since we're checking that we fallback to "debug" if no RUST_LOG is set.
assert!(std::env::var(env_logger::DEFAULT_FILTER_ENV).is_err());
assert!(logging::log_enabled!(logging::Level::Debug));
}

0 comments on commit 0f0aae0

Please sign in to comment.