diff --git a/rstest/tests/fixture/mod.rs b/rstest/tests/fixture/mod.rs index 93f92d20..c8b691a4 100644 --- a/rstest/tests/fixture/mod.rs +++ b/rstest/tests/fixture/mod.rs @@ -242,10 +242,9 @@ mod should { output.stderr.str(), format!( r#" - --> {}/src/lib.rs:12:33 + --> {name}/src/lib.rs:14:33 | - 12 | fn error_cannot_resolve_fixture(no_fixture: u32) {{"#, - name + 14 | fn error_cannot_resolve_fixture(no_fixture: u32) {{"# ) .unindent() ); @@ -260,11 +259,10 @@ mod should { format!( r#" error[E0308]: mismatched types - --> {}/src/lib.rs:8:18 - | - 8 | let a: u32 = ""; - "#, - name + --> {name}/src/lib.rs:10:18 + | + 10 | let a: u32 = ""; + "# ) .unindent() ); @@ -277,20 +275,19 @@ mod should { assert_in!( output.stderr.str(), format!( - " + r#" error[E0308]: mismatched types - --> {}/src/lib.rs:16:29 - ", - name + --> {name}/src/lib.rs:17:29 + "# ) .unindent() ); assert_in!( output.stderr.str(), - " - 16 | fn error_fixture_wrong_type(fixture: String) { - | ^^^^^^" + r#" + 17 | fn error_fixture_wrong_type(fixture: String) {} + | ^^^^^^"# .unindent() ); } @@ -304,12 +301,11 @@ mod should { format!( " error: Missed argument: 'not_a_fixture' should be a test function argument. - --> {}/src/lib.rs:19:11 + --> {name}/src/lib.rs:19:11 | 19 | #[fixture(not_a_fixture(24))] | ^^^^^^^^^^^^^ - ", - name + " ) .unindent() ); @@ -324,15 +320,56 @@ mod should { format!( r#" error: Duplicate argument: 'f' is already defined. - --> {}/src/lib.rs:33:23 + --> {name}/src/lib.rs:32:23 | - 33 | #[fixture(f("first"), f("second"))] + 32 | #[fixture(f("first"), f("second"))] | ^ - "#, - name + "# + ) + .unindent() + ); + } + + #[rstest] + fn on_destruct_implicit_fixture(errors_rs: &(Output, String)) { + let (output, name) = errors_rs.clone(); + + assert_in!( + output.stderr.str(), + format!( + r#" + error: To destruct a fixture you should provide a path to resolve it by '#[from(...)]' attribute. + --> {name}/src/lib.rs:48:35 + | + 48 | fn error_destruct_without_resolve(T(a): T) {{}} + | ^^^^^^^ + "# + ) + .unindent() + ); + } + + #[rstest] + fn on_destruct_explicit_fixture_without_from(errors_rs: &(Output, String)) { + let (output, name) = errors_rs.clone(); + + assert_in!( + output.stderr.str(), + format!( + r#" + error: To destruct a fixture you should provide a path to resolve it by '#[from(...)]' attribute. + --> {name}/src/lib.rs:51:57 + | + 51 | fn error_destruct_without_resolve_also_with(#[with(21)] T(a): T) {{}} + | ^^^^^^^ + "# ) .unindent() ); + assert_eq!( + 1, + output.stderr.str().count("51 | fn error_destruct_without") + ) } #[fixture] diff --git a/rstest/tests/resources/fixture/errors.rs b/rstest/tests/resources/fixture/errors.rs index aac0846d..f00ff382 100644 --- a/rstest/tests/resources/fixture/errors.rs +++ b/rstest/tests/resources/fixture/errors.rs @@ -1,7 +1,9 @@ use rstest::*; #[fixture] -pub fn fixture() -> u32 { 42 } +pub fn fixture() -> u32 { + 42 +} #[fixture] fn error_inner(fixture: u32) { @@ -9,16 +11,13 @@ fn error_inner(fixture: u32) { } #[fixture] -fn error_cannot_resolve_fixture(no_fixture: u32) { -} +fn error_cannot_resolve_fixture(no_fixture: u32) {} #[fixture] -fn error_fixture_wrong_type(fixture: String) { -} +fn error_fixture_wrong_type(fixture: String) {} #[fixture(not_a_fixture(24))] -fn error_inject_an_invalid_fixture(fixture: String) { -} +fn error_inject_an_invalid_fixture(fixture: String) {} #[fixture] fn name() -> &'static str { @@ -31,5 +30,22 @@ fn f(name: &str) -> String { } #[fixture(f("first"), f("second"))] -fn error_inject_a_fixture_more_than_once(f: String) { +fn error_inject_a_fixture_more_than_once(f: String) {} + +struct T(u32); + +#[fixture] +fn structed() -> T { + T(42) } + +#[fixture] +fn structed_injectd(fixture: u32) -> T { + T(fixture) +} + +#[fixture] +fn error_destruct_without_resolve(T(a): T) {} + +#[fixture] +fn error_destruct_without_resolve_also_with(#[with(21)] T(a): T) {} diff --git a/rstest/tests/resources/rstest/destruct.rs b/rstest/tests/resources/rstest/destruct.rs new file mode 100644 index 00000000..302978f5 --- /dev/null +++ b/rstest/tests/resources/rstest/destruct.rs @@ -0,0 +1,79 @@ +use rstest::*; + +struct T { + a: u32, + b: u32, +} + +impl T { + fn new(a: u32, b: u32) -> Self { + Self { a, b } + } +} + +struct S(u32, u32); + +#[fixture] +fn fix() -> T { + T::new(1, 42) +} + +#[fixture] +fn named() -> S { + S(1, 42) +} + +#[fixture] +fn tuple() -> (u32, u32) { + (1, 42) +} + +#[fixture] +fn swap(#[from(fix)] T { a, b }: T) -> T { + T::new(b, a) +} + +#[rstest] +fn swapped(#[from(swap)] T { a, b }: T) { + assert_eq!(a, 42); + assert_eq!(b, 1); +} + +#[rstest] +#[case::two_times_twenty_one(T::new(2, 21))] +#[case::six_times_seven(T{ a: 6, b: 7 })] +fn cases_destruct( + #[from(fix)] T { a, b }: T, + #[case] T { a: c, b: d }: T, + #[values(T::new(42, 1), T{ a: 3, b: 14})] T { a: e, b: f }: T, +) { + assert_eq!(a * b, 42); + assert_eq!(c * d, 42); + assert_eq!(e * f, 42); +} + +#[rstest] +#[case::two_times_twenty_one(S(2, 21))] +#[case::six_times_seven(S(6, 7))] +fn cases_destruct_named_tuple( + #[from(named)] S(a, b): S, + #[case] S(c, d): S, + #[values(S(42, 1), S(3, 14))] S(e, f): S, +) { + assert_eq!(a * b, 42); + assert_eq!(c * d, 42); + assert_eq!(e * f, 42); +} + +#[rstest] +#[case::two_times_twenty_one((2, 21))] +#[case::six_times_seven((6, 7))] +fn cases_destruct_tuple( + #[from(tuple)] (a, b): (u32, u32), + #[case] (c, d): (u32, u32), + #[values((42, 1), (3, 14))] (e, f): (u32, u32), +) { + assert_eq!(a * b, 42); + assert_eq!(c * d, 42); + assert_eq!(e * f, 42); +} diff --git a/rstest/tests/resources/rstest/errors.rs b/rstest/tests/resources/rstest/errors.rs index 516e1e72..a13fbaf5 100644 --- a/rstest/tests/resources/rstest/errors.rs +++ b/rstest/tests/resources/rstest/errors.rs @@ -118,3 +118,9 @@ fn error_timeout_without_duration() {} #[rstest] fn error_absolute_path_files(#[files("/tmp/tmp.Q81idVZYAV/*.txt")] path: std::path::PathBuf) {} + +struct T(u32, u32); + +#[rstest] +#[case(T(3, 4))] +fn wrong_destruct_fixture(T(a, b): T, #[with(42)] T(c, d): T) {} diff --git a/rstest/tests/rstest/mod.rs b/rstest/tests/rstest/mod.rs index 67fcd9b3..1dcb3156 100644 --- a/rstest/tests/rstest/mod.rs +++ b/rstest/tests/rstest/mod.rs @@ -973,6 +973,27 @@ fn happy_path() { .assert(output); } +#[test] +fn destruct() { + let (output, _) = run_test("destruct.rs"); + + TestResults::new() + .ok("cases_destruct::case_1_two_times_twenty_one::__destruct_3_1_T__new_42_1_") + .ok("cases_destruct::case_1_two_times_twenty_one::__destruct_3_2_T_a_3_b_14_") + .ok("cases_destruct::case_2_six_times_seven::__destruct_3_1_T__new_42_1_") + .ok("cases_destruct::case_2_six_times_seven::__destruct_3_2_T_a_3_b_14_") + .ok("cases_destruct_named_tuple::case_1_two_times_twenty_one::__destruct_3_1_S_42_1_") + .ok("cases_destruct_named_tuple::case_1_two_times_twenty_one::__destruct_3_2_S_3_14_") + .ok("cases_destruct_named_tuple::case_2_six_times_seven::__destruct_3_1_S_42_1_") + .ok("cases_destruct_named_tuple::case_2_six_times_seven::__destruct_3_2_S_3_14_") + .ok("cases_destruct_tuple::case_1_two_times_twenty_one::__destruct_3_1__42_1_") + .ok("cases_destruct_tuple::case_1_two_times_twenty_one::__destruct_3_2__3_14_") + .ok("cases_destruct_tuple::case_2_six_times_seven::__destruct_3_1__42_1_") + .ok("cases_destruct_tuple::case_2_six_times_seven::__destruct_3_2__3_14_") + .ok("swapped") + .assert(output); +} + #[test] fn rename() { let (output, _) = run_test("rename.rs"); @@ -1727,4 +1748,38 @@ mod should_show_correct_errors { .unindent() ); } + + #[test] + fn try_to_destruct_implicit_fixture() { + let (output, name) = execute(); + + assert_in!( + output.stderr.str(), + format!( + r#" + error: To destruct a fixture you should provide a path to resolve it by '#[from(...)]' attribute. + --> {name}/src/lib.rs:126:27 + | + 126 | fn wrong_destruct_fixture(T(a, b): T, #[with(42)] T(c, d): T) {{}} + | ^^^^^^^^^^"#, + ) + .unindent() + ); + + assert_in!( + output.stderr.str(), + format!( + r#" + error: To destruct a fixture you should provide a path to resolve it by '#[from(...)]' attribute. + --> {name}/src/lib.rs:126:51 + | + 126 | fn wrong_destruct_fixture(T(a, b): T, #[with(42)] T(c, d): T) {{}} + | ^^^^^^^^^^"#, + ) + .unindent() + ); + + assert_not_in!(output.stderr.str(), "#[case] T(e, f): T"); + assert_not_in!(output.stderr.str(), "#[values(T(1, 2))] T(g, h): T"); + } } diff --git a/rstest_macros/Cargo.toml b/rstest_macros/Cargo.toml index c5798349..293fb757 100644 --- a/rstest_macros/Cargo.toml +++ b/rstest_macros/Cargo.toml @@ -44,7 +44,7 @@ actix-rt = "2.7.0" async-std = { version = "1.12.0", features = ["attributes"] } maplit = "1.0.2" pretty_assertions = "1.2.1" -rstest = { path = "../rstest", default-features = false } +rstest = { version = "0.20.0", default-features = false } rstest_reuse = { path = "../rstest_reuse" } rstest_test = { path = "../rstest_test" } diff --git a/rstest_macros/src/error.rs b/rstest_macros/src/error.rs index 9d6f2fd6..5de23d30 100644 --- a/rstest_macros/src/error.rs +++ b/rstest_macros/src/error.rs @@ -3,21 +3,29 @@ use std::collections::HashMap; use proc_macro2::TokenStream; use syn::{spanned::Spanned, visit::Visit}; -use syn::{visit, ItemFn}; +use syn::{visit, ItemFn, Pat}; use crate::parse::{ fixture::FixtureInfo, rstest::{RsTestData, RsTestInfo}, }; -use crate::refident::MaybeIdent; +use crate::refident::{MaybeIdent, MaybePat}; -use super::utils::fn_args_has_ident; +use super::utils::fn_args_has_pat; + +pub mod messages { + pub const DESTRUCT_WITHOUT_FROM : &'static str = "To destruct a fixture you should provide a path to resolve it by '#[from(...)]' attribute."; + pub fn use_more_than_once(name: &str) -> String { + format!("You cannot use '{name}' attribute more than once for the same argument") + } +} pub(crate) fn rstest(test: &ItemFn, info: &RsTestInfo) -> TokenStream { missed_arguments(test, info.data.items.iter()) .chain(duplicate_arguments(info.data.items.iter())) .chain(invalid_cases(&info.data)) .chain(case_args_without_cases(&info.data)) + .chain(destruct_fixture_without_from(test, info)) .map(|e| e.to_compile_error()) .collect() } @@ -27,14 +35,15 @@ pub(crate) fn fixture(test: &ItemFn, info: &FixtureInfo) -> TokenStream { .chain(duplicate_arguments(info.data.items.iter())) .chain(async_once(test, info)) .chain(generics_once(test, info)) + .chain(destruct_fixture_without_from(test, info)) .map(|e| e.to_compile_error()) .collect() } fn async_once<'a>(test: &'a ItemFn, info: &FixtureInfo) -> Errors<'a> { match (test.sig.asyncness, info.arguments.get_once()) { - (Some(_asyncness), Some(once)) => Box::new(std::iter::once(syn::Error::new( - once.span(), + (Some(_asyncness), Some(once)) => Box::new(std::iter::once(syn::Error::new_spanned( + once, "Cannot apply #[once] to async fixture.", ))), _ => Box::new(std::iter::empty()), @@ -70,14 +79,48 @@ fn has_some_generics(test: &ItemFn) -> bool { fn generics_once<'a>(test: &'a ItemFn, info: &FixtureInfo) -> Errors<'a> { match (has_some_generics(test), info.arguments.get_once()) { - (true, Some(once)) => Box::new(std::iter::once(syn::Error::new( - once.span(), + (true, Some(once)) => Box::new(std::iter::once(syn::Error::new_spanned( + once, "Cannot apply #[once] on generic fixture.", ))), _ => Box::new(std::iter::empty()), } } +trait IsImplicitFixture { + fn is_implicit_fixture(&self, pat: &Pat) -> bool; +} + +impl IsImplicitFixture for FixtureInfo { + fn is_implicit_fixture(&self, pat: &Pat) -> bool { + !self.data.fixtures().any(|f| &f.arg == pat) + } +} + +impl IsImplicitFixture for RsTestInfo { + fn is_implicit_fixture(&self, pat: &Pat) -> bool { + !self.data.case_args().any(|a| a == pat) + && !self.data.list_values().any(|a| &a.arg == pat) + && !self.data.fixtures().any(|f| &f.arg == pat) + } +} + +fn destruct_fixture_without_from<'a>( + function: &'a ItemFn, + info: &'a impl IsImplicitFixture, +) -> Errors<'a> { + Box::new( + function + .sig + .inputs + .iter() + .filter_map(|a| a.maybe_pat().map(|p| (a, p))) + .filter(|&(_, p)| p.maybe_ident().is_none()) + .filter(|&(_, p)| info.is_implicit_fixture(p)) + .map(|(a, _)| syn::Error::new_spanned(a, messages::DESTRUCT_WITHOUT_FROM)), + ) +} + #[derive(Debug, Default)] pub struct ErrorsVec(Vec); @@ -159,41 +202,46 @@ impl From for proc_macro::TokenStream { type Errors<'a> = Box + 'a>; -fn missed_arguments<'a, I: MaybeIdent + Spanned + 'a>( +fn missed_arguments<'a, I: MaybePat + Spanned + 'a>( test: &'a ItemFn, args: impl Iterator + 'a, ) -> Errors<'a> { Box::new( - args.filter_map(|it| it.maybe_ident().map(|ident| (it, ident))) - .filter(move |(_, ident)| !fn_args_has_ident(test, ident)) - .map(|(missed, ident)| { + args.filter_map(|it| it.maybe_pat().map(|pat| (it, pat))) + .filter(move |(_, pat)| !fn_args_has_pat(test, pat)) + .map(|(missed, pat)| { syn::Error::new( missed.span(), - format!("Missed argument: '{ident}' should be a test function argument."), + format!( + "Missed argument: '{}' should be a test function argument.", + pat.render_type() + ), ) }), ) } -fn duplicate_arguments<'a, I: MaybeIdent + Spanned + 'a>( +fn duplicate_arguments<'a, I: MaybePat + Spanned + 'a>( args: impl Iterator + 'a, ) -> Errors<'a> { let mut used = HashMap::new(); Box::new( - args.filter_map(|it| it.maybe_ident().map(|ident| (it, ident))) - .filter_map(move |(it, ident)| { - let name = ident.to_string(); - let is_duplicate = used.contains_key(&name); - used.insert(name, it); + args.filter_map(|it| it.maybe_pat().map(|pat| (it, pat))) + .filter_map(move |(it, pat)| { + let is_duplicate = used.contains_key(&pat); + used.insert(pat, it); match is_duplicate { - true => Some((it, ident)), + true => Some((it, pat)), false => None, } }) - .map(|(duplicate, ident)| { + .map(|(duplicate, pat)| { syn::Error::new( duplicate.span(), - format!("Duplicate argument: '{ident}' is already defined."), + format!( + "Duplicate argument: '{}' is already defined.", + pat.render_type() + ), ) }), ) @@ -225,9 +273,25 @@ fn case_args_without_cases(params: &RsTestData) -> Errors { Box::new(std::iter::empty()) } +trait RenderType { + fn render_type(&self) -> String; +} + +impl RenderType for syn::Pat { + fn render_type(&self) -> String { + match self { + syn::Pat::Ident(ref i) => i.ident.to_string(), + other => format!("{other:?}"), + } + } +} + #[cfg(test)] mod test { - use crate::test::{assert_eq, *}; + use crate::{ + parse::ExtendWithFunctionAttrs, + test::{assert_eq, *}, + }; use rstest_test::assert_in; use super::*; @@ -268,4 +332,67 @@ mod test { assert_eq!(0, errors.count()); } + + #[rstest] + #[case::base_in_fixture("fn f(T{a}: T){}", FixtureInfo::default(), 1)] + #[case::one_of_two("fn f(T{a}: T, #[from(f)] T{a: c}: T){}", FixtureInfo::default(), 1)] + #[case::find_all( + "fn f(T{a}: T, z: u32, S(a,b): S, x: u32, (f , g, h): (u32, String, f32)){}", + FixtureInfo::default(), + 3 + )] + #[case::base_in_test("fn f(T{a}: T){}", RsTestInfo::default(), 1)] + #[case::not_case_or_values( + "fn f(#[case] T{a}: T, #[values(T::a(),T::b())] T{v}: T, S{e}: S){}", + RsTestInfo::default(), + 1 + )] + #[case::mixed_more( + r#"fn wrong_destruct_fixture( + T(a, b): T, + #[case] T(e, f): T, + #[values(T(1, 2))] T(g, h): T, + ) {}"#, + RsTestInfo::default(), + 1 + )] + fn destruct_implicit_from_should_return_error( + #[case] f: &str, + #[case] mut info: impl ExtendWithFunctionAttrs + IsImplicitFixture, + #[case] n: usize, + ) { + let mut f: ItemFn = f.ast(); + + info.extend_with_function_attrs(&mut f).unwrap(); + + let errors = destruct_fixture_without_from(&f, &info); + + let out = errors + .map(|e| format!("{:?}", e)) + .collect::>() + .join("\n"); + + assert_in!(out, messages::DESTRUCT_WITHOUT_FROM); + assert_eq!(n, out.lines().count()) + } + + #[rstest] + #[case::happy_fixture("fn f(#[from(b)] T{a}: T){}", FixtureInfo::default())] + #[case::happy_test("fn f(#[from(b)] T{a}: T){}", RsTestInfo::default())] + #[case::some_cases_or_values( + "fn f(#[case] T{a}: T, #[values(T::a(),T::b())] T{v}: T){}", + RsTestInfo::default() + )] + fn destruct_not_implicit_should_not_return_error( + #[case] f: &str, + #[case] mut info: impl ExtendWithFunctionAttrs + IsImplicitFixture, + ) { + let mut f: ItemFn = f.ast(); + + info.extend_with_function_attrs(&mut f).unwrap(); + + let errors = destruct_fixture_without_from(&f, &info); + + assert_eq!(0, errors.count()); + } } diff --git a/rstest_macros/src/parse/arguments.rs b/rstest_macros/src/parse/arguments.rs index ded4792e..dd9b0fb8 100644 --- a/rstest_macros/src/parse/arguments.rs +++ b/rstest_macros/src/parse/arguments.rs @@ -1,6 +1,12 @@ use std::collections::HashMap; -use syn::Ident; +use quote::format_ident; +use syn::{FnArg, Ident, Pat}; + +use crate::{ + refident::{IntoPat, MaybeIdent, MaybePatType, MaybePatTypeMut}, + resolver::pat_invert_mutability, +}; #[derive(PartialEq, Debug, Clone, Copy)] #[allow(dead_code)] @@ -12,11 +18,12 @@ pub(crate) enum FutureArg { Await, } -#[derive(PartialEq, Default, Debug)] +#[derive(Clone, PartialEq, Default, Debug)] pub(crate) struct ArgumentInfo { future: FutureArg, by_ref: bool, ignore: bool, + inner_pat: Option, // Optional pat used to inject data and call test function } impl ArgumentInfo { @@ -41,6 +48,13 @@ impl ArgumentInfo { } } + fn inner_pat(pat: Pat) -> Self { + Self { + inner_pat: Some(pat), + ..Default::default() + } + } + fn is_future(&self) -> bool { use FutureArg::*; @@ -62,23 +76,40 @@ impl ArgumentInfo { } } -#[derive(PartialEq, Default, Debug)] +#[derive(Clone, PartialEq, Default, Debug)] +struct Args { + args: HashMap, +} + +impl Args { + fn get(&self, pat: &Pat) -> Option<&ArgumentInfo> { + self.args + .get(pat) + .or_else(|| self.args.get(&pat_invert_mutability(&pat))) + } + + fn entry(&mut self, pat: Pat) -> std::collections::hash_map::Entry { + self.args.entry(pat) + } +} + +#[derive(Clone, PartialEq, Default, Debug)] pub(crate) struct ArgumentsInfo { - args: HashMap, + args: Args, is_global_await: bool, once: Option, } impl ArgumentsInfo { - pub(crate) fn set_future(&mut self, ident: Ident, kind: FutureArg) { + pub(crate) fn set_future(&mut self, pat: Pat, kind: FutureArg) { self.args - .entry(ident) + .entry(pat) .and_modify(|v| v.future = kind) .or_insert_with(|| ArgumentInfo::future(kind)); } - pub(crate) fn set_futures(&mut self, futures: impl Iterator) { - futures.for_each(|(ident, k)| self.set_future(ident, k)); + pub(crate) fn set_futures(&mut self, futures: impl Iterator) { + futures.for_each(|(pat, k)| self.set_future(pat, k)); } pub(crate) fn set_global_await(&mut self, is_global_await: bool) { @@ -86,19 +117,19 @@ impl ArgumentsInfo { } #[allow(dead_code)] - pub(crate) fn add_future(&mut self, ident: Ident) { - self.set_future(ident, FutureArg::Define); + pub(crate) fn add_future(&mut self, pat: Pat) { + self.set_future(pat, FutureArg::Define); } - pub(crate) fn is_future(&self, id: &Ident) -> bool { + pub(crate) fn is_future(&self, pat: &Pat) -> bool { self.args - .get(id) + .get(pat) .map(|arg| arg.is_future()) .unwrap_or_default() } - pub(crate) fn is_future_await(&self, ident: &Ident) -> bool { - match self.args.get(ident) { + pub(crate) fn is_future_await(&self, pat: &Pat) -> bool { + match self.args.get(pat) { Some(arg) => arg.is_future_await() || (arg.is_future() && self.is_global_await()), None => false, } @@ -120,41 +151,90 @@ impl ArgumentsInfo { self.get_once().is_some() } - pub(crate) fn set_by_ref(&mut self, ident: Ident) { + pub(crate) fn set_by_ref(&mut self, pat: Pat) { self.args - .entry(ident) + .entry(pat) .and_modify(|v| v.by_ref = true) .or_insert_with(ArgumentInfo::by_ref); } - pub(crate) fn set_ignore(&mut self, ident: Ident) { + pub(crate) fn set_ignore(&mut self, pat: Pat) { self.args - .entry(ident) + .entry(pat) .and_modify(|v| v.ignore = true) .or_insert_with(ArgumentInfo::ignore); } - pub(crate) fn set_by_refs(&mut self, by_refs: impl Iterator) { - by_refs.for_each(|ident| self.set_by_ref(ident)); + pub(crate) fn set_by_refs(&mut self, by_refs: impl Iterator) { + by_refs.for_each(|pat| self.set_by_ref(pat)); } - pub(crate) fn set_ignores(&mut self, ignores: impl Iterator) { - ignores.for_each(|ident| self.set_ignore(ident)); + pub(crate) fn set_ignores(&mut self, ignores: impl Iterator) { + ignores.for_each(|pat| self.set_ignore(pat)); } - pub(crate) fn is_by_refs(&self, id: &Ident) -> bool { + pub(crate) fn is_by_refs(&self, id: &Pat) -> bool { self.args .get(id) .map(|arg| arg.is_by_ref()) .unwrap_or_default() } - pub(crate) fn is_ignore(&self, id: &Ident) -> bool { + pub(crate) fn is_ignore(&self, pat: &Pat) -> bool { self.args - .get(id) + .get(pat) .map(|arg| arg.is_ignore()) .unwrap_or_default() } + + pub(crate) fn set_inner_pat(&mut self, pat: Pat, inner: Pat) { + self.args + .entry(pat) + .and_modify(|v| v.inner_pat = Some(inner.clone())) + .or_insert_with(|| ArgumentInfo::inner_pat(inner)); + } + + pub(crate) fn set_inner_ident(&mut self, pat: Pat, ident: Ident) { + self.set_inner_pat(pat, ident.into_pat()); + } + + pub(crate) fn inner_pat<'arguments: 'pat_ref, 'pat_ref>( + &'arguments self, + id: &'pat_ref Pat, + ) -> &'pat_ref Pat { + self.args + .get(id) + .and_then(|arg| arg.inner_pat.as_ref()) + .unwrap_or(id) + } + + pub(crate) fn register_inner_destructored_idents_names(&mut self, item_fn: &syn::ItemFn) { + let mut anonymous_destruct = 0_usize; + // On the signature we remove all destruct arguments and replace them with `__destruct_{id}` + // This is just to define the new arguments and local variable that we use in the test + // and coll the original signature that should preserve the destruct arguments. + for arg in item_fn.sig.inputs.iter() { + if let Some(pt) = arg.maybe_pat_type() { + if pt.maybe_ident().is_none() { + anonymous_destruct += 1; + let ident = format_ident!("__destruct_{}", anonymous_destruct); + self.set_inner_ident(pt.pat.as_ref().clone(), ident); + } + } + } + } + + pub(crate) fn replace_fn_args_with_related_inner_pat<'a>( + &'a self, + fn_args: impl Iterator + 'a, + ) -> impl Iterator + 'a { + fn_args.map(|mut fn_arg| { + if let Some(p) = fn_arg.maybe_pat_type_mut() { + p.pat = Box::new(self.inner_pat(p.pat.as_ref()).clone()); + } + fn_arg + }) + } } #[cfg(test)] @@ -165,35 +245,77 @@ mod should_implement_is_future_await_logic { #[fixture] fn info() -> ArgumentsInfo { let mut a = ArgumentsInfo::default(); - a.set_future(ident("simple"), FutureArg::Define); - a.set_future(ident("other_simple"), FutureArg::Define); - a.set_future(ident("awaited"), FutureArg::Await); - a.set_future(ident("other_awaited"), FutureArg::Await); - a.set_future(ident("none"), FutureArg::None); + a.set_future(pat("simple"), FutureArg::Define); + a.set_future(pat("other_simple"), FutureArg::Define); + a.set_future(pat("awaited"), FutureArg::Await); + a.set_future(pat("other_awaited"), FutureArg::Await); + a.set_future(pat("none"), FutureArg::None); a } #[rstest] fn no_matching_ident(info: ArgumentsInfo) { - assert!(!info.is_future_await(&ident("some"))); - assert!(!info.is_future_await(&ident("simple"))); - assert!(!info.is_future_await(&ident("none"))); + assert!(!info.is_future_await(&pat("some"))); + assert!(!info.is_future_await(&pat("simple"))); + assert!(!info.is_future_await(&pat("none"))); } #[rstest] fn matching_ident(info: ArgumentsInfo) { - assert!(info.is_future_await(&ident("awaited"))); - assert!(info.is_future_await(&ident("other_awaited"))); + assert!(info.is_future_await(&pat("awaited"))); + assert!(info.is_future_await(&pat("other_awaited"))); } #[rstest] fn global_matching_future_ident(mut info: ArgumentsInfo) { info.set_global_await(true); - assert!(info.is_future_await(&ident("simple"))); - assert!(info.is_future_await(&ident("other_simple"))); - assert!(info.is_future_await(&ident("awaited"))); + assert!(info.is_future_await(&pat("simple"))); + assert!(info.is_future_await(&pat("other_simple"))); + assert!(info.is_future_await(&pat("awaited"))); + + assert!(!info.is_future_await(&pat("some"))); + assert!(!info.is_future_await(&pat("none"))); + } +} + +#[cfg(test)] +mod should_register_inner_destructored_idents_names { + use super::*; + use crate::test::{assert_eq, *}; + + #[test] + fn implement_the_correct_pat_reolver() { + let item_fn = "fn test_function(A(a,b): A, (c,d,e): (u32, u32, u32), none: u32, B{s,d} : B, clean: C) {}".ast(); + + let mut arguments = ArgumentsInfo::default(); + + arguments.register_inner_destructored_idents_names(&item_fn); + + assert_eq!(arguments.inner_pat(&pat("A(a,b)")), &pat("__destruct_1")); + assert_eq!(arguments.inner_pat(&pat("(c,d,e)")), &pat("__destruct_2")); + assert_eq!(arguments.inner_pat(&pat("none")), &pat("none")); + assert_eq!(arguments.inner_pat(&pat("B{s,d}")), &pat("__destruct_3")); + assert_eq!(arguments.inner_pat(&pat("clean")), &pat("clean")); + } + + #[test] + fn and_replace_them_correctly() { + let item_fn = "fn test_function(A(a,b): A, (c,d,e): (u32, u32, u32), none: u32, B{s,d} : B, clean: C) {}".ast(); + + let mut arguments = ArgumentsInfo::default(); + + arguments.register_inner_destructored_idents_names(&item_fn); + + let new_args = arguments + .replace_fn_args_with_related_inner_pat(item_fn.sig.inputs.into_iter()) + .filter_map(|f| f.maybe_ident().cloned()) + .map(|id| id.to_string()) + .collect::>() + .join(" | "); - assert!(!info.is_future_await(&ident("some"))); - assert!(!info.is_future_await(&ident("none"))); + assert_eq!( + new_args, + "__destruct_1 | __destruct_2 | none | __destruct_3 | clean" + ); } } diff --git a/rstest_macros/src/parse/by_ref.rs b/rstest_macros/src/parse/by_ref.rs index 7cf3c16d..bf59f779 100644 --- a/rstest_macros/src/parse/by_ref.rs +++ b/rstest_macros/src/parse/by_ref.rs @@ -1,10 +1,10 @@ -use syn::{visit_mut::VisitMut, Ident, ItemFn}; +use syn::{visit_mut::VisitMut, ItemFn, Pat}; use crate::error::ErrorsVec; use super::just_once::JustOnceFnArgAttributeExtractor; -pub(crate) fn extract_by_ref(item_fn: &mut ItemFn) -> Result, ErrorsVec> { +pub(crate) fn extract_by_ref(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut extractor = JustOnceFnArgAttributeExtractor::from("by_ref"); extractor.visit_item_fn_mut(item_fn); extractor.take() @@ -46,7 +46,7 @@ mod should { let by_refs = extract_by_ref(&mut item_fn).unwrap(); assert_eq!(expected, item_fn); - assert_eq!(by_refs, to_idents!(expected_refs)); + assert_eq!(by_refs, to_pats!(expected_refs)); } #[rstest] diff --git a/rstest_macros/src/parse/fixture.rs b/rstest_macros/src/parse/fixture.rs index a463f232..2dad5bd2 100644 --- a/rstest_macros/src/parse/fixture.rs +++ b/rstest_macros/src/parse/fixture.rs @@ -3,19 +3,20 @@ use syn::{ parse::{Parse, ParseStream}, parse_quote, visit_mut::VisitMut, - Expr, FnArg, Ident, ItemFn, Token, + Expr, FnArg, Ident, ItemFn, Pat, Token, }; use super::{ arguments::ArgumentsInfo, - extract_default_return_type, extract_defaults, extract_fixtures, extract_partials_return_type, + extract_argument_attrs, extract_default_return_type, extract_defaults, extract_fixtures, + extract_partials_return_type, future::{extract_futures, extract_global_awt}, parse_vector_trailing_till_double_comma, Attributes, ExtendWithFunctionAttrs, Fixture, }; use crate::{ error::ErrorsVec, parse::extract_once, - refident::{MaybeIdent, RefIdent}, + refident::{IntoPat, MaybeIdent, MaybePat, MaybePatTypeMut, RefPat}, utils::attr_is, }; use crate::{parse::Attribute, utils::attr_in}; @@ -89,6 +90,9 @@ impl ExtendWithFunctionAttrs for FixtureInfo { self.arguments.set_once(once); self.arguments.set_global_await(global_awt); self.arguments.set_futures(futures.into_iter()); + self.arguments + .register_inner_destructored_idents_names(item_fn); + Ok(()) } } @@ -110,9 +114,7 @@ fn parse_attribute_args_just_once<'a, T: Parse>( (first, _) => { errors.push(syn::Error::new_spanned( a, - format!( - "You cannot use '{name}' attribute more than once for the same argument" - ), + crate::error::messages::use_more_than_once(name), )); first } @@ -127,14 +129,9 @@ pub(crate) struct FixturesFunctionExtractor(pub(crate) Vec, pub(crate) impl VisitMut for FixturesFunctionExtractor { fn visit_fn_arg_mut(&mut self, node: &mut FnArg) { - let arg = if let FnArg::Typed(ref mut arg) = node { - arg - } else { - return; - }; - let name = match arg.maybe_ident().cloned() { - Some(ident) => ident, - _ => return, + let arg = match node.maybe_pat_type_mut() { + Some(pt) => pt, + None => return, }; let (extracted, remain): (Vec<_>, Vec<_>) = std::mem::take(&mut arg.attrs) .into_iter() @@ -143,11 +140,58 @@ impl VisitMut for FixturesFunctionExtractor { let (pos, errors) = parse_attribute_args_just_once(extracted.iter(), "with"); self.1.extend(errors); - let (resolve, errors) = parse_attribute_args_just_once(extracted.iter(), "from"); + let (resolve, errors): (Option, _) = + parse_attribute_args_just_once(extracted.iter(), "from"); self.1.extend(errors); - if pos.is_some() || resolve.is_some() { - self.0 - .push(Fixture::new(name, resolve, pos.unwrap_or_default())) + + match (resolve, arg.pat.maybe_ident()) { + (Some(res), _) => self.0.push(Fixture::new( + arg.pat.as_ref().clone(), + res, + pos.unwrap_or_default(), + )), + (None, Some(ident)) if pos.is_some() => self.0.push(Fixture::new( + arg.pat.as_ref().clone(), + ident.clone().into(), + pos.unwrap_or_default(), + )), + (None, None) if pos.is_some() => { + self.1.push(syn::Error::new_spanned( + node, + crate::error::messages::DESTRUCT_WITHOUT_FROM, + )); + } + _ => {} + } + } +} + +/// Simple struct used to visit function attributes and extract fixture default values info and +/// eventualy parsing errors +#[derive(Default)] +pub(crate) struct DefaultsFunctionExtractor( + pub(crate) Vec, + pub(crate) Vec, +); + +impl VisitMut for DefaultsFunctionExtractor { + fn visit_fn_arg_mut(&mut self, node: &mut FnArg) { + let pat = match node.maybe_pat() { + Some(pat) => pat.clone(), + None => return, + }; + for r in extract_argument_attrs( + node, + |a| attr_is(a, "default"), + |a| { + a.parse_args::() + .map(|e| ArgumentValue::new(pat.clone(), e)) + }, + ) { + match r { + Ok(value) => self.0.push(value), + Err(err) => self.1.push(err), + } } } } @@ -187,13 +231,13 @@ impl Parse for FixtureData { #[derive(PartialEq, Debug)] pub(crate) struct ArgumentValue { - pub name: Ident, + pub arg: Pat, pub expr: Expr, } impl ArgumentValue { - pub(crate) fn new(name: Ident, expr: Expr) -> Self { - Self { name, expr } + pub(crate) fn new(arg: Pat, expr: Expr) -> Self { + Self { arg, expr } } } @@ -219,18 +263,24 @@ impl Parse for FixtureItem { } } -impl RefIdent for FixtureItem { - fn ident(&self) -> &Ident { +impl RefPat for FixtureItem { + fn pat(&self) -> &Pat { match self { - FixtureItem::Fixture(Fixture { ref name, .. }) => name, - FixtureItem::ArgumentValue(ref av) => &av.name, + FixtureItem::Fixture(Fixture { ref arg, .. }) => arg, + FixtureItem::ArgumentValue(ref av) => &av.arg, } } } +impl MaybePat for FixtureItem { + fn maybe_pat(&self) -> Option<&syn::Pat> { + Some(self.pat()) + } +} + impl ToTokens for FixtureItem { fn to_tokens(&self, tokens: &mut TokenStream) { - self.ident().to_tokens(tokens) + self.pat().to_tokens(tokens) } } @@ -242,10 +292,10 @@ impl From for FixtureItem { impl Parse for ArgumentValue { fn parse(input: ParseStream) -> syn::Result { - let name = input.parse()?; + let name: Ident = input.parse()?; let _eq: Token![=] = input.parse()?; let expr = input.parse()?; - Ok(ArgumentValue::new(name, expr)) + Ok(ArgumentValue::new(name.into_pat(), expr)) } } @@ -572,8 +622,8 @@ mod extend { info.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!(item_fn, expected); - assert!(info.arguments.is_future(&ident("a"))); - assert!(!info.arguments.is_future(&ident("b"))); + assert!(info.arguments.is_future(&pat("a"))); + assert!(!info.arguments.is_future(&pat("b"))); } mod raise_error { @@ -624,6 +674,21 @@ mod extend { assert_eq!(3, errors.len()); } + #[test] + fn fixture_destruct_without_from() { + let mut item_fn: ItemFn = r#" + fn my_fix(#[with(1)] T{a}: T) {} + "# + .ast(); + + let errors = FixtureInfo::default() + .extend_with_function_attrs(&mut item_fn) + .err() + .unwrap_or_default(); + + assert_in!(errors[0].to_string(), "destruct"); + } + #[test] fn from_used_more_than_once() { let mut item_fn: ItemFn = r#" diff --git a/rstest_macros/src/parse/future.rs b/rstest_macros/src/parse/future.rs index d176cc34..ff20988c 100644 --- a/rstest_macros/src/parse/future.rs +++ b/rstest_macros/src/parse/future.rs @@ -1,5 +1,5 @@ use quote::{format_ident, ToTokens}; -use syn::{visit_mut::VisitMut, FnArg, Ident, ItemFn, PatType, Type}; +use syn::{visit_mut::VisitMut, FnArg, Ident, ItemFn, Pat, PatType, Type}; use crate::{error::ErrorsVec, refident::MaybeType}; @@ -10,7 +10,7 @@ use super::{ }, }; -pub(crate) fn extract_futures(item_fn: &mut ItemFn) -> Result, ErrorsVec> { +pub(crate) fn extract_futures(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut extractor = JustOnceFnArgAttributeExtractor::::new("future"); extractor.visit_item_fn_mut(item_fn); @@ -38,11 +38,11 @@ impl Validator for GlobalAwtBuilder {} struct FutureBuilder; -impl AttrBuilder for FutureBuilder { - type Out = (Ident, FutureArg); +impl AttrBuilder for FutureBuilder { + type Out = (Pat, FutureArg); - fn build(attr: syn::Attribute, ident: &Ident) -> syn::Result { - Self::compute_arguments_kind(&attr).map(|kind| (ident.clone(), kind)) + fn build(attr: syn::Attribute, pat: &Pat) -> syn::Result { + Self::compute_arguments_kind(&attr).map(|kind| (pat.clone(), kind)) } } @@ -183,7 +183,7 @@ mod should { futures, expected_futures .into_iter() - .map(|(id, a)| (ident(id), *a)) + .map(|(id, a)| (pat(id), *a)) .collect::>() ); assert_eq!(expected_awt, awt); diff --git a/rstest_macros/src/parse/ignore.rs b/rstest_macros/src/parse/ignore.rs index 7a16a8e6..52d3985f 100644 --- a/rstest_macros/src/parse/ignore.rs +++ b/rstest_macros/src/parse/ignore.rs @@ -1,10 +1,10 @@ -use syn::{visit_mut::VisitMut, Ident, ItemFn}; +use syn::{visit_mut::VisitMut, ItemFn, Pat}; use crate::error::ErrorsVec; use super::just_once::JustOnceFnArgAttributeExtractor; -pub(crate) fn extract_ignores(item_fn: &mut ItemFn) -> Result, ErrorsVec> { +pub(crate) fn extract_ignores(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut extractor = JustOnceFnArgAttributeExtractor::from("ignore"); extractor.visit_item_fn_mut(item_fn); extractor.take() @@ -46,7 +46,7 @@ mod should { let by_refs = extract_ignores(&mut item_fn).unwrap(); assert_eq!(expected, item_fn); - assert_eq!(by_refs, to_idents!(expected_refs)); + assert_eq!(by_refs, to_pats!(expected_refs)); } #[test] diff --git a/rstest_macros/src/parse/just_once.rs b/rstest_macros/src/parse/just_once.rs index ca0e30fc..303d556e 100644 --- a/rstest_macros/src/parse/just_once.rs +++ b/rstest_macros/src/parse/just_once.rs @@ -1,9 +1,9 @@ use std::marker::PhantomData; use quote::ToTokens; -use syn::{visit_mut::VisitMut, Attribute, FnArg, Ident, ItemFn}; +use syn::{visit_mut::VisitMut, Attribute, FnArg, ItemFn, Pat}; -use crate::{error::ErrorsVec, refident::MaybeIdent, utils::attr_is}; +use crate::{error::ErrorsVec, refident::MaybePat, utils::attr_is}; pub trait AttrBuilder { type Out; @@ -17,11 +17,11 @@ pub trait Validator { } } -impl AttrBuilder for () { - type Out = Ident; +impl AttrBuilder for () { + type Out = Pat; - fn build(_attr: Attribute, ident: &Ident) -> syn::Result { - Ok(ident.clone()) + fn build(_attr: Attribute, pat: &Pat) -> syn::Result { + Ok(pat.clone()) } } @@ -39,7 +39,7 @@ impl Validator for () {} /// the `name`: Only one attribute is allowed for arguments. pub struct JustOnceFnArgAttributeExtractor<'a, B = ()> where - B: AttrBuilder, + B: AttrBuilder, { name: &'a str, elements: Vec, @@ -55,7 +55,7 @@ impl<'a> From<&'a str> for JustOnceFnArgAttributeExtractor<'a, ()> { impl<'a, B> JustOnceFnArgAttributeExtractor<'a, B> where - B: AttrBuilder, + B: AttrBuilder, { pub fn new(name: &'a str) -> Self { Self { @@ -77,14 +77,13 @@ where impl VisitMut for JustOnceFnArgAttributeExtractor<'_, B> where - B: AttrBuilder, + B: AttrBuilder, B: Validator, { fn visit_fn_arg_mut(&mut self, node: &mut FnArg) { - let name = if let Some(name) = node.maybe_ident().cloned() { - name - } else { - return; + let pat = match node.maybe_pat() { + Some(pat) => pat.clone(), + None => return, }; if let FnArg::Typed(ref mut arg) = node { // Extract interesting attributes @@ -96,7 +95,7 @@ where let parsed = extracted .into_iter() - .map(|attr| B::build(attr.clone(), &name).map(|t| (attr, t))) + .map(|attr| B::build(attr.clone(), &pat).map(|t| (attr, t))) .collect::, _>>(); match parsed { @@ -185,7 +184,6 @@ where Ok(data) => match data.len() { 1 => match B::validate(item_fn) { Ok(_) => { - print!("DDDDD"); out = data.into_iter().next().map(|(_attr, t)| t); } Err(e) => { diff --git a/rstest_macros/src/parse/mod.rs b/rstest_macros/src/parse/mod.rs index 52f0405a..5fddb847 100644 --- a/rstest_macros/src/parse/mod.rs +++ b/rstest_macros/src/parse/mod.rs @@ -5,13 +5,13 @@ use syn::{ punctuated::Punctuated, token::{self, Async, Paren}, visit_mut::VisitMut, - FnArg, Ident, ItemFn, Token, + FnArg, Ident, ItemFn, Pat, Token, }; use crate::{ error::ErrorsVec, parse::just_once::{AttrBuilder, JustOnceFnAttributeExtractor, Validator}, - refident::{MaybeIdent, RefIdent}, + refident::{IntoPat, MaybeIdent, MaybePat}, utils::{attr_is, attr_starts_with}, }; use fixture::{ArgumentValue, FixtureModifiers, FixturesFunctionExtractor}; @@ -61,7 +61,7 @@ impl Parse for Attributes { #[derive(Debug, PartialEq, Clone)] pub(crate) enum Attribute { Attr(Ident), - Tagged(Ident, Vec), + Tagged(Ident, Vec), Type(Ident, Box), } @@ -82,6 +82,7 @@ impl Parse for Attribute { let _ = syn::parenthesized!(content in input); let args = Punctuated::::parse_terminated(&content)? .into_iter() + .map(IntoPat::into_pat) .collect(); Ok(Attribute::Tagged(tag, args)) @@ -137,15 +138,15 @@ impl Parse for Positional { #[derive(PartialEq, Debug, Clone)] pub(crate) struct Fixture { - pub(crate) name: Ident, - pub(crate) resolve: Option, + pub(crate) arg: Pat, + pub(crate) resolve: syn::Path, pub(crate) positional: Positional, } impl Fixture { - pub(crate) fn new(name: Ident, resolve: Option, positional: Positional) -> Self { + pub(crate) fn new(arg: Pat, resolve: syn::Path, positional: Positional) -> Self { Self { - name, + arg, resolve, positional, } @@ -166,7 +167,8 @@ impl Parse for Fixture { if input.peek(Token![as]) { let _: Token![as] = input.parse()?; - Ok(Self::new(input.parse()?, Some(resolve), positional)) + let ident: Ident = input.parse()?; + Ok(Self::new(ident.into_pat(), resolve, positional)) } else { let name = resolve.get_ident().ok_or_else(|| { syn::Error::new_spanned( @@ -174,7 +176,11 @@ impl Parse for Fixture { "Should be an ident".to_string(), ) })?; - Ok(Self::new(name.clone(), None, positional)) + Ok(Self::new( + name.clone().into_pat(), + name.clone().into(), + positional, + )) } } else { Err(syn::Error::new( @@ -185,15 +191,9 @@ impl Parse for Fixture { } } -impl RefIdent for Fixture { - fn ident(&self) -> &Ident { - &self.name - } -} - impl ToTokens for Fixture { fn to_tokens(&self, tokens: &mut TokenStream) { - self.name.to_tokens(tokens) + self.arg.to_tokens(tokens) } } @@ -209,10 +209,10 @@ pub(crate) fn extract_fixtures(item_fn: &mut ItemFn) -> Result, Err } pub(crate) fn extract_defaults(item_fn: &mut ItemFn) -> Result, ErrorsVec> { struct DefaultBuilder; - impl AttrBuilder for DefaultBuilder { + impl AttrBuilder for DefaultBuilder { type Out = ArgumentValue; - fn build(attr: syn::Attribute, name: &Ident) -> syn::Result { + fn build(attr: syn::Attribute, name: &Pat) -> syn::Result { attr.parse_args::() .map(|e| ArgumentValue::new(name.clone(), e)) } @@ -263,14 +263,13 @@ pub(crate) fn extract_once(item_fn: &mut ItemFn) -> Result( node: &mut FnArg, is_valid_attr: fn(&syn::Attribute) -> bool, - build: fn(syn::Attribute, &Ident) -> syn::Result, + build: impl Fn(syn::Attribute) -> syn::Result + 'a, ) -> Box> + 'a> { let name = node.maybe_ident().cloned(); if name.is_none() { return Box::new(std::iter::empty()); } - let name = name.unwrap(); if let FnArg::Typed(ref mut arg) = node { // Extract interesting attributes let attrs = std::mem::take(&mut arg.attrs); @@ -279,7 +278,7 @@ pub(crate) fn extract_argument_attrs<'a, B: 'a + std::fmt::Debug>( arg.attrs = remain; // Parse attrs - Box::new(extracted.into_iter().map(move |attr| build(attr, &name))) + Box::new(extracted.into_iter().map(move |attr| build(attr))) } else { Box::new(std::iter::empty()) } @@ -341,8 +340,29 @@ impl VisitMut for PartialsTypeFunctionExtractor { }; } } +/// Simple struct used to visit function attributes and extract case arguments and +/// eventualy parsing errors +#[derive(Default)] +struct CaseArgsFunctionExtractor(Vec, Vec); -pub(crate) fn extract_case_args(item_fn: &mut ItemFn) -> Result, ErrorsVec> { +impl VisitMut for CaseArgsFunctionExtractor { + fn visit_fn_arg_mut(&mut self, node: &mut FnArg) { + let pat = match node.maybe_pat() { + Some(pat) => pat.clone(), + None => return, + }; + for r in extract_argument_attrs(node, |a| attr_is(a, "case"), |_| Ok(())) { + match r { + Ok(_) => self.0.push(pat.clone()), + Err(err) => self.1.push(err), + } + } + + syn::visit_mut::visit_fn_arg_mut(self, node); + } +} + +pub(crate) fn extract_case_args(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut extractor = JustOnceFnArgAttributeExtractor::from("case"); extractor.visit_item_fn_mut(item_fn); @@ -394,10 +414,10 @@ pub(crate) fn extract_cases(item_fn: &mut ItemFn) -> Result, Error pub(crate) fn extract_value_list(item_fn: &mut ItemFn) -> Result, ErrorsVec> { struct ValueListBuilder; - impl AttrBuilder for ValueListBuilder { + impl AttrBuilder for ValueListBuilder { type Out = ValueList; - fn build(attr: syn::Attribute, extra: &Ident) -> syn::Result { + fn build(attr: syn::Attribute, extra: &Pat) -> syn::Result { attr.parse_args::().map(|v| ValueList { arg: extra.clone(), values: v.take().into_iter().map(|e| e.into()).collect(), @@ -414,15 +434,15 @@ pub(crate) fn extract_value_list(item_fn: &mut ItemFn) -> Result, /// Simple struct used to visit function args attributes to extract the /// excluded ones and eventualy parsing errors -struct ExcludedTraceAttributesFunctionExtractor(Result, ErrorsVec>); -impl From, ErrorsVec>> for ExcludedTraceAttributesFunctionExtractor { - fn from(inner: Result, ErrorsVec>) -> Self { +struct ExcludedTraceAttributesFunctionExtractor(Result, ErrorsVec>); +impl From, ErrorsVec>> for ExcludedTraceAttributesFunctionExtractor { + fn from(inner: Result, ErrorsVec>) -> Self { Self(inner) } } impl ExcludedTraceAttributesFunctionExtractor { - pub(crate) fn take(self) -> Result, ErrorsVec> { + pub(crate) fn take(self) -> Result, ErrorsVec> { self.0 } @@ -433,7 +453,7 @@ impl ExcludedTraceAttributesFunctionExtractor { } } - fn update_excluded(&mut self, value: Ident) { + fn update_excluded(&mut self, value: Pat) { if let Some(inner) = self.0.iter_mut().next() { inner.push(value); } @@ -448,11 +468,13 @@ impl Default for ExcludedTraceAttributesFunctionExtractor { impl VisitMut for ExcludedTraceAttributesFunctionExtractor { fn visit_fn_arg_mut(&mut self, node: &mut FnArg) { - for r in - extract_argument_attrs(node, |a| attr_is(a, "notrace"), |_a, name| Ok(name.clone())) - { + let pat = match node.maybe_pat().cloned() { + Some(pat) => pat, + None => return, + }; + for r in extract_argument_attrs(node, |a| attr_is(a, "notrace"), |_a| Ok(())) { match r { - Ok(value) => self.update_excluded(value), + Ok(_) => self.update_excluded(pat.clone()), Err(err) => self.update_error(err.into()), } } @@ -461,7 +483,7 @@ impl VisitMut for ExcludedTraceAttributesFunctionExtractor { } } -pub(crate) fn extract_excluded_trace(item_fn: &mut ItemFn) -> Result, ErrorsVec> { +pub(crate) fn extract_excluded_trace(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut excluded_trace_extractor = ExcludedTraceAttributesFunctionExtractor::default(); excluded_trace_extractor.visit_item_fn_mut(item_fn); excluded_trace_extractor.take() diff --git a/rstest_macros/src/parse/rstest.rs b/rstest_macros/src/parse/rstest.rs index a233d62e..d058cb12 100644 --- a/rstest_macros/src/parse/rstest.rs +++ b/rstest_macros/src/parse/rstest.rs @@ -1,6 +1,6 @@ use syn::{ parse::{Parse, ParseStream}, - Ident, ItemFn, Token, + Ident, ItemFn, Pat, Token, }; use self::files::{extract_files, ValueListFromFiles}; @@ -16,11 +16,8 @@ use super::{ testcase::TestCase, Attribute, Attributes, ExtendWithFunctionAttrs, Fixture, }; -use crate::parse::vlist::ValueList; -use crate::{ - error::ErrorsVec, - refident::{MaybeIdent, RefIdent}, -}; +use crate::{error::ErrorsVec, refident::IntoPat}; +use crate::{parse::vlist::ValueList, refident::MaybePat}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, ToTokens}; @@ -66,6 +63,8 @@ impl ExtendWithFunctionAttrs for RsTestInfo { self.arguments.set_futures(futures.into_iter()); self.arguments.set_by_refs(by_refs.into_iter()); self.arguments.set_ignores(ignores.into_iter()); + self.arguments + .register_inner_destructored_idents_names(item_fn); Ok(()) } } @@ -76,7 +75,7 @@ pub(crate) struct RsTestData { } impl RsTestData { - pub(crate) fn case_args(&self) -> impl Iterator { + pub(crate) fn case_args(&self) -> impl Iterator { self.items.iter().filter_map(|it| match it { RsTestItem::CaseArgName(ref arg) => Some(arg), _ => None, @@ -162,20 +161,43 @@ impl ExtendWithFunctionAttrs for RsTestData { #[derive(PartialEq, Debug)] pub(crate) enum RsTestItem { Fixture(Fixture), - CaseArgName(Ident), + CaseArgName(Pat), TestCase(TestCase), ValueList(ValueList), } +impl MaybePat for Fixture { + fn maybe_pat(&self) -> Option<&syn::Pat> { + Some(&self.arg) + } +} + +impl MaybePat for RsTestItem { + fn maybe_pat(&self) -> Option<&syn::Pat> { + match self { + RsTestItem::Fixture(f) => f.maybe_pat(), + RsTestItem::CaseArgName(c) => Some(c), + RsTestItem::TestCase(_) => None, + RsTestItem::ValueList(vl) => Some(&vl.arg), + } + } +} + impl From for RsTestItem { fn from(f: Fixture) -> Self { RsTestItem::Fixture(f) } } +impl From for RsTestItem { + fn from(pat: Pat) -> Self { + RsTestItem::CaseArgName(pat) + } +} + impl From for RsTestItem { fn from(ident: Ident) -> Self { - RsTestItem::CaseArgName(ident) + RsTestItem::CaseArgName(ident.into_pat()) } } @@ -200,25 +222,16 @@ impl Parse for RsTestItem { } else if input.fork().parse::().is_ok() { input.parse::().map(RsTestItem::Fixture) } else if input.fork().parse::().is_ok() { - input.parse::().map(RsTestItem::CaseArgName) + input + .parse::() + .map(IntoPat::into_pat) + .map(RsTestItem::CaseArgName) } else { Err(syn::Error::new(Span::call_site(), "Cannot parse it")) } } } -impl MaybeIdent for RsTestItem { - fn maybe_ident(&self) -> Option<&Ident> { - use RsTestItem::*; - match self { - Fixture(ref fixture) => Some(fixture.ident()), - CaseArgName(ref case_arg) => Some(case_arg), - ValueList(ref value_list) => Some(value_list.ident()), - TestCase(_) => None, - } - } -} - impl ToTokens for RsTestItem { fn to_tokens(&self, tokens: &mut TokenStream) { use RsTestItem::*; @@ -237,18 +250,18 @@ impl RsTestAttributes { const TRACE_VARIABLE_ATTR: &'static str = "trace"; const NOTRACE_VARIABLE_ATTR: &'static str = "notrace"; - pub(crate) fn trace_me(&self, ident: &Ident) -> bool { + pub(crate) fn trace_me(&self, pat: &Pat) -> bool { if self.should_trace() { - !self.iter().any(|m| Self::is_notrace(ident, m)) + !self.iter().any(|m| Self::is_notrace(pat, m)) } else { false } } - fn is_notrace(ident: &Ident, m: &Attribute) -> bool { + fn is_notrace(pat: &Pat, m: &Attribute) -> bool { match m { Attribute::Tagged(i, args) if i == Self::NOTRACE_VARIABLE_ATTR => { - args.iter().any(|a| a == ident) + args.iter().any(|a| a == pat) } _ => false, } @@ -262,7 +275,7 @@ impl RsTestAttributes { self.inner.attributes.push(Attribute::Attr(trace)); } - pub(crate) fn add_notraces(&mut self, notraces: Vec) { + pub(crate) fn add_notraces(&mut self, notraces: Vec) { if notraces.is_empty() { return; } @@ -521,9 +534,9 @@ mod test { info.extend_with_function_attrs(&mut item_fn).unwrap(); info.attributes.add_trace(ident("trace")); - assert!(!info.attributes.trace_me(&ident("a"))); - assert!(info.attributes.trace_me(&ident("b"))); - assert!(!info.attributes.trace_me(&ident("c"))); + assert!(!info.attributes.trace_me(&pat("a"))); + assert!(info.attributes.trace_me(&pat("b"))); + assert!(!info.attributes.trace_me(&pat("c"))); let b_args = item_fn .sig .inputs @@ -547,8 +560,8 @@ mod test { info.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!(item_fn, expected); - assert!(info.arguments.is_future(&ident("a"))); - assert!(!info.arguments.is_future(&ident("b"))); + assert!(info.arguments.is_future(&pat("a"))); + assert!(!info.arguments.is_future(&pat("b"))); } } @@ -564,7 +577,7 @@ mod test { assert_eq!(1, args.len()); assert_eq!(1, cases.len()); - assert_eq!("arg", &args[0].to_string()); + assert_eq!("arg", &args[0].display_code()); assert_eq!(to_args!(["42"]), cases[0].args()) } @@ -587,7 +600,7 @@ mod test { assert_eq!( to_strs!(vec!["arg1", "arg2", "arg3"]), data.case_args() - .map(ToString::to_string) + .map(DisplayCode::display_code) .collect::>() ); @@ -618,7 +631,7 @@ mod test { let case_args = info.data.case_args().cloned().collect::>(); let cases = info.data.cases().cloned().collect::>(); - assert_eq!(to_idents!(["arg1", "arg2"]), case_args); + assert_eq!(to_pats!(["arg1", "arg2"]), case_args); assert_eq!( vec![ TestCase::from_iter(["42", r#""first""#].iter()).with_description("first"), @@ -627,6 +640,40 @@ mod test { ); } + #[test] + fn destruct_case() { + let mut item_fn: ItemFn = r#" + #[case::destruct(T::new(2, 21))] + fn test_fn(#[case] T{a, b}: T) { + } + "# + .ast(); + + let mut info = RsTestInfo::default(); + + info.extend_with_function_attrs(&mut item_fn).unwrap(); + + let case_args = info.data.case_args().cloned().collect::>(); + let cases = info.data.cases().cloned().collect::>(); + + // Should just remove attributes + assert_eq!( + to_fnargs!(["T{a, b}: T"]), + item_fn.sig.inputs.into_iter().collect::>() + ); + assert_eq!(to_pats!(["T{a, b}"]), case_args); + assert_eq!( + vec![ + TestCase::from_iter(["T::new(2, 21)"].iter()).with_description("destruct"), + ], + cases + ); + assert_eq!( + info.arguments.inner_pat(&pat("T{a, b}")), + &pat("__destruct_1") + ); + } + #[test] fn parse_tuple_value() { let mut item_fn = r#" @@ -666,7 +713,7 @@ mod test { let case_args = info.data.case_args().cloned().collect::>(); let cases = info.data.cases().cloned().collect::>(); - assert_eq!(to_idents!(["arg1", "arg2"]), case_args); + assert_eq!(to_pats!(["arg1", "arg2"]), case_args); assert_eq!( vec![ TestCase::from_iter(["42", r#""first""#].iter()).with_description("first"), @@ -776,7 +823,7 @@ mod test { assert_eq!(1, args.len()); assert_eq!(1, cases.len()); - assert_eq!("arg", &args[0].to_string()); + assert_eq!("arg", &args[0].display_code()); assert_eq!(to_args!(["42"]), cases[0].args()) } @@ -802,7 +849,7 @@ mod test { ) .data; - assert_eq!("case", &data.case_args().next().unwrap().to_string()); + assert_eq!("case", &data.case_args().next().unwrap().display_code()); let cases = data.cases().collect::>(); @@ -924,6 +971,43 @@ mod test { list_values[1].args() ); } + + #[test] + fn destruct() { + let mut item_fn = r#" + fn test_fn(#[values(S(1,2), S(3,4))] S(a,b): S, #[values(T::new("a", "b"), T{s: "a" ,t: "c" })] T{s, t}: T) { + } + "# + .ast(); + + let mut info = RsTestInfo::default(); + + info.extend_with_function_attrs(&mut item_fn).unwrap(); + + let list_values = info.data.list_values().cloned().collect::>(); + + // Should just remove attributes + assert_eq!( + to_fnargs!(["S(a, b): S", "T{s, t}: T"]), + item_fn.sig.inputs.into_iter().collect::>() + ); + assert_eq!(2, list_values.len()); + assert_eq!(list_values[0].arg, pat("S(a, b)")); + assert_eq!(to_args!(["S(1,2)", "S(3,4)"]), list_values[0].args()); + assert_eq!(list_values[1].arg, pat("T{s, t}")); + assert_eq!( + to_args!([r#"T::new("a", "b")"#, r#"T{s: "a" ,t: "c" }"#]), + list_values[1].args() + ); + assert_eq!( + info.arguments.inner_pat(&pat("S(a, b)")), + &pat("__destruct_1") + ); + assert_eq!( + info.arguments.inner_pat(&pat("T{s, t}")), + &pat("__destruct_2") + ); + } } #[test] @@ -968,7 +1052,7 @@ mod test { assert_eq!( to_strs!(vec!["u", "a", "d"]), data.case_args() - .map(ToString::to_string) + .map(DisplayCode::display_code) .collect::>() ); diff --git a/rstest_macros/src/parse/rstest/files.rs b/rstest_macros/src/parse/rstest/files.rs index 62af21d5..35c259cd 100644 --- a/rstest_macros/src/parse/rstest/files.rs +++ b/rstest_macros/src/parse/rstest/files.rs @@ -12,7 +12,7 @@ use crate::{ extract_argument_attrs, vlist::{Value, ValueList}, }, - refident::MaybeIdent, + refident::{IntoPat, MaybeIdent}, utils::attr_is, }; @@ -168,7 +168,7 @@ impl ValueFilesExtractor { &mut self, node: &mut FnArg, is_valid_attr: fn(&syn::Attribute) -> bool, - build: fn(syn::Attribute, &Ident) -> syn::Result, + build: impl Fn(syn::Attribute) -> syn::Result + 'a, ) -> Vec { self.collect_errors( extract_argument_attrs(node, is_valid_attr, build).collect::, _>>(), @@ -176,14 +176,14 @@ impl ValueFilesExtractor { } fn extract_files(&mut self, node: &mut FnArg) -> Vec { - self.extract_argument_attrs(node, |a| attr_is(a, "files"), |attr, _| attr.try_into()) + self.extract_argument_attrs(node, |a| attr_is(a, "files"), |attr| attr.try_into()) } fn extract_exclude(&mut self, node: &mut FnArg) -> Vec { self.extract_argument_attrs( node, |a| attr_is(a, "exclude"), - |attr, _| Exclude::try_from(attr), + |attr| Exclude::try_from(attr), ) } @@ -191,7 +191,7 @@ impl ValueFilesExtractor { self.extract_argument_attrs( node, |a| attr_is(a, "include_dot_files"), - |attr, _| { + |attr| { attr.meta .require_path_only() .map_err(|_| attr.error("Use #[include_dot_files] to include dot files"))?; @@ -296,8 +296,10 @@ impl<'a> ValueListFromFiles<'a> { files .into_iter() .map(|(arg, refs)| { - self.file_list_values(refs) - .map(|values| ValueList { arg, values }) + self.file_list_values(refs).map(|values| ValueList { + arg: arg.into_pat(), + values, + }) }) .collect::, _>>() } diff --git a/rstest_macros/src/parse/vlist.rs b/rstest_macros/src/parse/vlist.rs index 2fe04f77..eb72e90a 100644 --- a/rstest_macros/src/parse/vlist.rs +++ b/rstest_macros/src/parse/vlist.rs @@ -2,10 +2,10 @@ use proc_macro2::TokenStream; use quote::ToTokens; use syn::{ parse::{Parse, ParseStream, Result}, - Expr, Ident, Token, + Expr, Ident, Pat, Token, }; -use crate::refident::RefIdent; +use crate::refident::IntoPat; use super::expressions::Expressions; @@ -35,20 +35,20 @@ impl From for Value { #[derive(Debug, PartialEq, Clone)] pub(crate) struct ValueList { - pub(crate) arg: Ident, + pub(crate) arg: Pat, pub(crate) values: Vec, } impl Parse for ValueList { fn parse(input: ParseStream) -> Result { - let arg = input.parse()?; + let ident: Ident = input.parse()?; let _to: Token![=>] = input.parse()?; let content; let paren = syn::bracketed!(content in input); let values: Expressions = content.parse()?; let ret = Self { - arg, + arg: ident.into_pat(), values: values.take().into_iter().map(|e| e.into()).collect(), }; if ret.values.is_empty() { @@ -62,12 +62,6 @@ impl Parse for ValueList { } } -impl RefIdent for ValueList { - fn ident(&self) -> &Ident { - &self.arg - } -} - impl ToTokens for ValueList { fn to_tokens(&self, tokens: &mut TokenStream) { self.arg.to_tokens(tokens) @@ -103,7 +97,7 @@ mod should { .join(", ") )); - assert_eq!(name, &values_list.arg.to_string()); + assert_eq!(name, &values_list.arg.display_code()); assert_eq!(values_list.args(), to_args!(literals)); } diff --git a/rstest_macros/src/refident.rs b/rstest_macros/src/refident.rs index a7e0015e..45be6e9f 100644 --- a/rstest_macros/src/refident.rs +++ b/rstest_macros/src/refident.rs @@ -1,7 +1,7 @@ /// Provide `RefIdent` and `MaybeIdent` traits that give a shortcut to extract identity reference /// (`syn::Ident` struct). use proc_macro2::Ident; -use syn::{FnArg, Pat, PatType, Type}; +use syn::{FnArg, Pat, PatIdent, PatType, Type}; pub trait RefIdent { /// Return the reference to ident if any @@ -99,6 +99,30 @@ impl MaybeIdent for crate::parse::Attribute { } } +pub trait MaybeIntoPath { + fn maybe_into_path(self) -> Option; +} + +impl MaybeIntoPath for PatIdent { + fn maybe_into_path(self) -> Option { + Some(self.ident.into()) + } +} + +impl MaybeIntoPath for Pat { + fn maybe_into_path(self) -> Option { + match self { + Pat::Ident(pi) => pi.maybe_into_path(), + _ => None, + } + } +} + +pub trait RefPat { + /// Return the reference to ident if any + fn pat(&self) -> &Pat; +} + pub trait MaybePatIdent { fn maybe_patident(&self) -> Option<&syn::PatIdent>; } @@ -115,6 +139,54 @@ impl MaybePatIdent for FnArg { } } +impl MaybePatIdent for Pat { + fn maybe_patident(&self) -> Option<&syn::PatIdent> { + match self { + Pat::Ident(ident) => Some(ident), + _ => None, + } + } +} + +pub trait MaybePatType { + fn maybe_pat_type(&self) -> Option<&syn::PatType>; +} + +impl MaybePatType for FnArg { + fn maybe_pat_type(&self) -> Option<&syn::PatType> { + match self { + FnArg::Typed(pt) => Some(pt), + _ => None, + } + } +} + +pub trait MaybePatTypeMut { + fn maybe_pat_type_mut(&mut self) -> Option<&mut syn::PatType>; +} + +impl MaybePatTypeMut for FnArg { + fn maybe_pat_type_mut(&mut self) -> Option<&mut syn::PatType> { + match self { + FnArg::Typed(pt) => Some(pt), + _ => None, + } + } +} + +pub trait MaybePat { + fn maybe_pat(&self) -> Option<&syn::Pat>; +} + +impl MaybePat for FnArg { + fn maybe_pat(&self) -> Option<&syn::Pat> { + match self { + FnArg::Typed(PatType { pat, .. }) => Some(pat.as_ref()), + _ => None, + } + } +} + pub trait RemoveMutability { fn remove_mutability(&mut self); } @@ -128,3 +200,19 @@ impl RemoveMutability for FnArg { }; } } + +pub trait IntoPat { + fn into_pat(self) -> Pat; +} + +impl IntoPat for Ident { + fn into_pat(self) -> Pat { + Pat::Ident(syn::PatIdent { + attrs: vec![], + by_ref: None, + mutability: None, + ident: self, + subpat: None, + }) + } +} diff --git a/rstest_macros/src/render/apply_argumets.rs b/rstest_macros/src/render/apply_argumets.rs index af7a57ff..e8fa8ae0 100644 --- a/rstest_macros/src/render/apply_argumets.rs +++ b/rstest_macros/src/render/apply_argumets.rs @@ -3,21 +3,35 @@ use syn::{parse_quote, FnArg, Generics, Ident, ItemFn, Lifetime, Signature, Type use crate::{ parse::{arguments::ArgumentsInfo, future::MaybeFutureImplType}, - refident::{MaybeIdent, MaybePatIdent, RemoveMutability}, + refident::{MaybeIdent, MaybePat, MaybePatIdent, RemoveMutability}, }; -pub(crate) trait ApplyArgumets { - fn apply_argumets(&mut self, arguments: &ArgumentsInfo) -> R; +pub(crate) trait ApplyArguments { + type Output: Sized; + type Context; + + fn apply_argumets( + &mut self, + arguments: &mut ArgumentsInfo, + ctx: &mut Self::Context, + ) -> Self::Output; } -impl ApplyArgumets> for FnArg { - fn apply_argumets(&mut self, arguments: &ArgumentsInfo) -> Option { +impl ApplyArguments for FnArg { + type Output = Option; + type Context = usize; + + fn apply_argumets( + &mut self, + arguments: &mut ArgumentsInfo, + anoymous_id: &mut usize, + ) -> Self::Output { if self - .maybe_ident() + .maybe_pat() .map(|id| arguments.is_future(id)) .unwrap_or_default() { - self.impl_future_arg() + self.impl_future_arg(anoymous_id) } else { None } @@ -42,12 +56,16 @@ fn extend_generics_with_lifetimes<'a, 'b>( } } -impl ApplyArgumets for Signature { - fn apply_argumets(&mut self, arguments: &ArgumentsInfo) { +impl ApplyArguments for Signature { + type Output = (); + type Context = (); + + fn apply_argumets(&mut self, arguments: &mut ArgumentsInfo, _: &mut ()) { + let mut anonymous_lt = 0_usize; let new_lifetimes = self .inputs .iter_mut() - .filter_map(|arg| arg.apply_argumets(arguments)) + .filter_map(|arg| arg.apply_argumets(arguments, &mut anonymous_lt)) .collect::>(); if !new_lifetimes.is_empty() || !self.generics.params.is_empty() { let new_generics = @@ -57,14 +75,18 @@ impl ApplyArgumets for Signature { } } -impl ApplyArgumets for ItemFn { - fn apply_argumets(&mut self, arguments: &ArgumentsInfo) { - let rebound_awaited_args = self - .sig - .inputs +impl ApplyArguments for ItemFn { + type Output = (); + type Context = (); + + fn apply_argumets(&mut self, arguments: &mut ArgumentsInfo, _: &mut ()) { + let args = self.sig.inputs.iter().cloned().collect::>(); + self.sig.apply_argumets(arguments, &mut ()); + let rebound_awaited_args = args .iter() - .filter_map(|a| a.maybe_patident()) - .filter(|p| arguments.is_future_await(&p.ident)) + .filter_map(MaybePat::maybe_pat) + .filter(|p| arguments.is_future_await(&p)) + .filter_map(MaybePatIdent::maybe_patident) .map(|p| { let a = &p.ident; quote::quote! { let #p = #a.await; } @@ -76,20 +98,25 @@ impl ApplyArgumets for ItemFn { #orig_block_impl } }; - self.sig.apply_argumets(arguments); } } pub(crate) trait ImplFutureArg { - fn impl_future_arg(&mut self) -> Option; + fn impl_future_arg(&mut self, anonymous_lt: &mut usize) -> Option; } impl ImplFutureArg for FnArg { - fn impl_future_arg(&mut self) -> Option { - let lifetime_id = self.maybe_ident().map(|id| format_ident!("_{}", id)); + fn impl_future_arg(&mut self, anonymous_lt: &mut usize) -> Option { + let lifetime_id = self + .maybe_ident() + .map(|id| format_ident!("_{}", id)) + .unwrap_or_else(|| { + *anonymous_lt += 1; + format_ident!("_anonymous_lt_{}", anonymous_lt) + }); match self.as_mut_future_impl_type() { Some(ty) => { - let lifetime = lifetime_id.and_then(|id| update_type_with_lifetime(ty, id)); + let lifetime = update_type_with_lifetime(ty, lifetime_id); *ty = parse_quote! { impl std::future::Future }; @@ -128,8 +155,9 @@ mod should { fn no_change(#[case] item_fn: &str) { let mut item_fn: ItemFn = item_fn.ast(); let orig = item_fn.clone(); + let mut args = ArgumentsInfo::default(); - item_fn.sig.apply_argumets(&ArgumentsInfo::default()); + item_fn.sig.apply_argumets(&mut args, &mut ()); assert_eq!(orig, item_fn) } @@ -174,9 +202,9 @@ mod should { let mut arguments = ArgumentsInfo::default(); futures .into_iter() - .for_each(|&f| arguments.add_future(ident(f))); + .for_each(|&f| arguments.add_future(pat(f))); - item_fn.sig.apply_argumets(&arguments); + item_fn.sig.apply_argumets(&mut arguments, &mut ()); assert_eq!(expected, item_fn) } @@ -208,9 +236,9 @@ mod should { let mut arguments = ArgumentsInfo::default(); futures .into_iter() - .for_each(|&f| arguments.add_future(ident(f))); + .for_each(|&f| arguments.add_future(pat(f))); - item_fn.sig.apply_argumets(&arguments); + item_fn.sig.apply_argumets(&mut arguments, &mut ()); assert_eq!(expected, item_fn) } @@ -227,10 +255,10 @@ mod should { let mut item_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut arguments: ArgumentsInfo = Default::default(); arguments.set_global_await(true); - arguments.add_future(ident("a")); - arguments.add_future(ident("b")); + arguments.add_future(pat("a")); + arguments.add_future(pat("b")); - item_fn.apply_argumets(&arguments); + item_fn.apply_argumets(&mut arguments, &mut ()); let code = item_fn.block.display_code(); @@ -243,10 +271,10 @@ mod should { fn with_selective_await() { let mut item_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut arguments: ArgumentsInfo = Default::default(); - arguments.set_future(ident("a"), FutureArg::Define); - arguments.set_future(ident("b"), FutureArg::Await); + arguments.set_future(pat("a"), FutureArg::Define); + arguments.set_future(pat("b"), FutureArg::Await); - item_fn.apply_argumets(&arguments); + item_fn.apply_argumets(&mut arguments, &mut ()); let code = item_fn.block.display_code(); @@ -259,9 +287,10 @@ mod should { fn with_mut_await() { let mut item_fn: ItemFn = r#"fn test(mut a: i32) {} "#.ast(); let mut arguments: ArgumentsInfo = Default::default(); - arguments.set_future(ident("a"), FutureArg::Await); - item_fn.apply_argumets(&arguments); + arguments.set_future(pat("a").with_mut(), FutureArg::Await); + + item_fn.apply_argumets(&mut arguments, &mut ()); let code = item_fn.block.display_code(); assert_in!(code, mut_await_argument_code_string("a")); diff --git a/rstest_macros/src/render/fixture.rs b/rstest_macros/src/render/fixture.rs index b12aac30..d0cfd08c 100644 --- a/rstest_macros/src/render/fixture.rs +++ b/rstest_macros/src/render/fixture.rs @@ -1,12 +1,13 @@ use proc_macro2::{Span, TokenStream}; -use syn::{parse_quote, Ident, ItemFn, ReturnType}; +use syn::token::Async; +use syn::{parse_quote, FnArg, Generics, Ident, ItemFn, ReturnType}; use quote::quote; -use super::apply_argumets::ApplyArgumets; +use super::apply_argumets::ApplyArguments; use super::{inject, render_exec_call}; +use crate::refident::MaybeIdent; use crate::resolver::{self, Resolver}; -use crate::utils::{fn_args, fn_args_idents}; use crate::{parse::fixture::FixtureInfo, utils::generics_clean_up}; fn wrap_return_type_as_static_ref(rt: ReturnType) -> ReturnType { @@ -33,12 +34,19 @@ fn wrap_call_impl_with_call_once_impl(call_impl: TokenStream, rt: &ReturnType) - } pub(crate) fn render(mut fixture: ItemFn, info: FixtureInfo) -> TokenStream { - fixture.apply_argumets(&info.arguments); + let mut arguments = info.arguments.clone(); + fixture.apply_argumets(&mut arguments, &mut ()); let name = &fixture.sig.ident; let asyncness = &fixture.sig.asyncness.clone(); - let vargs = fn_args_idents(&fixture).cloned().collect::>(); - let args = &vargs; - let orig_args = &fixture.sig.inputs; + let inner_args = info + .arguments + .replace_fn_args_with_related_inner_pat(fixture.sig.inputs.iter().cloned()) + .collect::>(); + let args_ident = inner_args + .iter() + .filter_map(MaybeIdent::maybe_ident) + .cloned() + .collect::>(); let orig_attrs = &fixture.attrs; let generics = &fixture.sig.generics; let mut default_output = info @@ -52,7 +60,7 @@ pub(crate) fn render(mut fixture: ItemFn, info: FixtureInfo) -> TokenStream { let mut output = fixture.sig.output.clone(); let visibility = &fixture.vis; let resolver = ( - resolver::fixtures::get(info.data.fixtures()), + resolver::fixtures::get(&info.arguments, info.data.fixtures()), resolver::values::get(info.data.values()), ); let generics_idents = generics @@ -60,12 +68,21 @@ pub(crate) fn render(mut fixture: ItemFn, info: FixtureInfo) -> TokenStream { .map(|tp| &tp.ident) .cloned() .collect::>(); - let inject = inject::resolve_aruments(fixture.sig.inputs.iter(), &resolver, &generics_idents); - - let partials = - (1..=orig_args.len()).map(|n| render_partial_impl(&fixture, n, &resolver, &info)); + let inject = inject::resolve_aruments(inner_args.iter(), &resolver, &generics_idents); + + let partials = (1..=inner_args.len()).map(|n| { + render_partial_impl( + &inner_args, + &fixture.sig.output, + &fixture.sig.generics, + fixture.sig.asyncness.as_ref(), + n, + &resolver, + &info, + ) + }); - let args = args + let args = args_ident .iter() .map(|arg| parse_quote! { #arg }) .collect::>(); @@ -85,7 +102,7 @@ pub(crate) fn render(mut fixture: ItemFn, info: FixtureInfo) -> TokenStream { impl #name { #(#orig_attrs)* #[allow(unused_mut)] - pub #asyncness fn get #generics (#orig_args) #output #where_clause { + pub #asyncness fn get #generics (#(#inner_args),*) #output #where_clause { #call_impl } @@ -103,7 +120,10 @@ pub(crate) fn render(mut fixture: ItemFn, info: FixtureInfo) -> TokenStream { } fn render_partial_impl( - fixture: &ItemFn, + args: &[FnArg], + output: &ReturnType, + generics: &Generics, + asyncness: Option<&Async>, n: usize, resolver: &impl Resolver, info: &FixtureInfo, @@ -111,26 +131,27 @@ fn render_partial_impl( let mut output = info .attributes .extract_partial_type(n) - .unwrap_or_else(|| fixture.sig.output.clone()); + .unwrap_or_else(|| output.clone()); if info.arguments.is_once() { output = wrap_return_type_as_static_ref(output); } - let generics = generics_clean_up(&fixture.sig.generics, fn_args(fixture).take(n), &output); + let generics = generics_clean_up(generics, args.iter().take(n), &output); let where_clause = &generics.where_clause; - let asyncness = &fixture.sig.asyncness; + let asyncness = asyncness; let genercs_idents = generics .type_params() .map(|tp| &tp.ident) .cloned() .collect::>(); - let inject = - inject::resolve_aruments(fixture.sig.inputs.iter().skip(n), resolver, &genercs_idents); + let inject = inject::resolve_aruments(args.iter().skip(n), resolver, &genercs_idents); - let sign_args = fn_args(fixture).take(n); - let fixture_args = fn_args_idents(fixture) + let sign_args = args.iter().take(n); + let fixture_args = args + .iter() + .filter_map(MaybeIdent::maybe_ident) .map(|arg| parse_quote! {#arg}) .collect::>(); let name = Ident::new(&format!("partial_{n}"), Span::call_site()); @@ -502,8 +523,8 @@ mod should { r#"async fn test(async_ref_u32: &u32, async_u32: u32,simple: u32) { }"#.ast(); let mut arguments = ArgumentsInfo::default(); - arguments.add_future(ident("async_ref_u32")); - arguments.add_future(ident("async_u32")); + arguments.add_future(pat("async_ref_u32")); + arguments.add_future(pat("async_u32")); let tokens = render( item_fn.clone(), @@ -536,8 +557,8 @@ mod should { let item_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut arguments: ArgumentsInfo = Default::default(); arguments.set_global_await(true); - arguments.add_future(ident("a")); - arguments.add_future(ident("b")); + arguments.add_future(pat("a")); + arguments.add_future(pat("b")); let tokens = render( item_fn.clone(), @@ -559,8 +580,8 @@ mod should { fn use_selective_await() { let item_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut arguments: ArgumentsInfo = Default::default(); - arguments.set_future(ident("a"), FutureArg::Define); - arguments.set_future(ident("b"), FutureArg::Await); + arguments.set_future(pat("a"), FutureArg::Define); + arguments.set_future(pat("b"), FutureArg::Await); let tokens = render( item_fn.clone(), diff --git a/rstest_macros/src/render/inject.rs b/rstest_macros/src/render/inject.rs index b052e052..ccd20e8c 100644 --- a/rstest_macros/src/render/inject.rs +++ b/rstest_macros/src/render/inject.rs @@ -2,10 +2,10 @@ use std::borrow::Cow; use proc_macro2::TokenStream; use quote::quote; -use syn::{parse_quote, Expr, FnArg, Ident, Stmt, Type}; +use syn::{parse_quote, Expr, FnArg, Ident, Pat, Stmt, Type}; use crate::{ - refident::{MaybeIdent, MaybeType}, + refident::{IntoPat, MaybeIdent, MaybePat, MaybeType}, render::crate_resolver::crate_name, resolver::Resolver, utils::{fn_arg_mutability, IsLiteralExpression}, @@ -44,18 +44,18 @@ where } fn resolve(&self, arg: &FnArg) -> Option { - let ident = arg.maybe_ident()?; + let pat = arg.maybe_pat()?; let mutability = fn_arg_mutability(arg); let unused_mut: Option = mutability .as_ref() .map(|_| parse_quote! {#[allow(unused_mut)]}); let arg_type = arg.maybe_type()?; - let fixture_name = self.fixture_name(ident); + let fixture_name = self.fixture_name(pat); let mut fixture = self .resolver - .resolve(ident) - .or_else(|| self.resolver.resolve(&fixture_name)) + .resolve(pat) + .or_else(|| self.resolver.resolve(&fixture_name.clone().into_pat())) .unwrap_or_else(|| default_fixture_resolve(&fixture_name)); if fixture.is_literal() && self.type_can_be_get_from_literal_str(arg_type) { @@ -63,16 +63,20 @@ where } Some(parse_quote! { #unused_mut - let #mutability #ident = #fixture; + let #pat = #fixture; }) } - fn fixture_name<'a>(&self, ident: &'a Ident) -> Cow<'a, Ident> { + fn fixture_name<'a>(&self, ident: &'a Pat) -> Ident { + let ident = ident + .maybe_ident() + .cloned() + .expect("BUG: Here all arguments should be PatIdent types"); let id_str = ident.to_string(); if id_str.starts_with('_') && !id_str.starts_with("__") { - Cow::Owned(Ident::new(&id_str[1..], ident.span())) + Ident::new(&id_str[1..], ident.span()) } else { - Cow::Borrowed(ident) + ident } } @@ -150,7 +154,7 @@ mod should { ) { let arg = arg_str.ast(); let mut resolver = std::collections::HashMap::new(); - resolver.insert(rule.0.to_owned(), &rule.1); + resolver.insert(pat(rule.0), &rule.1); let injected = ArgumentResolver::new(&resolver, &[]).resolve(&arg).unwrap(); @@ -192,7 +196,7 @@ mod should { let mut resolver = std::collections::HashMap::new(); let expr = expr(r#""value to convert""#); - resolver.insert(arg.maybe_ident().unwrap().to_string(), &expr); + resolver.insert(arg.maybe_pat().unwrap().clone(), &expr); let ag = ArgumentResolver { resolver: &resolver, diff --git a/rstest_macros/src/render/mod.rs b/rstest_macros/src/render/mod.rs index 5155109d..f6bfe6fb 100644 --- a/rstest_macros/src/render/mod.rs +++ b/rstest_macros/src/render/mod.rs @@ -8,14 +8,15 @@ use std::collections::HashMap; use syn::token::Async; use proc_macro2::{Span, TokenStream}; -use syn::{parse_quote, Attribute, Expr, FnArg, Ident, ItemFn, Path, ReturnType, Stmt}; +use syn::{parse_quote, Attribute, Expr, FnArg, Ident, ItemFn, Pat, Path, ReturnType, Stmt}; use quote::{format_ident, quote}; +use crate::refident::MaybePat; use crate::utils::{attr_ends_with, sanitize_ident}; use crate::{ parse::{ - rstest::{RsTestAttributes, RsTestData, RsTestInfo}, + rstest::{RsTestAttributes, RsTestInfo}, testcase::TestCase, vlist::ValueList, }, @@ -29,14 +30,16 @@ use wrapper::WrapByModule; pub(crate) use fixture::render as fixture; -use self::apply_argumets::ApplyArgumets; +use self::apply_argumets::ApplyArguments; use self::crate_resolver::crate_name; pub(crate) mod apply_argumets; pub(crate) mod inject; -pub(crate) fn single(mut test: ItemFn, info: RsTestInfo) -> TokenStream { - test.apply_argumets(&info.arguments); - let resolver = resolver::fixtures::get(info.data.fixtures()); +pub(crate) fn single(mut test: ItemFn, mut info: RsTestInfo) -> TokenStream { + test.apply_argumets(&mut info.arguments, &mut ()); + + let resolver = resolver::fixtures::get(&info.arguments, info.data.fixtures()); + let args = test.sig.inputs.iter().cloned().collect::>(); let attrs = std::mem::take(&mut test.attrs); let asyncness = test.sig.asyncness; @@ -56,10 +59,12 @@ pub(crate) fn single(mut test: ItemFn, info: RsTestInfo) -> TokenStream { } pub(crate) fn parametrize(mut test: ItemFn, info: RsTestInfo) -> TokenStream { - test.apply_argumets(&info.arguments); - let resolver_fixtures = resolver::fixtures::get(info.data.fixtures()); + let mut arguments_info = info.arguments.clone(); + test.apply_argumets(&mut arguments_info, &mut ()); + + let resolver_fixtures = resolver::fixtures::get(&info.arguments, info.data.fixtures()); - let rendered_cases = cases_data(&info.data, test.sig.ident.span()) + let rendered_cases = cases_data(&info, test.sig.ident.span()) .map(|(name, attrs, resolver)| { TestCaseRender::new(name, attrs, (resolver, &resolver_fixtures)) }) @@ -79,7 +84,7 @@ impl ValueList { ) -> TokenStream { let span = test.sig.ident.span(); let test_cases = self - .argument_data(resolver) + .argument_data(resolver, info) .map(|(name, r)| TestCaseRender::new(Ident::new(&name, span), attrs, r)) .map(|test_case| test_case.render(test, info)); @@ -89,17 +94,25 @@ impl ValueList { fn argument_data<'a>( &'a self, resolver: &'a dyn Resolver, - ) -> impl Iterator)> + 'a { + info: &'a RsTestInfo, + ) -> impl Iterator)> + 'a { let max_len = self.values.len(); self.values.iter().enumerate().map(move |(index, value)| { let description = sanitize_ident(&value.description()); + let arg = info.arguments.inner_pat(&self.arg); + + let arg_name = arg + .maybe_ident() + .expect("BUG: Here all arguments should be PatIdent types") + .to_string(); + let name = format!( "{}_{:0len$}_{description:.64}", - self.arg, + arg_name, index + 1, len = max_len.display_len() ); - let resolver_this = (self.arg.to_string(), value.expr.clone()); + let resolver_this = (arg.clone(), value.expr.clone()); (name, Box::new((resolver, resolver_this))) }) } @@ -126,10 +139,12 @@ fn _matrix_recursive<'a>( vlist.render(test, resolver, &attrs, info) } else { let span = test.sig.ident.span(); - let modules = vlist.argument_data(resolver).map(move |(name, resolver)| { - _matrix_recursive(test, list_values, &resolver, attrs, info) - .wrap_by_mod(&Ident::new(&name, span)) - }); + let modules = vlist + .argument_data(resolver, info) + .map(move |(name, resolver)| { + _matrix_recursive(test, list_values, &resolver, attrs, info) + .wrap_by_mod(&Ident::new(&name, span)) + }); quote! { #( #[allow(non_snake_case)] @@ -138,13 +153,13 @@ fn _matrix_recursive<'a>( } } -pub(crate) fn matrix(mut test: ItemFn, info: RsTestInfo) -> TokenStream { - test.apply_argumets(&info.arguments); +pub(crate) fn matrix(mut test: ItemFn, mut info: RsTestInfo) -> TokenStream { + test.apply_argumets(&mut info.arguments, &mut ()); let span = test.sig.ident.span(); - let cases = cases_data(&info.data, span).collect::>(); + let cases = cases_data(&info, span).collect::>(); - let resolver = resolver::fixtures::get(info.data.fixtures()); + let resolver = resolver::fixtures::get(&info.arguments, info.data.fixtures()); let rendered_cases = if cases.is_empty() { let list_values = info.data.list_values().collect::>(); _matrix_recursive(&test, &list_values, &resolver, &[], &info) @@ -246,11 +261,16 @@ fn single_test_case( if !trace_me.is_empty() { attributes.add_trace(format_ident!("trace")); } + let generics_types = generics_types_ident(generics).cloned().collect::>(); + let args = info + .arguments + .replace_fn_args_with_related_inner_pat(args.iter().cloned()) + .collect::>(); let (injectable_args, ignored_args): (Vec<_>, Vec<_>) = - args.iter().partition(|arg| match arg.maybe_ident() { - Some(ident) => !info.arguments.is_ignore(ident), + args.iter().partition(|arg| match arg.maybe_pat() { + Some(pat) => !info.arguments.is_ignore(pat), None => true, }); @@ -258,7 +278,7 @@ fn single_test_case( let args = args .iter() - .filter_map(MaybeIdent::maybe_ident) + .filter_map(MaybePat::maybe_pat) .cloned() .collect::>(); let trace_args = trace_arguments(args.iter(), &attributes); @@ -283,9 +303,11 @@ fn single_test_case( }; let args = args - .into_iter() - .map(|arg| { - if info.arguments.is_by_refs(&arg) { + .iter() + .map(|arg| (arg, info.arguments.is_by_refs(arg))) + .filter_map(|(a, by_refs)| a.maybe_ident().map(|id| (id, by_refs))) + .map(|(arg, by_ref)| { + if by_ref { parse_quote! { &#arg } } else { parse_quote! { #arg } @@ -309,7 +331,7 @@ fn single_test_case( } fn trace_arguments<'a>( - args: impl Iterator, + args: impl Iterator, attributes: &RsTestAttributes, ) -> Option { let mut statements = args @@ -405,15 +427,17 @@ fn format_case_name(case: &TestCase, index: usize, display_len: usize) -> String } fn cases_data( - data: &RsTestData, + info: &RsTestInfo, name_span: Span, -) -> impl Iterator)> { - let display_len = data.cases().count().display_len(); - data.cases().enumerate().map({ +) -> impl Iterator)> { + let display_len = info.data.cases().count().display_len(); + info.data.cases().enumerate().map({ move |(n, case)| { - let resolver_case = data + let resolver_case = info + .data .case_args() - .map(|a| a.to_string()) + .cloned() + .map(|arg| info.arguments.inner_pat(&arg).clone()) .zip(case.args.iter()) .collect::>(); ( diff --git a/rstest_macros/src/render/test.rs b/rstest_macros/src/render/test.rs index 2ce93e27..8df59a0d 100644 --- a/rstest_macros/src/render/test.rs +++ b/rstest_macros/src/render/test.rs @@ -149,8 +149,8 @@ mod single_test_should { let input_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut info: RsTestInfo = Default::default(); info.arguments.set_global_await(true); - info.arguments.add_future(ident("a")); - info.arguments.add_future(ident("b")); + info.arguments.add_future(pat("a")); + info.arguments.add_future(pat("b")); let item_fn: ItemFn = single(input_fn.clone(), info).ast(); @@ -172,8 +172,8 @@ mod single_test_should { fn use_selective_await() { let input_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut info: RsTestInfo = Default::default(); - info.arguments.set_future(ident("a"), FutureArg::Define); - info.arguments.set_future(ident("b"), FutureArg::Await); + info.arguments.set_future(pat("a"), FutureArg::Define); + info.arguments.set_future(pat("b"), FutureArg::Await); let item_fn: ItemFn = single(input_fn.clone(), info).ast(); @@ -195,8 +195,8 @@ mod single_test_should { fn use_ref_if_any() { let input_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut info: RsTestInfo = Default::default(); - info.arguments.set_by_ref(ident("a")); - info.arguments.set_by_ref(ident("c")); + info.arguments.set_by_ref(pat("a")); + info.arguments.set_by_ref(pat("c")); let item_fn: ItemFn = single(input_fn.clone(), info).ast(); @@ -237,7 +237,7 @@ mod single_test_should { .ast(); let mut attributes = RsTestAttributes::default(); - attributes.add_notraces(vec![ident("b_no_trace"), ident("c_no_trace")]); + attributes.add_notraces(vec![pat("b_no_trace"), pat("c_no_trace")]); let item_fn: ItemFn = single( input_fn.clone(), @@ -313,8 +313,8 @@ mod single_test_should { .ast(); let mut arguments = ArgumentsInfo::default(); - arguments.add_future(ident("async_ref_u32")); - arguments.add_future(ident("async_u32")); + arguments.add_future(pat("async_ref_u32")); + arguments.add_future(pat("async_u32")); let info = RsTestInfo { arguments, @@ -523,14 +523,14 @@ mod cases_should { use crate::parse::{ arguments::{ArgumentsInfo, FutureArg}, - rstest::RsTestItem, + rstest::{RsTestData, RsTestItem}, }; use super::{assert_eq, *}; fn into_rstest_data(item_fn: &ItemFn) -> RsTestData { RsTestData { - items: fn_args_idents(item_fn) + items: fn_args_pats(item_fn) .cloned() .map(RsTestItem::CaseArgName) .collect(), @@ -571,8 +571,8 @@ mod cases_should { (self.item_fn, self.info) } - fn add_notrace(mut self, idents: Vec) -> Self { - self.info.attributes.add_notraces(idents); + fn add_notrace(mut self, pats: Vec) -> Self { + self.info.attributes.add_notraces(pats); self } } @@ -898,8 +898,8 @@ mod cases_should { .take(); let mut arguments = ArgumentsInfo::default(); - arguments.add_future(ident("async_ref_u32")); - arguments.add_future(ident("async_u32")); + arguments.add_future(pat("async_ref_u32")); + arguments.add_future(pat("async_u32")); info.arguments = arguments; @@ -965,7 +965,7 @@ mod cases_should { TestCaseBuilder::from(r#"#[trace] fn test(a_trace_me: i32, b_no_trace_me: i32, c_no_trace_me: i32, d_trace_me: i32) {}"#) .push_case(TestCase::from_iter(vec!["1", "2", "1", "2"])) .push_case(TestCase::from_iter(vec!["3", "4", "3", "4"])) - .add_notrace(to_idents!(["b_no_trace_me", "c_no_trace_me"])) + .add_notrace(to_pats!(["b_no_trace_me", "c_no_trace_me"])) .take(); let tokens = parametrize(item_fn, info); @@ -995,7 +995,7 @@ mod cases_should { TestCaseBuilder::from(r#"fn test(a_no_trace_me: i32, b_trace_me: i32) {}"#) .push_case(TestCase::from_iter(vec!["1", "2"])) .push_case(TestCase::from_iter(vec!["3", "4"]).with_attrs(attrs("#[trace]"))) - .add_notrace(to_idents!(["a_no_trace_me"])) + .add_notrace(to_pats!(["a_no_trace_me"])) .take(); let tokens = parametrize(item_fn, info); @@ -1023,8 +1023,8 @@ mod cases_should { .push_case(TestCase::from_iter(vec!["1", "2", "3"])) .take(); info.arguments.set_global_await(true); - info.arguments.add_future(ident("a")); - info.arguments.add_future(ident("b")); + info.arguments.add_future(pat("a")); + info.arguments.add_future(pat("b")); let tokens = parametrize(item_fn, info); @@ -1043,8 +1043,8 @@ mod cases_should { .push_case(TestCase::from_iter(vec!["1", "2", "3"])) .push_case(TestCase::from_iter(vec!["1", "2", "3"])) .take(); - info.arguments.set_future(ident("a"), FutureArg::Define); - info.arguments.set_future(ident("b"), FutureArg::Await); + info.arguments.set_future(pat("a"), FutureArg::Define); + info.arguments.set_future(pat("b"), FutureArg::Await); let tokens = parametrize(item_fn, info); @@ -1061,7 +1061,10 @@ mod cases_should { mod matrix_cases_should { use rstest_test::{assert_in, assert_not_in}; - use crate::parse::arguments::{ArgumentsInfo, FutureArg}; + use crate::parse::{ + arguments::{ArgumentsInfo, FutureArg}, + rstest::RsTestData, + }; /// Should test matrix tests render without take in account MatrixInfo to RsTestInfo /// transformation @@ -1069,7 +1072,7 @@ mod matrix_cases_should { fn into_rstest_data(item_fn: &ItemFn) -> RsTestData { RsTestData { - items: fn_args_idents(item_fn) + items: fn_args_pats(item_fn) .cloned() .map(|it| { ValueList { @@ -1337,8 +1340,8 @@ mod matrix_cases_should { let item_fn = r#"async fn test(async_ref_u32: &u32, async_u32: u32,simple: u32) { }"#.ast(); let mut arguments = ArgumentsInfo::default(); - arguments.add_future(ident("async_ref_u32")); - arguments.add_future(ident("async_u32")); + arguments.add_future(pat("async_ref_u32")); + arguments.add_future(pat("async_u32")); let info = RsTestInfo { arguments, @@ -1455,7 +1458,7 @@ mod matrix_cases_should { .into(), }; let mut attributes: RsTestAttributes = Default::default(); - attributes.add_notraces(vec![ident("b_no_trace_me"), ident("c_no_trace_me")]); + attributes.add_notraces(vec![pat("b_no_trace_me"), pat("c_no_trace_me")]); let item_fn: ItemFn = r#"#[trace] fn test(a_trace_me: u32, b_no_trace_me: u32, c_no_trace_me: u32, d_trace_me: u32) {}"#.ast(); let tokens = matrix( @@ -1503,8 +1506,8 @@ mod matrix_cases_should { arguments: Default::default(), }; info.arguments.set_global_await(true); - info.arguments.add_future(ident("a")); - info.arguments.add_future(ident("b")); + info.arguments.add_future(pat("a")); + info.arguments.add_future(pat("b")); let tokens = matrix(item_fn, info); @@ -1534,8 +1537,8 @@ mod matrix_cases_should { arguments: Default::default(), }; - info.arguments.set_future(ident("a"), FutureArg::Define); - info.arguments.set_future(ident("b"), FutureArg::Await); + info.arguments.set_future(pat("a"), FutureArg::Define); + info.arguments.set_future(pat("b"), FutureArg::Await); let tokens = matrix(item_fn, info); @@ -1734,6 +1737,8 @@ mod matrix_cases_should { } mod complete_should { + use crate::parse::rstest::RsTestData; + use super::{assert_eq, *}; fn rendered_case(fn_name: &str) -> TestsGroup { diff --git a/rstest_macros/src/resolver.rs b/rstest_macros/src/resolver.rs index e1d9c879..a9bd59d4 100644 --- a/rstest_macros/src/resolver.rs +++ b/rstest_macros/src/resolver.rs @@ -5,27 +5,33 @@ use std::borrow::Cow; use std::collections::HashMap; -use proc_macro2::Ident; -use syn::{parse_quote, Expr}; +use syn::{parse_quote, Expr, Pat}; use crate::parse::Fixture; pub(crate) mod fixtures { use quote::format_ident; + use crate::parse::arguments::ArgumentsInfo; + use super::*; - pub(crate) fn get<'a>(fixtures: impl Iterator) -> impl Resolver + 'a { + pub(crate) fn get<'a>( + arguments: &ArgumentsInfo, + fixtures: impl Iterator, + ) -> impl Resolver + 'a { fixtures - .map(|f| (f.name.to_string(), extract_resolve_expression(f))) + .map(|f| { + ( + arguments.inner_pat(&f.arg).clone(), + extract_resolve_expression(f), + ) + }) .collect::>() } fn extract_resolve_expression(fixture: &Fixture) -> syn::Expr { - let resolve = fixture - .resolve - .clone() - .unwrap_or_else(|| fixture.name.clone().into()); + let resolve = fixture.resolve.clone(); let positional = &fixture.positional.0; let f_name = match positional.len() { 0 => format_ident!("default"), @@ -43,11 +49,22 @@ pub(crate) mod fixtures { #[case(&[], "default()")] #[case(&["my_expression"], "partial_1(my_expression)")] #[case(&["first", "other"], "partial_2(first, other)")] - fn resolve_by_use_the_given_name(#[case] args: &[&str], #[case] expected: &str) { + fn resolve_by_use_the_given_name( + #[case] args: &[&str], + #[case] expected: &str, + #[values(None, Some("minnie"), Some("__destruct_1"))] inner_pat: Option<&str>, + ) { let data = vec![fixture("pippo", args)]; - let resolver = get(data.iter()); + let mut arguments: ArgumentsInfo = Default::default(); + let mut request = pat("pippo"); + if let Some(inner) = inner_pat { + arguments.set_inner_pat(pat("pippo"), pat(inner)); + request = pat(inner); + } - let resolved = resolver.resolve(&ident("pippo")).unwrap().into_owned(); + let resolver = get(&arguments, data.iter()); + + let resolved = resolver.resolve(&request).unwrap().into_owned(); assert_eq!(resolved, format!("pippo::{}", expected).ast()); } @@ -60,11 +77,18 @@ pub(crate) mod fixtures { #[case] args: &[&str], #[case] expected: &str, #[values("pluto", "minnie::pluto")] resolver_path: &str, + #[values(None, Some("minnie"), Some("__destruct_1"))] inner_pat: Option<&str>, ) { let data = vec![fixture("pippo", args).with_resolve(resolver_path)]; - let resolver = get(data.iter()); + let mut arguments: ArgumentsInfo = Default::default(); + let mut request = pat("pippo"); + if let Some(inner) = inner_pat { + arguments.set_inner_pat(pat("pippo"), pat(inner)); + request = pat(inner); + } + let resolver = get(&arguments, data.iter()); - let resolved = resolver.resolve(&ident("pippo")).unwrap().into_owned(); + let resolved = resolver.resolve(&request).unwrap().into_owned(); assert_eq!(resolved, format!("{}::{}", resolver_path, expected).ast()); } @@ -77,7 +101,7 @@ pub(crate) mod values { pub(crate) fn get<'a>(values: impl Iterator) -> impl Resolver + 'a { values - .map(|av| (av.name.to_string(), &av.expr)) + .map(|av| (av.arg.clone(), &av.expr)) .collect::>() } @@ -95,11 +119,11 @@ pub(crate) mod values { let resolver = get(data.iter()); assert_eq!( - resolver.resolve(&ident("pippo")).unwrap().into_owned(), + resolver.resolve(&pat("pippo")).unwrap().into_owned(), "42".ast() ); assert_eq!( - resolver.resolve(&ident("donaldduck")).unwrap().into_owned(), + resolver.resolve(&pat("donaldduck")).unwrap().into_owned(), "vec![1,2]".ast() ); } @@ -108,44 +132,44 @@ pub(crate) mod values { /// A trait that `resolve` the given ident to expression code to assign the value. pub(crate) trait Resolver { - fn resolve(&self, ident: &Ident) -> Option>; + fn resolve(&self, arg: &Pat) -> Option>; } -impl<'a> Resolver for HashMap { - fn resolve(&self, ident: &Ident) -> Option> { - let ident = ident.to_string(); - self.get(&ident).map(|&c| Cow::Borrowed(c)) +impl<'a> Resolver for HashMap { + fn resolve(&self, arg: &Pat) -> Option> { + self.get(arg) + .or_else(|| self.get(&pat_invert_mutability(arg))) + .map(|&c| Cow::Borrowed(c)) } } -impl Resolver for HashMap { - fn resolve(&self, ident: &Ident) -> Option> { - let ident = ident.to_string(); - self.get(&ident).map(Cow::Borrowed) +impl Resolver for HashMap { + fn resolve(&self, arg: &Pat) -> Option> { + self.get(arg).map(Cow::Borrowed) } } impl Resolver for (R1, R2) { - fn resolve(&self, ident: &Ident) -> Option> { - self.0.resolve(ident).or_else(|| self.1.resolve(ident)) + fn resolve(&self, arg: &Pat) -> Option> { + self.0.resolve(arg).or_else(|| self.1.resolve(arg)) } } impl Resolver for &R { - fn resolve(&self, ident: &Ident) -> Option> { - (*self).resolve(ident) + fn resolve(&self, arg: &Pat) -> Option> { + (*self).resolve(arg) } } impl Resolver for Box { - fn resolve(&self, ident: &Ident) -> Option> { - (**self).resolve(ident) + fn resolve(&self, arg: &Pat) -> Option> { + (**self).resolve(arg) } } -impl Resolver for (String, Expr) { - fn resolve(&self, ident: &Ident) -> Option> { - if *ident == self.0 { +impl Resolver for (Pat, Expr) { + fn resolve(&self, arg: &Pat) -> Option> { + if arg == &self.0 { Some(Cow::Borrowed(&self.1)) } else { None @@ -153,6 +177,19 @@ impl Resolver for (String, Expr) { } } +pub(crate) fn pat_invert_mutability(p: &Pat) -> Pat { + match p.clone() { + Pat::Ident(mut ident) => { + ident.mutability = match ident.mutability { + Some(_) => None, + None => Some(syn::parse_quote! { mut }), + }; + syn::Pat::Ident(ident) + } + p => p, + } +} + #[cfg(test)] mod should { use super::*; @@ -162,11 +199,23 @@ mod should { #[test] fn return_the_given_expression() { let ast = parse_str("fn function(mut foo: String) {}").unwrap(); - let arg = first_arg_ident(&ast); + let arg = first_arg_pat(&ast); + let expected = expr("bar()"); + let mut resolver = HashMap::new(); + + resolver.insert(pat("foo").with_mut(), &expected); + + assert_eq!(expected, (&resolver).resolve(&arg).unwrap().into_owned()) + } + + #[test] + fn return_the_given_expression_also_if_not_mut_searched() { + let ast = parse_str("fn function(foo: String) {}").unwrap(); + let arg = first_arg_pat(&ast); let expected = expr("bar()"); let mut resolver = HashMap::new(); - resolver.insert("foo".to_string(), &expected); + resolver.insert(pat("foo").with_mut(), &expected); assert_eq!(expected, (&resolver).resolve(&arg).unwrap().into_owned()) } @@ -174,7 +223,7 @@ mod should { #[test] fn return_none_for_unknown_argument() { let ast = "fn function(mut fix: String) {}".ast(); - let arg = first_arg_ident(&ast); + let arg = first_arg_pat(&ast); assert!(EmptyResolver.resolve(&arg).is_none()) } diff --git a/rstest_macros/src/test.rs b/rstest_macros/src/test.rs index 22233254..de630ce2 100644 --- a/rstest_macros/src/test.rs +++ b/rstest_macros/src/test.rs @@ -9,7 +9,8 @@ pub(crate) use pretty_assertions::assert_eq; use proc_macro2::TokenTree; use quote::quote; pub(crate) use rstest::{fixture, rstest}; -use syn::{parse::Parse, parse2, parse_quote, parse_str, Error, Expr, Ident, Stmt}; +use syn::{parse::Parse, parse2, parse_quote, parse_str, Error, Expr, Ident, Pat, Stmt}; +use utils::fn_args_pats; use super::*; use crate::parse::{ @@ -20,15 +21,17 @@ use crate::parse::{ Attribute, Fixture, Positional, }; use crate::resolver::Resolver; -use crate::utils::fn_args_idents; use parse::fixture::ArgumentValue; macro_rules! to_args { ($e:expr) => {{ - $e.iter() - .map(|s| s as &dyn AsRef) - .map(expr) - .collect::>() + $e.iter().map(expr).collect::>() + }}; +} + +macro_rules! to_fnargs { + ($e:expr) => {{ + $e.iter().map(fn_arg).collect::>() }}; } @@ -50,6 +53,12 @@ macro_rules! to_idents { }; } +macro_rules! to_pats { + ($e:expr) => { + $e.iter().map(|s| pat(s)).collect::>() + }; +} + struct Outer(T); impl Parse for Outer { fn parse(input: syn::parse::ParseStream) -> syn::Result { @@ -119,10 +128,34 @@ pub(crate) fn path(s: impl AsRef) -> syn::Path { s.as_ref().ast() } +pub(crate) fn pat(s: impl AsRef) -> syn::Pat { + syn::parse::Parser::parse_str(Pat::parse_single, s.as_ref()).unwrap() +} + +pub trait PatBuilder { + fn with_mut(self) -> Self; +} + +impl PatBuilder for syn::Pat { + fn with_mut(self) -> Self { + match self { + Pat::Ident(mut ident) => { + ident.mutability = Some("mut".ast()); + syn::Pat::Ident(ident) + } + _ => unimplemented!("Unsupported pattern: {:?}", self,), + } + } +} + pub(crate) fn expr(s: impl AsRef) -> syn::Expr { s.as_ref().ast() } +pub(crate) fn fn_arg(s: impl AsRef) -> syn::FnArg { + s.as_ref().ast() +} + pub(crate) fn attr(s: impl AsRef) -> syn::Attribute { let a = attrs(s); assert_eq!(1, a.len()); @@ -141,22 +174,23 @@ pub(crate) fn attrs(s: impl AsRef) -> Vec { } pub(crate) fn fixture(name: impl AsRef, args: &[&str]) -> Fixture { - Fixture::new(ident(name), None, Positional(to_exprs!(args))) + let name = name.as_ref().to_owned(); + Fixture::new(pat(&name), path(&name), Positional(to_exprs!(args))) } pub(crate) fn arg_value(name: impl AsRef, value: impl AsRef) -> ArgumentValue { - ArgumentValue::new(ident(name), expr(value)) + ArgumentValue::new(pat(name), expr(value)) } pub(crate) fn values_list>(arg: &str, values: &[S]) -> ValueList { ValueList { - arg: ident(arg), + arg: pat(arg), values: values.into_iter().map(|s| expr(s).into()).collect(), } } -pub(crate) fn first_arg_ident(ast: &ItemFn) -> &Ident { - fn_args_idents(&ast).next().unwrap() +pub(crate) fn first_arg_pat(ast: &ItemFn) -> &Pat { + fn_args_pats(&ast).next().unwrap() } pub(crate) fn extract_inner_functions(block: &syn::Block) -> impl Iterator { @@ -203,7 +237,7 @@ impl Attribute { } pub fn tagged, SA: AsRef>(tag: SI, attrs: Vec) -> Self { - Attribute::Tagged(ident(tag), attrs.into_iter().map(|a| ident(a)).collect()) + Attribute::Tagged(ident(tag), attrs.into_iter().map(pat).collect()) } pub fn typed, T: AsRef>(tag: S, inner: T) -> Self { @@ -223,7 +257,7 @@ impl RsTestInfo { impl Fixture { pub fn with_resolve(mut self, resolve_path: &str) -> Self { - self.resolve = Some(path(resolve_path)); + self.resolve = path(resolve_path); self } } @@ -286,7 +320,7 @@ impl From> for FixtureData { pub(crate) struct EmptyResolver; impl<'a> Resolver for EmptyResolver { - fn resolve(&self, _ident: &Ident) -> Option> { + fn resolve(&self, _pat: &Pat) -> Option> { None } } @@ -339,8 +373,8 @@ pub(crate) fn ref_argument_code_string(arg_name: &str) -> String { pub(crate) fn mut_await_argument_code_string(arg_name: &str) -> String { let arg_name = ident(arg_name); - let statement: Stmt = parse_quote! { + let statment: Stmt = parse_quote! { let mut #arg_name = #arg_name.await; }; - statement.display_code() + statment.display_code() } diff --git a/rstest_macros/src/utils.rs b/rstest_macros/src/utils.rs index e309a676..d78a9d58 100644 --- a/rstest_macros/src/utils.rs +++ b/rstest_macros/src/utils.rs @@ -4,19 +4,29 @@ use quote::format_ident; use std::collections::{HashMap, HashSet}; use unicode_ident::is_xid_continue; -use crate::refident::MaybeIdent; -use syn::{Attribute, Expr, FnArg, Generics, Ident, ItemFn, ReturnType, Type, WherePredicate}; +use crate::refident::{MaybeIdent, MaybePat}; +use syn::{Attribute, Expr, FnArg, Generics, Ident, ItemFn, Pat, ReturnType, Type, WherePredicate}; /// Return an iterator over fn arguments items. /// -pub(crate) fn fn_args_idents(test: &ItemFn) -> impl Iterator { - fn_args(test).filter_map(MaybeIdent::maybe_ident) +pub(crate) fn fn_args_pats(test: &ItemFn) -> impl Iterator { + fn_args(test).filter_map(MaybePat::maybe_pat) +} + +pub(crate) fn compare_pat(a: &Pat, b: &Pat) -> bool { + match (a, b) { + (Pat::Ident(a), Pat::Ident(b)) => a.ident == b.ident, + (Pat::Tuple(a), Pat::Tuple(b)) => a.elems == b.elems, + (Pat::TupleStruct(a), Pat::TupleStruct(b)) => a.path == b.path && a.elems == b.elems, + (Pat::Struct(a), Pat::Struct(b)) => a.path == b.path && a.fields == b.fields, + _ => false, + } } /// Return if function declaration has an ident /// -pub(crate) fn fn_args_has_ident(fn_decl: &ItemFn, ident: &Ident) -> bool { - fn_args_idents(fn_decl).any(|id| id == ident) +pub(crate) fn fn_args_has_pat(fn_decl: &ItemFn, pat: &Pat) -> bool { + fn_args_pats(fn_decl).any(|id| compare_pat(id, pat)) } /// Return an iterator over fn arguments. @@ -294,25 +304,13 @@ mod test { use crate::test::{assert_eq, *}; #[test] - fn fn_args_idents_should() { - let item_fn = parse_quote! { - fn the_functon(first: u32, second: u32) {} - }; - - let mut args = fn_args_idents(&item_fn); - - assert_eq!("first", args.next().unwrap().to_string()); - assert_eq!("second", args.next().unwrap().to_string()); - } - - #[test] - fn fn_args_has_ident_should() { + fn fn_args_has_pat_should() { let item_fn = parse_quote! { fn the_functon(first: u32, second: u32) {} }; - assert!(fn_args_has_ident(&item_fn, &ident("first"))); - assert!(!fn_args_has_ident(&item_fn, &ident("third"))); + assert!(fn_args_has_pat(&item_fn, &pat("first"))); + assert!(!fn_args_has_pat(&item_fn, &pat("third"))); } #[rstest]