From 17faee84ee3feb21b6c7a5b2aa30129dbae7a0d2 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Sun, 19 Nov 2023 13:07:23 -0500 Subject: [PATCH] Automatically include dependencies - Move macros to a new test-log-macros crate. - Export env_logger and tracing_subscriber from the test-log crate. - Macros emit test_log::{env_logger,tracing_subscriber} references. Removes the need for users to explicitly depend on additional crates. Fixes #36. --- Cargo.toml | 18 +-- README.md | 11 -- crates/test-log-macros/Cargo.toml | 19 +++ crates/test-log-macros/src/lib.rs | 231 +++++++++++++++++++++++++++++ src/lib.rs | 235 +----------------------------- 5 files changed, 265 insertions(+), 249 deletions(-) create mode 100644 crates/test-log-macros/Cargo.toml create mode 100644 crates/test-log-macros/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 0f28249..4528732 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,6 @@ tracing infrastructure before running tests. include = ["src/lib.rs", "LICENSE-*", "README.md", "CHANGELOG.md"] [lib] -proc-macro = true [[test]] name = "default_log_filter" @@ -36,19 +35,20 @@ required-features = ["log"] [features] default = ["log"] -trace = [] -log = [] +# TODO: use "dep:{tracing-subscriber,evn_logger}" once our MSRV is 1.60 or higher. +trace = ["tracing-subscriber", "test-log-macros/trace"] +log = ["env_logger", "test-log-macros/log"] + +[workspace] +members = ["crates/*"] [dependencies] -proc-macro2 = {version = "1.0.32", default-features = false} -quote = {version = "1.0", default-features = false} -syn = {version = "2.0", default-features = false, features = ["full", "parsing", "printing", "proc-macro"]} +test-log-macros = {path = "crates/test-log-macros"} +tracing-subscriber = {version = "0.3.17", default-features = false, optional = true, features = ["env-filter", "fmt"]} +env_logger = {version = "0.9.3", default-features = false, optional = true} [dev-dependencies] -env_logger = {version = "0.9", default-features = false} logging = {version = "0.4.8", package = "log"} test-case = {version = "3.1"} tokio = {version = "1.0", default-features = false, features = ["rt-multi-thread", "macros"]} tracing = {version = "0.1.20"} -tracing-futures = {version = "0.2", default-features = false, features = ["std-future"]} -tracing-subscriber = {version = "0.3.10", default-features = false, features = ["env-filter", "fmt", "std"]} diff --git a/README.md b/README.md index 4352e8c..8c4fbf0 100644 --- a/README.md +++ b/README.md @@ -113,17 +113,6 @@ Depending on what backend the crate-under-test (and its dependencies) use, the respective feature should be enabled to make messages that are emitted by the test manifest on the console. -Note that as a user you are required to explicitly add `env_logger` or -`tracing-subscriber` as a dependency to your project-under-test (when -enabling the `log` or `trace` feature, respectively). E.g., - -```toml -[dev-dependencies] -env_logger = "*" -tracing-subscriber = {version = "0.3.10", default-features = false, features = ["env-filter", "fmt", "std"]} -``` - - [docs-rs]: https://docs.rs/crate/test-log [env-docs-rs]: https://docs.rs/env_logger/0.9.0/env_logger [log]: https://crates.io/crates/log diff --git a/crates/test-log-macros/Cargo.toml b/crates/test-log-macros/Cargo.toml new file mode 100644 index 0000000..4bbcc5f --- /dev/null +++ b/crates/test-log-macros/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "test-log-macros" +version = "0.2.13" +authors = ["Daniel Mueller "] +edition = "2018" +rust-version = "1.56" +license = "Apache-2.0 OR MIT" + +[lib] +proc-macro = true + +[features] +trace = [] +log = [] + +[dependencies] +proc-macro2 = {version = "1.0.32", default-features = false} +quote = {version = "1.0", default-features = false} +syn = {version = "2.0", default-features = false, features = ["full", "parsing", "printing", "proc-macro"]} diff --git a/crates/test-log-macros/src/lib.rs b/crates/test-log-macros/src/lib.rs new file mode 100644 index 0000000..076ac38 --- /dev/null +++ b/crates/test-log-macros/src/lib.rs @@ -0,0 +1,231 @@ +// Copyright (C) 2019-2023 Daniel Mueller +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as Tokens; + +use quote::quote; + +use syn::parse::Parse; +use syn::parse_macro_input; +use syn::Attribute; +use syn::Expr; +use syn::ItemFn; +use syn::Lit; +use syn::Meta; + +#[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 parse_attrs(attrs: Vec) -> syn::Result<(AttributeArgs, Vec)> { + let mut attribute_args = AttributeArgs::default(); + let mut ignored_attrs = vec![]; + for attr in attrs { + let matched = attribute_args.try_parse_attr_single(&attr)?; + // Keep only attrs that didn't match the #[test_log(_)] syntax. + if !matched { + ignored_attrs.push(attr); + } + } + + Ok((attribute_args, ignored_attrs)) +} + +fn try_test(attr: TokenStream, input: ItemFn) -> syn::Result { + let inner_test = if attr.is_empty() { + quote! { ::core::prelude::v1::test } + } else { + attr.into() + }; + + let ItemFn { + attrs, + vis, + sig, + block, + } = input; + + let (attribute_args, ignored_attrs) = parse_attrs(attrs)?; + let logging_init = expand_logging_init(&attribute_args); + let tracing_init = expand_tracing_init(&attribute_args); + + let result = quote! { + #[#inner_test] + #(#ignored_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 + } + }; + Ok(result) +} + +#[derive(Debug, Default)] +struct AttributeArgs { + default_log_filter: Option, +} + +impl AttributeArgs { + fn try_parse_attr_single(&mut self, attr: &Attribute) -> syn::Result { + if !attr.path().is_ident("test_log") { + return Ok(false); + } + + let nested_meta = attr.parse_args_with(Meta::parse)?; + let name_value = if let Meta::NameValue(name_value) = nested_meta { + name_value + } else { + return Err(syn::Error::new_spanned( + &nested_meta, + "Expected NameValue syntax, e.g. 'default_log_filter = \"debug\"'.", + )); + }; + + let ident = if let Some(ident) = name_value.path.get_ident() { + ident + } else { + return Err(syn::Error::new_spanned( + &name_value.path, + "Expected NameValue syntax, e.g. 'default_log_filter = \"debug\"'.", + )); + }; + + let arg_ref = if ident == "default_log_filter" { + &mut self.default_log_filter + } else { + 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) + } +} + +/// Expand the initialization code for the `log` crate. +#[cfg(feature = "log")] +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(::test_log::env_logger::Env::default().default_filter_or(#default_log_filter)); + } + } else { + quote! {} + }; + + quote! { + { + let mut env_logger_builder = ::test_log::env_logger::builder(); + #add_default_log_filter + let _ = env_logger_builder.is_test(true).try_init(); + } + } +} + +#[cfg(not(feature = "log"))] +fn expand_logging_init(_attribute_args: &AttributeArgs) -> Tokens { + quote! {} +} + +/// Expand the initialization code for the `tracing` crate. +#[cfg(feature = "trace")] +fn expand_tracing_init(attribute_args: &AttributeArgs) -> Tokens { + let env_filter = if let Some(default_log_filter) = &attribute_args.default_log_filter { + quote! { + ::test_log::tracing_subscriber::EnvFilter::builder() + .with_default_directive( + #default_log_filter + .parse() + .expect("test-log: default_log_filter must be valid") + ) + .from_env_lossy() + } + } else { + quote! { ::test_log::tracing_subscriber::EnvFilter::from_default_env() } + }; + + quote! { + { + let __internal_event_filter = { + use ::test_log::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 _ = ::test_log::tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(#env_filter) + .with_span_events(__internal_event_filter) + .with_test_writer() + .try_init(); + } + } +} + +#[cfg(not(feature = "trace"))] +fn expand_tracing_init(_attribute_args: &AttributeArgs) -> Tokens { + quote! {} +} diff --git a/src/lib.rs b/src/lib.rs index b46c29d..59084b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,22 +7,6 @@ //! 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::Parse; -use syn::parse_macro_input; -use syn::Attribute; -use syn::Expr; -use syn::ItemFn; -use syn::Lit; -use syn::Meta; - - /// A procedural macro for the `test` attribute. /// /// The attribute can be used to define a test that has the `env_logger` @@ -75,219 +59,12 @@ use syn::Meta; /// } /// # } /// ``` -#[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 parse_attrs(attrs: Vec) -> syn::Result<(AttributeArgs, Vec)> { - let mut attribute_args = AttributeArgs::default(); - let mut ignored_attrs = vec![]; - for attr in attrs { - let matched = attribute_args.try_parse_attr_single(&attr)?; - // Keep only attrs that didn't match the #[test_log(_)] syntax. - if !matched { - ignored_attrs.push(attr); - } - } - - Ok((attribute_args, ignored_attrs)) -} - -fn try_test(attr: TokenStream, input: ItemFn) -> syn::Result { - let inner_test = if attr.is_empty() { - quote! { ::core::prelude::v1::test } - } else { - attr.into() - }; - - let ItemFn { - attrs, - vis, - sig, - block, - } = input; - - let (attribute_args, ignored_attrs) = parse_attrs(attrs)?; - let logging_init = expand_logging_init(&attribute_args); - let tracing_init = expand_tracing_init(&attribute_args); - - let result = quote! { - #[#inner_test] - #(#ignored_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 - } - }; - Ok(result) -} - - -#[derive(Debug, Default)] -struct AttributeArgs { - default_log_filter: Option, -} - -impl AttributeArgs { - fn try_parse_attr_single(&mut self, attr: &Attribute) -> syn::Result { - if !attr.path().is_ident("test_log") { - return Ok(false) - } +pub use test_log_macros::test; - let nested_meta = attr.parse_args_with(Meta::parse)?; - let name_value = if let Meta::NameValue(name_value) = nested_meta { - name_value - } else { - return Err(syn::Error::new_spanned( - &nested_meta, - "Expected NameValue syntax, e.g. 'default_log_filter = \"debug\"'.", - )) - }; - - let ident = if let Some(ident) = name_value.path.get_ident() { - ident - } else { - return Err(syn::Error::new_spanned( - &name_value.path, - "Expected NameValue syntax, e.g. 'default_log_filter = \"debug\"'.", - )) - }; - - let arg_ref = if ident == "default_log_filter" { - &mut self.default_log_filter - } else { - 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) - } -} - - -/// Expand the initialization code for the `log` crate. -#[cfg(feature = "log")] -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! {} - }; - - quote! { - { - 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"))] -fn expand_logging_init(_attribute_args: &AttributeArgs) -> Tokens { - quote! {} -} - - -/// Expand the initialization code for the `tracing` crate. #[cfg(feature = "trace")] -fn expand_tracing_init(attribute_args: &AttributeArgs) -> Tokens { - let env_filter = if let Some(default_log_filter) = &attribute_args.default_log_filter { - quote! { - ::tracing_subscriber::EnvFilter::builder() - .with_default_directive( - #default_log_filter - .parse() - .expect("test-log: default_log_filter must be valid") - ) - .from_env_lossy() - } - } else { - quote! { ::tracing_subscriber::EnvFilter::from_default_env() } - }; +#[doc(hidden)] +pub use tracing_subscriber; - 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(#env_filter) - .with_span_events(__internal_event_filter) - .with_test_writer() - .try_init(); - } - } -} - -#[cfg(not(feature = "trace"))] -fn expand_tracing_init(_attribute_args: &AttributeArgs) -> Tokens { - quote! {} -} +#[cfg(feature = "log")] +#[doc(hidden)] +pub use env_logger;