diff --git a/resources/rstest/happy_path.rs b/resources/rstest/happy_path.rs new file mode 100644 index 00000000..7bfaa8cf --- /dev/null +++ b/resources/rstest/happy_path.rs @@ -0,0 +1,24 @@ +use rstest::*; + +#[fixture] +fn inject() -> u32 { 0 } + +#[fixture] +fn ex() -> u32 { 42 } + +#[fixture] +fn fix(inject: u32, ex: u32) -> bool { (inject * 2) == ex } + +#[rstest( + fix(21), + a, b, + case(21, 2), + case::second(14, 3), + expected => [4, 2*3-2], + input => ["ciao", "buzz"], +)] +fn happy(fix: bool, a: u32, b: u32, expected: usize, input: &str) { + assert!(fix); + assert_eq!(a*b, 42); + assert_eq!(expected, input.len()); +} diff --git a/src/lib.rs b/src/lib.rs index 1ce7fba8..59b8996a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -570,10 +570,10 @@ pub fn rstest(args: proc_macro::TokenStream, let errors = errors_in_rstest(&test, &info); if errors.is_empty() { - if info.data.has_cases() { - render_parametrize_cases(test, info) - } else if info.data.has_list_values() { + if info.data.has_list_values() { render_matrix_cases(test, info) + } else if info.data.has_list_values() { + render_parametrize_cases(test, info) } else { render_single_case(test, info) } @@ -670,14 +670,14 @@ fn errors_in_fixture(test: &ItemFn, info: &parse::fixture::FixtureInfo) -> Token .collect() } -struct CaseRender { +struct TestCaseRender { name: Ident, resolver: R, } -impl CaseRender { +impl TestCaseRender { pub fn new(name: Ident, resolver: R) -> Self { - CaseRender { name, resolver } + TestCaseRender { name, resolver } } fn render(self, testfn: &ItemFn, attributes: &RsTestAttributes) -> TokenStream { @@ -685,7 +685,7 @@ impl CaseRender { } } -fn render_cases(test: ItemFn, cases: impl Iterator>, attributes: RsTestAttributes) -> TokenStream { +fn render_cases(test: ItemFn, cases: impl Iterator>, attributes: RsTestAttributes) -> TokenStream { let fname = &test.sig.ident; let cases = cases.map(|case| case.render(&test, &attributes)); @@ -736,8 +736,8 @@ fn render_parametrize_cases(test: ItemFn, info: RsTestInfo) -> TokenStream { .map(|a| a.to_string()) .zip(case.args.iter()) .collect::>(); - CaseRender::new(Ident::new(&format_case_name(case, n + 1, display_len), span), - (resolver_case, resolver_fixtures)) + TestCaseRender::new(Ident::new(&format_case_name(case, n + 1, display_len), span), + (resolver_case, resolver_fixtures)) } } ); @@ -748,36 +748,117 @@ fn render_parametrize_cases(test: ItemFn, info: RsTestInfo) -> TokenStream { fn render_matrix_cases(test: ItemFn, info: parse::rstest::RsTestInfo) -> TokenStream { let parse::rstest::RsTestInfo { data, attributes, .. } = info; let span = test.sig.ident.span(); + let fname = &test.sig.ident; - // Steps: - // 1. pack data P=(ident, expr, (pos, max_len)) in one iterator for each variable - // 2. do a cartesian product of iterators to build all cases (every case is a vector of P) - // 3. format case by packed data vector - let cases = data.list_values() - .map(|group| - group.values.iter() - .enumerate() - .map(move |(pos, expr)| (&group.arg, expr, (pos, group.values.len()))) - ) - .multi_cartesian_product() - .map(|c| { - let args_indexes = c.iter() - .map(|(_, _, (index, max))| - format!("{:0len$}", index + 1, len = max.display_len()) - ) - .collect::>() - .join("_"); - let name = format!("case_{}", args_indexes); - let resolver_fixture = resolver::fixture_resolver(data.fixtures()); - let resolver_case = c.into_iter() - .map(|(a, e, _)| (a.to_string(), e)) - .collect::>(); - CaseRender::new(Ident::new(&name, span), - (resolver_case, resolver_fixture)) + let display_len = data.cases().count().display_len(); + let cases: Vec<(_, _)> = data.cases() + .enumerate() + .map({ + let data = &data; + move |(n, case)| + { + let resolver_case = data.case_args() + .map(|a| a.to_string()) + .zip(case.args.iter()) + .collect::>(); + (resolver_case, Ident::new(&format_case_name(case, n + 1, display_len), span)) + } } + ).collect(); + + let base_resolver = resolver::fixture_resolver(data.fixtures()); + if cases.is_empty() { + // Steps: + // 1. pack data P=(ident, expr, (pos, max_len)) in one iterator for each variable + // 2. do a cartesian product of iterators to build all cases (every case is a vector of P) + // 3. format case by packed data vector + let test_cases = data.list_values() + .map(|group| + group.values.iter() + .enumerate() + .map(move |(pos, expr)| (&group.arg, expr, (pos, group.values.len()))) + ) + .multi_cartesian_product() + .map(|c| { + let args_indexes = c.iter() + .map(|(_, _, (index, max))| + format!("{:0len$}", index + 1, len = max.display_len()) + ) + .collect::>() + .join("_"); + let name = format!("case_{}", args_indexes); + let resolver_case = c.into_iter() + .map(|(a, e, _)| (a.to_string(), e)) + .collect::>(); + TestCaseRender::new(Ident::new(&name, span), + (resolver_case, &base_resolver)) + } + ); + + let test_cases = test_cases.map(|test_case| test_case.render(&test, &attributes)); + + quote! { + #[cfg(test)] + #test + + #[cfg(test)] + mod #fname { + use super::*; + + #(#test_cases)* + } + } + } else { + + let test_cases = cases.into_iter().map( + |(resolver, case_name)| { + let resolver = (resolver, &base_resolver); + let test_cases = data.list_values() + .map(|group| + group.values.iter() + .enumerate() + .map(move |(pos, expr)| (&group.arg, expr, (pos, group.values.len()))) + ) + .multi_cartesian_product() + .map(|c| { + let args_indexes = c.iter() + .map(|(_, _, (index, max))| + format!("{:0len$}", index + 1, len = max.display_len()) + ) + .collect::>() + .join("_"); + let name = format!("case_{}", args_indexes); + let resolver_case = c.into_iter() + .map(|(a, e, _)| (a.to_string(), e)) + .collect::>(); + TestCaseRender::new(Ident::new(&name, span), + (resolver_case, &resolver) + ) + } + ) + .map(|test_case| test_case.render(&test, &attributes)); + quote! { + mod #case_name { + use super::*; + + #(#test_cases)* + } + } + } ); - render_cases(test, cases, attributes) + quote! { + #[cfg(test)] + #test + + #[cfg(test)] + mod #fname { + use super::*; + + #(#test_cases)* + } + } + } } /// Write table-based tests: you must indicate the arguments that you want use in your cases @@ -943,14 +1024,13 @@ pub fn rstest_matrix(args: proc_macro::TokenStream, input: proc_macro::TokenStre #[cfg(test)] mod render { - use pretty_assertions::assert_eq; use syn::{ ItemFn, ItemMod, parse::{Parse, ParseStream, Result}, parse2, parse_str, punctuated, visit::Visit, }; use crate::resolver::*; - use crate::test::*; + use crate::test::{*, fixture, assert_eq}; use super::*; @@ -1095,6 +1175,16 @@ mod render { } } + /// To extract all submodules + struct SubModules(Vec); + + impl<'ast> Visit<'ast> for SubModules { + //noinspection RsTypeCheck + fn visit_item_mod(&mut self, item_mod: &'ast ItemMod) { + self.0.push(item_mod.clone()) + } + } + impl TestsGroup { pub fn get_test_functions(&self) -> Vec { let mut f = TestFunctions(vec![]); @@ -1102,6 +1192,13 @@ mod render { f.visit_item_mod(&self.module); f.0 } + + pub fn get_submodules(&self) -> Vec { + let mut f = SubModules(vec![]); + + self.module.content.as_ref().map(|(_, items)| items.iter().for_each(|it| f.visit_item(it))); + f.0 + } } impl From for TestsGroup { @@ -1111,8 +1208,6 @@ mod render { } mod cases { - use std::iter::FromIterator; - use crate::parse::{ rstest::{RsTestInfo, RsTestData, RsTestItem}, testcase::TestCase, @@ -1129,18 +1224,11 @@ mod render { } } - fn into_rstest_info(item_fn: &ItemFn) -> RsTestInfo { - RsTestInfo { - data: into_rstest_data(item_fn), - attributes: Default::default(), - } - } - #[test] fn should_create_a_module_named_as_test_function() { let item_fn = parse_str::("fn should_be_the_module_name(mut fix: String) {}").unwrap(); - let info = into_rstest_info(&item_fn); - let tokens = render_parametrize_cases(item_fn.clone(), info); + let data = into_rstest_data(&item_fn); + let tokens = render_parametrize_cases(item_fn.clone(), data.into()); let output = TestsGroup::from(tokens); @@ -1152,8 +1240,8 @@ mod render { let item_fn = parse_str::( r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"# ).unwrap(); - let info = into_rstest_info(&item_fn); - let tokens = render_parametrize_cases(item_fn.clone(), info); + let data = into_rstest_data(&item_fn); + let tokens = render_parametrize_cases(item_fn.clone(), data.into()); let mut output = TestsGroup::from(tokens); @@ -1166,8 +1254,8 @@ mod render { let item_fn = parse_str::( r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"# ).unwrap(); - let info = into_rstest_info(&item_fn); - let tokens = render_parametrize_cases(item_fn.clone(), info); + let data = into_rstest_data(&item_fn); + let tokens = render_parametrize_cases(item_fn.clone(), data.into()); let output = TestsGroup::from(tokens); @@ -1184,8 +1272,8 @@ mod render { let item_fn = parse_str::( r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"# ).unwrap(); - let info = into_rstest_info(&item_fn); - let tokens = render_parametrize_cases(item_fn.clone(), info); + let data = into_rstest_data(&item_fn); + let tokens = render_parametrize_cases(item_fn.clone(), data.into()); let output = TestsGroup::from(tokens); @@ -1197,38 +1285,12 @@ mod render { assert_eq!(expected, output.module.attrs); } - impl RsTestInfo { - fn push_case(&mut self, case: TestCase) { - self.data.items.push(RsTestItem::TestCase(case)); - } - - fn extend(&mut self, cases: impl Iterator) { - self.data.items.extend(cases.map(RsTestItem::TestCase)); - } - } - - impl<'a> FromIterator<&'a str> for TestCase { - fn from_iter>(iter: T) -> Self { - TestCase { - args: iter.into_iter() - .map(expr) - .collect(), - description: None, - } - } - } - - impl<'a> From<&'a str> for TestCase { - fn from(argument: &'a str) -> Self { - std::iter::once(argument).collect() - } - } fn one_simple_case() -> (ItemFn, RsTestInfo) { let item_fn = parse_str::( r#"fn test(mut fix: String) { println!("user code") }"# ).unwrap(); - let mut info: RsTestInfo = into_rstest_info(&item_fn); + let mut info: RsTestInfo = into_rstest_data(&item_fn).into(); info.push_case(TestCase::from(r#"String::from("3")"#)); (item_fn, info) } @@ -1237,7 +1299,7 @@ mod render { let item_fn = parse_str::( r#"fn test(mut fix: String) { println!("user code") }"# ).unwrap(); - let mut info: RsTestInfo = into_rstest_info(&item_fn); + let mut info: RsTestInfo = into_rstest_data(&item_fn).into(); info.extend((0..cases).map(|_| TestCase::from(r#"String::from("3")"#))); (item_fn, info) } @@ -1336,18 +1398,11 @@ mod render { } } - fn into_rstest_info(item_fn: &ItemFn) -> RsTestInfo { - RsTestInfo { - data: into_rstest_data(item_fn), - attributes: Default::default(), - } - } - #[test] fn should_create_a_module_named_as_test_function() { let item_fn = parse_str::("fn should_be_the_module_name(mut fix: String) {}").unwrap(); - let info = into_rstest_info(&item_fn); - let tokens = render_matrix_cases(item_fn.clone(), info); + let data = into_rstest_data(&item_fn); + let tokens = render_matrix_cases(item_fn.clone(), data.into()); let output = TestsGroup::from(tokens); @@ -1359,8 +1414,8 @@ mod render { let item_fn = parse_str::( r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"# ).unwrap(); - let info = into_rstest_info(&item_fn); - let tokens = render_matrix_cases(item_fn.clone(), info); + let data = into_rstest_data(&item_fn); + let tokens = render_matrix_cases(item_fn.clone(), data.into()); let mut output = TestsGroup::from(tokens); @@ -1373,8 +1428,8 @@ mod render { let item_fn = parse_str::( r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"# ).unwrap(); - let info = into_rstest_info(&item_fn); - let tokens = render_matrix_cases(item_fn.clone(), info); + let data = into_rstest_data(&item_fn); + let tokens = render_matrix_cases(item_fn.clone(), data.into()); let output = TestsGroup::from(tokens); @@ -1391,8 +1446,8 @@ mod render { let item_fn = parse_str::( r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"# ).unwrap(); - let info = into_rstest_info(&item_fn); - let tokens = render_matrix_cases(item_fn.clone(), info); + let data = into_rstest_data(&item_fn); + let tokens = render_matrix_cases(item_fn.clone(), data.into()); let output = TestsGroup::from(tokens); @@ -1494,6 +1549,34 @@ mod render { } } + mod cases_and_values_lists { + use super::{*, assert_eq}; + + #[test] + fn should_module_for_each_cases() { + let item_fn = parse_str::(r#" + fn should_be_the_outer_module_name( + fix: u32, + a: f64, b: f32, + x: i32, y: i32) {}"#).unwrap(); + let data = RsTestData { + items: vec![fixture("fix", vec!["2"]).into(), + ident("a").into(), ident("b").into(), + vec!["1f64", "2f32"].into_iter().collect::().into(), + vec!["3f64", "4f32"].into_iter().collect::().into(), + values_list("x", &["12", "-2"]).into(), + values_list("y", &["-3", "42"]).into(), + ], + }; + let tokens = render_matrix_cases(item_fn.clone(), data.into()); + + let output = TestsGroup::from(tokens); + + assert_eq!(output.module.ident, "should_be_the_outer_module_name"); + assert_eq!(2, output.get_submodules().len()); + } + } + mod generics_clean_up { use super::{*, assert_eq}; diff --git a/src/resolver.rs b/src/resolver.rs index 9251a638..491a5bc5 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -11,7 +11,7 @@ use proc_macro2::{Ident, Span}; use crate::parse::Fixture; -pub(crate) fn fixture_resolver<'a>(fixtures: impl Iterator) -> impl Resolver + 'a { +pub(crate) fn fixture_resolver<'a>(fixtures: impl Iterator) -> impl Resolver + 'a { fixtures.map(|f| ( f.name.to_string(), extract_resolve_expression(f).into() ) ).collect::>() @@ -54,6 +54,12 @@ impl Resolver for (R1, R2) { } } +impl Resolver for &R { + fn resolve(&self, ident: &Ident) -> Option> { + (*self).resolve(ident) + } +} + fn default_fixture_resolve(ident: &Ident) -> Cow { Cow::Owned(parse_quote! { #ident::default() } ) } diff --git a/src/test.rs b/src/test.rs index 4f5e467a..400cc939 100644 --- a/src/test.rs +++ b/src/test.rs @@ -18,6 +18,7 @@ use crate::parse::{ testcase::TestCase, }; use syn::parse::Parse; +use std::iter::FromIterator; macro_rules! to_args { ($e:expr) => { @@ -133,9 +134,42 @@ impl Attribute { } } +impl RsTestInfo { + pub fn push_case(&mut self, case: TestCase) { + self.data.items.push(RsTestItem::TestCase(case)); + } + + pub fn extend(&mut self, cases: impl Iterator) { + self.data.items.extend(cases.map(RsTestItem::TestCase)); + } +} + +impl<'a> FromIterator<&'a str> for TestCase { + fn from_iter>(iter: T) -> Self { + TestCase { + args: iter.into_iter() + .map(expr) + .collect(), + description: None, + } + } +} + +impl<'a> From<&'a str> for TestCase { + fn from(argument: &'a str) -> Self { + std::iter::once(argument).collect() + } +} + impl From> for RsTestData { - fn from(fixtures: Vec) -> Self { - Self { items: fixtures } + fn from(items: Vec) -> Self { + Self { items } + } +} + +impl From for RsTestInfo { + fn from(data: RsTestData) -> Self { + Self { data, attributes: Default::default() } } } diff --git a/tests/rstest/mod.rs b/tests/rstest/mod.rs index a0ca6f70..eba69ea5 100644 --- a/tests/rstest/mod.rs +++ b/tests/rstest/mod.rs @@ -519,6 +519,22 @@ mod matrix { } } +#[test] +fn happay_path() { + let (output, _) = run_test("happy_path.rs"); + + TestResults::new() + .ok("happy::case_1::case_1_1") + .ok("happy::case_1::case_1_2") + .ok("happy::case_1::case_2_1") + .ok("happy::case_1::case_2_2") + .ok("happy::case_2_second::case_1_1") + .ok("happy::case_2_second::case_1_2") + .ok("happy::case_2_second::case_2_1") + .ok("happy::case_2_second::case_2_2") + .assert(output); +} + mod should_show_correct_errors { use std::process::Output;