diff --git a/rstest_reuse/src/lib.rs b/rstest_reuse/src/lib.rs index 3db390e..e31414b 100644 --- a/rstest_reuse/src/lib.rs +++ b/rstest_reuse/src/lib.rs @@ -194,7 +194,10 @@ use std::collections::HashMap; use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse, parse::Parse, parse_macro_input, Attribute, Ident, ItemFn, PatType, Path, Token}; +use syn::{ + parse, parse::Parse, parse_macro_input, Attribute, Error, Ident, Item, ItemFn, ItemMod, + PatType, Path, Token, +}; struct MergeAttrs { template: ItemFn, @@ -447,3 +450,158 @@ pub fn apply(args: proc_macro::TokenStream, input: proc_macro::TokenStream) -> T }; tokens.into() } + +/// Define a template for a group of methods located in module. +/// +/// The template supports attribute `#[replace]` to mark methods that should be excluded from +/// the template because they will contain implementation differences for specific cases. +/// +/// Example: +/// +/// ``` +/// #[rstest_reuse::template_group(template_tests)] +/// mod tests { +/// use rstest::rstest; +/// use rstest_reuse::replace; +/// +/// #[replace] +/// fn version() -> u8 { +/// 1 +/// } +/// +/// #[rstest] +/// fn test() { +/// let value = version(); +/// assert!(value > 0, "{} is not greater than 0", value) +/// } +/// } +/// +/// #[rstest_reuse::apply_group(template_tests)] +/// mod tests_first { +/// fn version() -> u8 { +/// 2 +/// } +/// } +/// +/// #[rstest_reuse::apply_group(template_tests)] +/// mod tests_seconds { +/// fn version() -> u8 { +/// 3 +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn template_group( + args: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let macro_name = parse_macro_input!(args as Path); + let module = parse_macro_input!(input as ItemMod); + let module_name = module.ident.clone(); + + let Some((_, content)) = module.content.clone() else { + return Error::new_spanned( + module.ident, + "Cannot create template tests to non-inline module. Use `mod name { ... }`", + ) + .to_compile_error() + .into(); + }; + + let mut macro_context = Vec::with_capacity(content.len()); + let mut module_context = Vec::with_capacity(content.len()); + + for item in content.iter() { + module_context.push(item); + + if let Item::Fn(func) = item { + if func + .attrs + .iter() + .any(|attr| attr.path().is_ident(&format_ident!("{}", "replace"))) + { + continue; + } + } + + macro_context.push(item); + } + + let tokens = quote! { + #[macro_export] + macro_rules! #macro_name { + () => { + #(#macro_context)* + } + } + + #[cfg(test)] + pub mod #module_name { + #(#module_context)* + } + }; + + tokens.into() +} + +/// Mark a method as requiring replacement with a different implementation. +/// Used in conjunction with `#[template_group]` +#[proc_macro_attribute] +pub fn replace(_args: TokenStream, input: proc_macro::TokenStream) -> proc_macro::TokenStream { + input.into() +} + +/// Apply a defined group template. +/// +/// Example: +/// +/// ``` +/// #[rstest_reuse::template_group(template_inner)] +/// mod inner { +/// use rstest::{rstest, fixture}; +/// use rstest_reuse::replace; +/// +/// #[fixture] +/// #[replace] +/// fn fixture() -> u8 { +/// 1 +/// } +/// +/// #[rstest] +/// fn test(fixture: u8) { +/// assert!(fixture > 0, "{} is not greater than 0", fixture) +/// } +/// } +/// +/// #[rstest_reuse::apply_group(template_inner)] +/// mod test { +/// #[fixture] +/// fn fixture() -> u8 { +/// 2 +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn apply_group(args: TokenStream, input: TokenStream) -> TokenStream { + let macro_name = parse_macro_input!(args as Path); + let module = parse_macro_input!(input as ItemMod); + let module_name = module.ident.clone(); + + let Some((_, module_content)) = module.content else { + return Error::new_spanned( + module.ident, + "Cannot add tests to non-inline module. Use `mod name { ... }`", + ) + .to_compile_error() + .into(); + }; + + TokenStream::from(quote! { + mod #module_name { + #![allow(unused_imports)] + + #(#module_content)* + #macro_name!(); + } + }) +} diff --git a/rstest_reuse/tests/acceptance.rs b/rstest_reuse/tests/acceptance.rs index 8339fab..2cbe35c 100644 --- a/rstest_reuse/tests/acceptance.rs +++ b/rstest_reuse/tests/acceptance.rs @@ -162,6 +162,36 @@ fn use_same_name_for_more_templates() { .assert(output); } +#[test] +fn use_template_group() { + let (output, _) = run_test("template_group.rs"); + + TestResults::new() + .ok("inner::test") + .fail("inner1::test") + .assert(output); +} + +#[test] +fn use_template_group_with_replace() { + let (output, _) = run_test("template_group_with_replace.rs"); + + TestResults::new() + .ok("inner::test") + .fail("inner1::test") + .assert(output); +} + +#[test] +fn use_template_group_with_replace_fixture() { + let (output, _) = run_test("template_group_with_replace_fixture.rs"); + + TestResults::new() + .ok("inner::test") + .fail("inner1::test") + .assert(output); +} + #[test] fn no_local_macro_should_not_compile() { let (output, _) = run_test("no_local_macro_should_not_compile.rs"); diff --git a/rstest_reuse/tests/resources/template_group.rs b/rstest_reuse/tests/resources/template_group.rs new file mode 100644 index 0000000..0b37e27 --- /dev/null +++ b/rstest_reuse/tests/resources/template_group.rs @@ -0,0 +1,23 @@ +#[rstest_reuse::template_group(template_example)] +pub mod inner1 { + use rstest::rstest; + use rstest_reuse::replace; + + #[replace] + fn prepare() -> u32 { + unimplemented!() + } + + #[rstest] + fn test() { + let value = prepare(); + assert_eq!(value, 0); + } +} + +#[rstest_reuse::apply_group(template_example)] +pub mod inner { + fn prepare() -> u32 { + 0 + } +} diff --git a/rstest_reuse/tests/resources/template_group_with_replace.rs b/rstest_reuse/tests/resources/template_group_with_replace.rs new file mode 100644 index 0000000..8ea259a --- /dev/null +++ b/rstest_reuse/tests/resources/template_group_with_replace.rs @@ -0,0 +1,23 @@ +#[rstest_reuse::template_group(template_inner)] +mod inner1 { + use rstest::rstest; + use rstest_reuse::replace; + + #[replace] + fn prepare() -> u8 { + unimplemented!() + } + + #[rstest] + fn test() { + let value = prepare(); + assert!(value > 0, "{} is not greater than 0", value) + } +} + +#[rstest_reuse::apply_group(template_inner)] +mod inner { + fn prepare() -> u8 { + 1 + } +} diff --git a/rstest_reuse/tests/resources/template_group_with_replace_fixture.rs b/rstest_reuse/tests/resources/template_group_with_replace_fixture.rs new file mode 100644 index 0000000..152d708 --- /dev/null +++ b/rstest_reuse/tests/resources/template_group_with_replace_fixture.rs @@ -0,0 +1,24 @@ +#[rstest_reuse::template_group(template_inner)] +mod inner1 { + use rstest::{fixture, rstest}; + use rstest_reuse::replace; + + #[fixture] + #[replace] + fn fixture() -> u8 { + unimplemented!() + } + + #[rstest] + fn test(fixture: u8) { + assert!(fixture > 0, "{} is not greater than 0", fixture) + } +} + +#[rstest_reuse::apply_group(template_inner)] +mod inner { + #[fixture] + fn fixture() -> u8 { + 1 + } +}