From bbfb4bbd676c02f823b249adbe6303b1f89a77e6 Mon Sep 17 00:00:00 2001 From: Marco Bacis Date: Sat, 20 Jul 2024 11:52:07 +0200 Subject: [PATCH 1/5] add failing test for argument ignore feature --- .../resources/rstest/ignore_not_fixture_arg.rs | 16 ++++++++++++++++ rstest/tests/rstest/mod.rs | 13 +++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 rstest/tests/resources/rstest/ignore_not_fixture_arg.rs diff --git a/rstest/tests/resources/rstest/ignore_not_fixture_arg.rs b/rstest/tests/resources/rstest/ignore_not_fixture_arg.rs new file mode 100644 index 00000000..47466a00 --- /dev/null +++ b/rstest/tests/resources/rstest/ignore_not_fixture_arg.rs @@ -0,0 +1,16 @@ +use rstest::{fixture, rstest}; + +use sqlx::PgPool; + +struct FixtureStruct {} + +#[fixture] +async fn my_fixture() -> FixtureStruct { + FixtureStruct {} +} + +#[rstest] +#[sqlx::test] +async fn test_db(#[future] my_fixture: FixtureStruct, #[ignore] pool: PgPool) { + assert!(true); +} diff --git a/rstest/tests/rstest/mod.rs b/rstest/tests/rstest/mod.rs index 10165218..58b6fef2 100644 --- a/rstest/tests/rstest/mod.rs +++ b/rstest/tests/rstest/mod.rs @@ -1004,6 +1004,19 @@ fn ignore_underscore_args() { .assert(output); } +#[test] +fn ignore_args_not_fixtures() { + let prj = prj("ignore_not_fixture_arg.rs"); + prj.add_dependency("sqlx", r#"{version="*", features=["postgres","macros"]}"#); + + let output = prj.run_tests().unwrap(); + + TestResults::new() + .with_contains(true) + .ok("test_db") + .assert(output); +} + #[test] fn timeout() { let mut prj = prj("timeout.rs"); From bb02cfd78f7f5225372e0a518ca2b8bee34c6f56 Mon Sep 17 00:00:00 2001 From: Marco Bacis Date: Sat, 20 Jul 2024 22:44:52 +0200 Subject: [PATCH 2/5] implement ignore attribute parsing --- rstest_macros/src/parse/arguments.rs | 30 ++++++++++++++ rstest_macros/src/parse/ignore.rs | 61 ++++++++++++++++++++++++++++ rstest_macros/src/parse/mod.rs | 1 + rstest_macros/src/parse/rstest.rs | 7 +++- 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 rstest_macros/src/parse/ignore.rs diff --git a/rstest_macros/src/parse/arguments.rs b/rstest_macros/src/parse/arguments.rs index c4bf31e3..ded4792e 100644 --- a/rstest_macros/src/parse/arguments.rs +++ b/rstest_macros/src/parse/arguments.rs @@ -16,6 +16,7 @@ pub(crate) enum FutureArg { pub(crate) struct ArgumentInfo { future: FutureArg, by_ref: bool, + ignore: bool, } impl ArgumentInfo { @@ -33,6 +34,13 @@ impl ArgumentInfo { } } + fn ignore() -> Self { + Self { + ignore: true, + ..Default::default() + } + } + fn is_future(&self) -> bool { use FutureArg::*; @@ -48,6 +56,10 @@ impl ArgumentInfo { fn is_by_ref(&self) -> bool { self.by_ref } + + fn is_ignore(&self) -> bool { + self.ignore + } } #[derive(PartialEq, Default, Debug)] @@ -115,16 +127,34 @@ impl ArgumentsInfo { .or_insert_with(ArgumentInfo::by_ref); } + pub(crate) fn set_ignore(&mut self, ident: Ident) { + self.args + .entry(ident) + .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_ignores(&mut self, ignores: impl Iterator) { + ignores.for_each(|ident| self.set_ignore(ident)); + } + pub(crate) fn is_by_refs(&self, id: &Ident) -> bool { self.args .get(id) .map(|arg| arg.is_by_ref()) .unwrap_or_default() } + + pub(crate) fn is_ignore(&self, id: &Ident) -> bool { + self.args + .get(id) + .map(|arg| arg.is_ignore()) + .unwrap_or_default() + } } #[cfg(test)] diff --git a/rstest_macros/src/parse/ignore.rs b/rstest_macros/src/parse/ignore.rs new file mode 100644 index 00000000..f51c6dfc --- /dev/null +++ b/rstest_macros/src/parse/ignore.rs @@ -0,0 +1,61 @@ +use syn::{visit_mut::VisitMut, Ident, ItemFn}; + +use crate::error::ErrorsVec; + +use super::just_once::JustOnceFnArgAttributeExtractor; + +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() +} + +#[cfg(test)] +mod should { + use super::*; + use crate::test::{assert_eq, *}; + use rstest_test::assert_in; + + #[rstest] + #[case("fn simple(a: u32) {}")] + #[case("fn more(a: u32, b: &str) {}")] + #[case("fn gen>(a: u32, b: S) {}")] + #[case("fn attr(#[case] a: u32, #[values(1,2)] b: i32) {}")] + fn not_change_anything_if_no_ignore_attribute_found(#[case] item_fn: &str) { + let mut item_fn: ItemFn = item_fn.ast(); + let orig = item_fn.clone(); + + let by_refs = extract_ignores(&mut item_fn).unwrap(); + + assert_eq!(orig, item_fn); + assert!(by_refs.is_empty()); + } + + #[rstest] + #[case::simple("fn f(#[ignore] a: u32) {}", "fn f(a: u32) {}", &["a"])] + #[case::more_than_one( + "fn f(#[ignore] a: u32, #[ignore] b: String, #[ignore] c: std::collection::HashMap) {}", + r#"fn f(a: u32, + b: String, + c: std::collection::HashMap) {}"#, + &["a", "b", "c"])] + fn extract(#[case] item_fn: &str, #[case] expected: &str, #[case] expected_refs: &[&str]) { + let mut item_fn: ItemFn = item_fn.ast(); + let expected: ItemFn = expected.ast(); + + let by_refs = extract_ignores(&mut item_fn).unwrap(); + + assert_eq!(expected, item_fn); + assert_eq!(by_refs, to_idents!(expected_refs)); + } + + #[rstest] + #[case::no_more_than_one("fn f(#[ignore] #[ignore] a: u32) {}", "more than once")] + fn raise_error(#[case] item_fn: &str, #[case] message: &str) { + let mut item_fn: ItemFn = item_fn.ast(); + + let err = extract_ignores(&mut item_fn).unwrap_err(); + + assert_in!(format!("{:?}", err), message); + } +} diff --git a/rstest_macros/src/parse/mod.rs b/rstest_macros/src/parse/mod.rs index 14e6d1dc..52f0405a 100644 --- a/rstest_macros/src/parse/mod.rs +++ b/rstest_macros/src/parse/mod.rs @@ -31,6 +31,7 @@ pub(crate) mod by_ref; pub(crate) mod expressions; pub(crate) mod fixture; pub(crate) mod future; +pub(crate) mod ignore; pub(crate) mod just_once; pub(crate) mod rstest; pub(crate) mod testcase; diff --git a/rstest_macros/src/parse/rstest.rs b/rstest_macros/src/parse/rstest.rs index 911d9e7e..a233d62e 100644 --- a/rstest_macros/src/parse/rstest.rs +++ b/rstest_macros/src/parse/rstest.rs @@ -11,6 +11,7 @@ use super::{ check_timeout_attrs, extract_case_args, extract_cases, extract_excluded_trace, extract_fixtures, extract_value_list, future::{extract_futures, extract_global_awt}, + ignore::extract_ignores, parse_vector_trailing_till_double_comma, testcase::TestCase, Attribute, Attributes, ExtendWithFunctionAttrs, Fixture, @@ -51,18 +52,20 @@ impl Parse for RsTestInfo { impl ExtendWithFunctionAttrs for RsTestInfo { fn extend_with_function_attrs(&mut self, item_fn: &mut ItemFn) -> Result<(), ErrorsVec> { - let composed_tuple!(_inner, excluded, _timeout, futures, global_awt, by_refs) = merge_errors!( + let composed_tuple!(_inner, excluded, _timeout, futures, global_awt, by_refs, ignores) = merge_errors!( self.data.extend_with_function_attrs(item_fn), extract_excluded_trace(item_fn), check_timeout_attrs(item_fn), extract_futures(item_fn), extract_global_awt(item_fn), - extract_by_ref(item_fn) + extract_by_ref(item_fn), + extract_ignores(item_fn) )?; self.attributes.add_notraces(excluded); self.arguments.set_global_await(global_awt); self.arguments.set_futures(futures.into_iter()); self.arguments.set_by_refs(by_refs.into_iter()); + self.arguments.set_ignores(ignores.into_iter()); Ok(()) } } From 43f44cf01650a29aa3d43fdf7106e3eb8a773b03 Mon Sep 17 00:00:00 2001 From: Marco Bacis Date: Sat, 20 Jul 2024 23:08:31 +0200 Subject: [PATCH 3/5] implement argument ignore feature --- .../resources/rstest/ignore_not_fixture_arg.rs | 8 ++++---- rstest/tests/rstest/mod.rs | 5 ++++- rstest_macros/src/render/mod.rs | 14 ++++++++++++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/rstest/tests/resources/rstest/ignore_not_fixture_arg.rs b/rstest/tests/resources/rstest/ignore_not_fixture_arg.rs index 47466a00..d16fc652 100644 --- a/rstest/tests/resources/rstest/ignore_not_fixture_arg.rs +++ b/rstest/tests/resources/rstest/ignore_not_fixture_arg.rs @@ -1,16 +1,16 @@ -use rstest::{fixture, rstest}; +use rstest::*; -use sqlx::PgPool; +use sqlx::SqlitePool; struct FixtureStruct {} #[fixture] -async fn my_fixture() -> FixtureStruct { +fn my_fixture() -> FixtureStruct { FixtureStruct {} } #[rstest] #[sqlx::test] -async fn test_db(#[future] my_fixture: FixtureStruct, #[ignore] pool: PgPool) { +async fn test_db(my_fixture: FixtureStruct, #[ignore] pool: SqlitePool) { assert!(true); } diff --git a/rstest/tests/rstest/mod.rs b/rstest/tests/rstest/mod.rs index 58b6fef2..67fcd9b3 100644 --- a/rstest/tests/rstest/mod.rs +++ b/rstest/tests/rstest/mod.rs @@ -1007,7 +1007,10 @@ fn ignore_underscore_args() { #[test] fn ignore_args_not_fixtures() { let prj = prj("ignore_not_fixture_arg.rs"); - prj.add_dependency("sqlx", r#"{version="*", features=["postgres","macros"]}"#); + prj.add_dependency( + "sqlx", + r#"{version="*", features=["sqlite","macros","runtime-tokio"]}"#, + ); let output = prj.run_tests().unwrap(); diff --git a/rstest_macros/src/render/mod.rs b/rstest_macros/src/render/mod.rs index 4cd4224c..5155109d 100644 --- a/rstest_macros/src/render/mod.rs +++ b/rstest_macros/src/render/mod.rs @@ -247,7 +247,15 @@ fn single_test_case( attributes.add_trace(format_ident!("trace")); } let generics_types = generics_types_ident(generics).cloned().collect::>(); - let inject = inject::resolve_aruments(args.iter(), &resolver, &generics_types); + + let (injectable_args, ignored_args): (Vec<_>, Vec<_>) = + args.iter().partition(|arg| match arg.maybe_ident() { + Some(ident) => !info.arguments.is_ignore(ident), + None => true, + }); + + let inject = inject::resolve_aruments(injectable_args.into_iter(), &resolver, &generics_types); + let args = args .iter() .filter_map(MaybeIdent::maybe_ident) @@ -273,6 +281,7 @@ fn single_test_case( } else { Some(resolve_default_test_attr(is_async)) }; + let args = args .into_iter() .map(|arg| { @@ -283,13 +292,14 @@ fn single_test_case( } }) .collect::>(); + let execute = render_test_call(testfn_name.clone().into(), &args, timeout, is_async); let lifetimes = generics.lifetimes(); quote! { #test_attr #(#attrs)* - #asyncness fn #name<#(#lifetimes,)*>() #output { + #asyncness fn #name<#(#lifetimes,)*>(#(#ignored_args,)*) #output { #test_impl #inject #trace_args From a2b902e7d996cfe9fddbb15544a62d5fade85690 Mon Sep 17 00:00:00 2001 From: Marco Bacis Date: Sun, 21 Jul 2024 10:46:11 +0200 Subject: [PATCH 4/5] update docs --- CHANGELOG.md | 2 ++ rstest/src/lib.rs | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3560187..63803575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Add +- Implemented `#[ignore]` attribute to ignore test parameters during fixtures resolution/injection. See [#228](https://github.com/la10736/rstest/issues/228) for details + ### Fixed ## [0.21.0] 2024/6/1 diff --git a/rstest/src/lib.rs b/rstest/src/lib.rs index 1c8c9da4..12450d8a 100644 --- a/rstest/src/lib.rs +++ b/rstest/src/lib.rs @@ -1182,6 +1182,30 @@ pub use rstest_macros::fixture; /// } /// ``` /// +/// ## Ignoring Arguments +/// +/// Sometimes, you may want to inject and use fixtures not managed by rstest +/// (e.g. db connection pools for sqlx tests). +/// +/// In these cases, you can use the `#[ignore]` attribute to ignore the additional +/// parameter and let another crate take care of it: +/// +/// ```rust, ignore +/// use rstest::*; +/// use sqlx::*; +/// +/// #[fixture] +/// fn my_fixture() -> i32 { 42 } +/// +/// #[rstest] +/// #[sqlx::test] +/// async fn test_db(my_fixture: i32, #[ignore] pool: PgPool) { +/// assert_eq!(42, injected); +/// // do stuff with the connection pool +/// } +/// ``` +/// +/// /// ## Trace Input Arguments /// /// Sometimes can be very helpful to print all test's input arguments. To From cf9dd0bf5109f5d59adad247348174f98a07bb01 Mon Sep 17 00:00:00 2001 From: Marco Bacis Date: Sun, 21 Jul 2024 20:49:15 +0200 Subject: [PATCH 5/5] update docs, simplified unit test --- rstest/src/lib.rs | 4 ++++ rstest_macros/src/parse/ignore.rs | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/rstest/src/lib.rs b/rstest/src/lib.rs index 12450d8a..e84563d8 100644 --- a/rstest/src/lib.rs +++ b/rstest/src/lib.rs @@ -1107,6 +1107,10 @@ pub use rstest_macros::fixture; /// in this case the `#[actix_rt::test]` attribute will replace the standard `#[test]` /// attribute. /// +/// Some test attributes allow to inject arguments into the test function, in a similar way to rstest. +/// This can lead to compile errors when rstest is not able to resolve the additional arguments. +/// To avoid this, see [Ignoring Arguments](attr.rstest.html#ignoring-arguments). +/// /// ## Local lifetime and `#[by_ref]` attribute /// /// In some cases you may want to use a local lifetime for some arguments of your test. diff --git a/rstest_macros/src/parse/ignore.rs b/rstest_macros/src/parse/ignore.rs index f51c6dfc..7a16a8e6 100644 --- a/rstest_macros/src/parse/ignore.rs +++ b/rstest_macros/src/parse/ignore.rs @@ -49,13 +49,12 @@ mod should { assert_eq!(by_refs, to_idents!(expected_refs)); } - #[rstest] - #[case::no_more_than_one("fn f(#[ignore] #[ignore] a: u32) {}", "more than once")] - fn raise_error(#[case] item_fn: &str, #[case] message: &str) { - let mut item_fn: ItemFn = item_fn.ast(); + #[test] + fn raise_error() { + let mut item_fn: ItemFn = "fn f(#[ignore] #[ignore] a: u32) {}".ast(); let err = extract_ignores(&mut item_fn).unwrap_err(); - assert_in!(format!("{:?}", err), message); + assert_in!(format!("{:?}", err), "more than once"); } }