diff --git a/CHANGELOG.md b/CHANGELOG.md index b46b670..54a17d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,11 @@ ### Add +- `#[timeout(duration)]` test implementation for both sync and async tests (See #136) + ### Changed +- Split rstest in separated crates for macro and libs (See #32) ### Fixed ## [0.12.0] 2021/12/12 diff --git a/playground/Cargo.toml b/playground/Cargo.toml index 96ee69f..3fff967 100644 --- a/playground/Cargo.toml +++ b/playground/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "playground" -version = "0.1.0" authors = ["michele "] edition = "2018" +name = "playground" +version = "0.1.0" [features] default = [] @@ -10,11 +10,11 @@ default = [] trace_all = [] [dependencies] +async-std = {version = "1.5", features = ["attributes"]} lazy_static = "*" -async-std = { version="1.5", features = ["attributes"] } [dependencies.rstest] -path = "../" +path = "../rstest" version = "*" [dependencies.rstest_reuse] diff --git a/rstest/Cargo.toml b/rstest/Cargo.toml index 663b75f..c255926 100644 --- a/rstest/Cargo.toml +++ b/rstest/Cargo.toml @@ -17,6 +17,7 @@ version = "0.13.0" [lib] [dependencies] +async-std = {version = "1.9.0", features = ["attributes"]} futures = "0.3.15" futures-timer = "3.0.2" rstest_macros = {version = "0.13.0", path = "../rstest_macros"} diff --git a/rstest/tests/resources/rstest/errors.rs b/rstest/tests/resources/rstest/errors.rs index 565e912..d068bbe 100644 --- a/rstest/tests/resources/rstest/errors.rs +++ b/rstest/tests/resources/rstest/errors.rs @@ -92,3 +92,15 @@ async fn error_future_on_impl_type(#[case] #[future] s: impl AsRef) {} #[rstest] #[case(async { 42 } )] async fn error_future_on_impl_type(#[case] #[future] #[future] a: i32) {} + +#[rstest] +#[timeout] +fn error_timeout_without_arg() {} + +#[rstest] +#[timeout(some -> strange -> invalid -> expression)] +fn error_timeout_without_expression_arg() {} + +#[rstest] +#[timeout(42)] +fn error_timeout_without_duration() {} diff --git a/rstest/tests/resources/rstest/timeout.rs b/rstest/tests/resources/rstest/timeout.rs index 3b55138..ee71528 100644 --- a/rstest/tests/resources/rstest/timeout.rs +++ b/rstest/tests/resources/rstest/timeout.rs @@ -2,7 +2,7 @@ use rstest::*; use std::time::Duration; fn ms(ms: u32) -> Duration { - Duration::from_millis(ms) + Duration::from_millis(ms.into()) } mod thread { @@ -13,6 +13,24 @@ mod thread { a + b } + #[rstest] + #[timeout(ms(80))] + fn single_pass() { + assert_eq!(4, delayed_sum(2, 2, ms(10))); + } + + #[rstest] + #[timeout(ms(100))] + fn single_fail_value() { + assert_eq!(5, delayed_sum(2, 2, ms(1))); + } + + #[rstest] + #[timeout(ms(10))] + fn single_fail_timeout() { + assert_eq!(4, delayed_sum(2, 2, ms(80))); + } + #[rstest] #[timeout(ms(80))] #[case(ms(10))] @@ -39,7 +57,7 @@ mod thread { #[case::fail_timeout(ms(80), 4)] #[case::fail_value(ms(1), 5)] #[timeout(ms(40))] - fn group_same_timeout(#[case] delay: Duration, expected: u32) { + fn group_same_timeout(#[case] delay: Duration,#[case] expected: u32) { assert_eq!(expected, delayed_sum(2, 2, delay)); } @@ -50,7 +68,7 @@ mod thread { #[case::fail_timeout(ms(70), 4)] #[timeout(ms(100))] #[case::fail_value(ms(1), 5)] - fn group_single_timeout(#[case] delay: Duration, expected: u32) { + fn group_single_timeout(#[case] delay: Duration,#[case] expected: u32) { assert_eq!(expected, delayed_sum(2, 2, delay)); } @@ -60,71 +78,85 @@ mod thread { #[case::fail_timeout(ms(60), 4)] #[case::fail_value(ms(1), 5)] #[timeout(ms(100))] - fn group_one_timeout_override(#[case] delay: Duration, expected: u32) { + fn group_one_timeout_override(#[case] delay: Duration,#[case] expected: u32) { assert_eq!(expected, delayed_sum(2, 2, delay)); } } -mod async_cases { +mod async_std_cases { use super::*; - mod async_std { - use super::*; - - async fn delayed_sum(a: u32, b: u32, delay: Duration) -> u32 { - async_std::task::sleep(delay).await; - a + b - } - - #[rstest] - #[timeout(ms(80))] - #[case(ms(10))] - async fn one_pass(#[case] delay: Duration) { - assert_eq!(4, delayed_sum(2, 2, delay).await); - } - - #[rstest] - #[timeout(ms(10))] - #[case(ms(80))] - async fn one_fail_timeout(#[case] delay: Duration) { - assert_eq!(4, delayed_sum(2, 2, delay).await); - } - - #[rstest] - #[timeout(ms(100))] - #[case(ms(1))] - async fn one_fail_value(#[case] delay: Duration) { - assert_eq!(5, delayed_sum(2, 2, delay).await); - } - - #[rstest] - #[case::pass(ms(1), 4)] - #[case::fail_timeout(ms(80), 4)] - #[case::fail_value(ms(1), 5)] - #[timeout(ms(40))] - async fn group_same_timeout(#[case] delay: Duration, expected: u32) { - assert_eq!(expected, delayed_sum(2, 2, delay).await); - } - - #[rstest] - #[timeout(ms(100))] - #[case::pass(ms(1), 4)] - #[timeout(ms(30))] - #[case::fail_timeout(ms(70), 4)] - #[timeout(ms(100))] - #[case::fail_value(ms(1), 5)] - async fn group_single_timeout(#[case] delay: Duration, expected: u32) { - assert_eq!(expected, delayed_sum(2, 2, delay).await); - } - - #[rstest] - #[case::pass(ms(1), 4)] - #[timeout(ms(10))] - #[case::fail_timeout(ms(60), 4)] - #[case::fail_value(ms(1), 5)] - #[timeout(ms(100))] - async fn group_one_timeout_override(#[case] delay: Duration, expected: u32) { - assert_eq!(expected, delayed_sum(2, 2, delay).await); - } + async fn delayed_sum(a: u32, b: u32,delay: Duration) -> u32 { + async_std::task::sleep(delay).await; + a + b } -} + + #[rstest] + #[timeout(ms(80))] + async fn single_pass() { + assert_eq!(4, delayed_sum(2, 2, ms(10)).await); + } + + #[rstest] + #[timeout(ms(10))] + async fn single_fail_timeout() { + assert_eq!(4, delayed_sum(2, 2, ms(80)).await); + } + + #[rstest] + #[timeout(ms(100))] + async fn single_fail_value() { + assert_eq!(5, delayed_sum(2, 2, ms(1)).await); + } + + #[rstest] + #[timeout(ms(80))] + #[case(ms(10))] + async fn one_pass(#[case] delay: Duration) { + assert_eq!(4, delayed_sum(2, 2, delay).await); + } + + #[rstest] + #[timeout(ms(10))] + #[case(ms(80))] + async fn one_fail_timeout(#[case] delay: Duration) { + assert_eq!(4, delayed_sum(2, 2, delay).await); + } + + #[rstest] + #[timeout(ms(100))] + #[case(ms(1))] + async fn one_fail_value(#[case] delay: Duration) { + assert_eq!(5, delayed_sum(2, 2, delay).await); + } + + #[rstest] + #[case::pass(ms(1), 4)] + #[case::fail_timeout(ms(80), 4)] + #[case::fail_value(ms(1), 5)] + #[timeout(ms(40))] + async fn group_same_timeout(#[case] delay: Duration, #[case] expected: u32) { + assert_eq!(expected, delayed_sum(2, 2, delay).await); + } + + #[rstest] + #[timeout(ms(100))] + #[case::pass(ms(1), 4)] + #[timeout(ms(30))] + #[case::fail_timeout(ms(70), 4)] + #[timeout(ms(100))] + #[case::fail_value(ms(1), 5)] + async fn group_single_timeout(#[case] delay: Duration, #[case] expected: u32) { + assert_eq!(expected, delayed_sum(2, 2, delay).await); + } + + #[rstest] + #[case::pass(ms(1), 4)] + #[timeout(ms(10))] + #[case::fail_timeout(ms(60), 4)] + #[case::fail_value(ms(1), 5)] + #[timeout(ms(100))] + async fn group_one_timeout_override(#[case] delay: Duration, #[case] expected: u32) { + assert_eq!(expected, delayed_sum(2, 2, delay).await); + } +} \ No newline at end of file diff --git a/rstest/tests/rstest/mod.rs b/rstest/tests/rstest/mod.rs index 3b8090a..c3d31f1 100644 --- a/rstest/tests/rstest/mod.rs +++ b/rstest/tests/rstest/mod.rs @@ -893,6 +893,46 @@ fn ignore_underscore_args() { .assert(output); } +#[test] +fn timeout() { + let prj = prj("timeout.rs"); + prj.add_dependency("async-std", r#"{version="*", features=["attributes"]}"#); + let output = prj.run_tests().unwrap(); + + TestResults::new() + .ok("thread::single_pass") + .fail("thread::single_fail_value") + .fail("thread::single_fail_timeout") + .ok("thread::one_pass::case_1") + .fail("thread::one_fail_value::case_1") + .fail("thread::one_fail_timeout::case_1") + .ok("thread::group_same_timeout::case_1_pass") + .fail("thread::group_same_timeout::case_2_fail_timeout") + .fail("thread::group_same_timeout::case_3_fail_value") + .ok("thread::group_single_timeout::case_1_pass") + .fail("thread::group_single_timeout::case_2_fail_timeout") + .fail("thread::group_single_timeout::case_3_fail_value") + .ok("thread::group_one_timeout_override::case_1_pass") + .fail("thread::group_one_timeout_override::case_2_fail_timeout") + .fail("thread::group_one_timeout_override::case_3_fail_value") + .ok("async_std_cases::single_pass") + .fail("async_std_cases::single_fail_value") + .fail("async_std_cases::single_fail_timeout") + .ok("async_std_cases::one_pass::case_1") + .fail("async_std_cases::one_fail_value::case_1") + .fail("async_std_cases::one_fail_timeout::case_1") + .ok("async_std_cases::group_same_timeout::case_1_pass") + .fail("async_std_cases::group_same_timeout::case_2_fail_timeout") + .fail("async_std_cases::group_same_timeout::case_3_fail_value") + .ok("async_std_cases::group_single_timeout::case_1_pass") + .fail("async_std_cases::group_single_timeout::case_2_fail_timeout") + .fail("async_std_cases::group_single_timeout::case_3_fail_value") + .ok("async_std_cases::group_one_timeout_override::case_1_pass") + .fail("async_std_cases::group_one_timeout_override::case_2_fail_timeout") + .fail("async_std_cases::group_one_timeout_override::case_3_fail_value") + .assert(output); +} + mod should_show_correct_errors { use std::process::Output; @@ -1326,4 +1366,64 @@ mod should_show_correct_errors { .unindent() ); } + + #[test] + fn if_use_timeout_without_arg() { + let (output, name) = execute(); + + assert_in!( + output.stderr.str(), + format!( + " + error: expected attribute arguments in parentheses: #[timeout(...)] + --> {}/src/lib.rs:97:1 + | + 97 | #[timeout] + | ^^^^^^^^^^ + ", + name + ) + .unindent() + ); + } + + #[test] + fn if_timeout_is_not_an_expression() { + let (output, name) = execute(); + + assert_in!( + output.stderr.str(), + format!( + " + error: expected expression + --> {}/src/lib.rs:101:16 + | + 101 | #[timeout(some -> strange -> invalid -> expression)] + | ^^ + ", + name + ) + .unindent() + ); + } + + #[test] + fn if_timeout_is_not_a_duration() { + let (output, name) = execute(); + + assert_in!( + output.stderr.str(), + format!( + " + error[E0308]: mismatched types + --> {}/src/lib.rs:105:11 + | + 105 | #[timeout(42)] + | ^^ expected struct `Duration`, found integer + ", + name + ) + .unindent() + ); + } } diff --git a/rstest_macros/src/parse/mod.rs b/rstest_macros/src/parse/mod.rs index 61168fb..9f57b89 100644 --- a/rstest_macros/src/parse/mod.rs +++ b/rstest_macros/src/parse/mod.rs @@ -561,6 +561,47 @@ pub(crate) fn extract_excluded_trace(item_fn: &mut ItemFn) -> Result, excluded_trace_extractor.take() } +/// Simple struct used to visit function args attributes to check timeout syntax +struct CheckTimeoutAttributesFunction(Result<(), ErrorsVec>); +impl From for CheckTimeoutAttributesFunction { + fn from(errors: ErrorsVec) -> Self { + Self(Err(errors)) + } +} + +impl CheckTimeoutAttributesFunction { + pub(crate) fn take(self) -> Result<(), ErrorsVec> { + self.0 + } +} + +impl Default for CheckTimeoutAttributesFunction { + fn default() -> Self { + Self(Ok(Default::default())) + } +} + +impl VisitMut for CheckTimeoutAttributesFunction { + fn visit_item_fn_mut(&mut self, node: &mut ItemFn) { + let errors = node + .attrs + .iter() + .filter(|&a| attr_is(a, "timeout")) + .map(|attr| attr.parse_args::()) + .filter_map(Result::err) + .collect::>(); + if !errors.is_empty() { + *self = Self(Err(errors.into())); + } + } +} + +pub(crate) fn check_timeout_attrs(item_fn: &mut ItemFn) -> Result<(), ErrorsVec> { + let mut checker = CheckTimeoutAttributesFunction::default(); + checker.visit_item_fn_mut(item_fn); + checker.take() +} + #[cfg(test)] mod should { use super::*; diff --git a/rstest_macros/src/parse/rstest.rs b/rstest_macros/src/parse/rstest.rs index 7ad00ea..f661713 100644 --- a/rstest_macros/src/parse/rstest.rs +++ b/rstest_macros/src/parse/rstest.rs @@ -5,9 +5,9 @@ use syn::{ use super::testcase::TestCase; use super::{ - extract_case_args, extract_cases, extract_excluded_trace, extract_fixtures, extract_value_list, - parse_vector_trailing_till_double_comma, Attribute, Attributes, ExtendWithFunctionAttrs, - Fixture, + check_timeout_attrs, extract_case_args, extract_cases, extract_excluded_trace, + extract_fixtures, extract_value_list, parse_vector_trailing_till_double_comma, Attribute, + Attributes, ExtendWithFunctionAttrs, Fixture, }; use crate::parse::vlist::ValueList; use crate::{ @@ -41,9 +41,10 @@ impl Parse for RsTestInfo { impl ExtendWithFunctionAttrs for RsTestInfo { fn extend_with_function_attrs(&mut self, item_fn: &mut ItemFn) -> Result<(), ErrorsVec> { - let (_, excluded) = merge_errors!( + let (_, (excluded, _)) = merge_errors!( self.data.extend_with_function_attrs(item_fn), - extract_excluded_trace(item_fn) + extract_excluded_trace(item_fn), + check_timeout_attrs(item_fn) )?; self.attributes.add_notraces(excluded); Ok(()) @@ -259,7 +260,7 @@ impl Parse for RsTestAttributes { #[cfg(test)] mod test { use super::*; - use crate::test::*; + use crate::test::{assert_eq, *}; mod parse_rstest_data { use super::assert_eq; @@ -281,6 +282,25 @@ mod test { } } + #[test] + fn should_check_all_timeout_to_catch_the_right_errors() { + let mut item_fn = r#" + #[timeout()] + #[timeout(42)] + #[timeout] + #[timeout(Duration::from_millis(20))] + fn test_fn(#[case] arg: u32) { + } + "# + .ast(); + + let mut info = RsTestInfo::default(); + + let errors = info.extend_with_function_attrs(&mut item_fn).unwrap_err(); + + assert_eq!(2, errors.len()); + } + fn parse_rstest>(rstest_data: S) -> RsTestInfo { parse_meta(rstest_data) } diff --git a/rstest_macros/src/render/mod.rs b/rstest_macros/src/render/mod.rs index 1b99879..6c3afb0 100644 --- a/rstest_macros/src/render/mod.rs +++ b/rstest_macros/src/render/mod.rs @@ -178,6 +178,25 @@ fn render_exec_call(fn_path: Path, args: &[Ident], is_async: bool) -> TokenStrea } } +fn render_test_call( + fn_path: Path, + args: &[Ident], + timeout: Option, + is_async: bool, +) -> TokenStream { + match (timeout, is_async) { + (Some(to_expr), true) => quote! { + use rstest::timeout::*; + execute_with_timeout_async(move || #fn_path(#(#args),*), #to_expr).await + }, + (Some(to_expr), false) => quote! { + use rstest::timeout::*; + execute_with_timeout_sync(move || #fn_path(#(#args),*), #to_expr) + }, + _ => render_exec_call(fn_path, args, is_async), + } +} + /// Render a single test case: /// /// * `name` - Test case name @@ -220,6 +239,14 @@ fn single_test_case<'a>( let trace_args = trace_arguments(args.iter(), &attributes); let is_async = asyncness.is_some(); + let (attrs, timeouts): (Vec<_>, Vec<_>) = + attrs.iter().cloned().partition(|a| !attr_is(a, "timeout")); + + let timeout = timeouts + .into_iter() + .last() + .map(|attribute| attribute.parse_args::().unwrap()); + // If no injected attribut provided use the default one let test_attr = if attrs .iter() @@ -229,7 +256,7 @@ fn single_test_case<'a>( } else { Some(resolve_default_test_attr(is_async)) }; - let execute = render_exec_call(testfn_name.clone().into(), &args, is_async); + let execute = render_test_call(testfn_name.clone().into(), &args, timeout, is_async); quote! { #test_attr