diff --git a/CHANGELOG.md b/CHANGELOG.md index f16533b..293014f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [Unreleased] +### New features +* Add `test_matrix` macro: generates test cases from Cartesian product of possible test function argument values. + ## 3.1.0 ### New features * Copy attribute span to generated test functions so that IDEs recognize them properly as individual tests diff --git a/README.md b/README.md index c94d505..8566a7a 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,39 @@ test tests::multiplication_tests::when_operands_are_swapped ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` +### Test Matrix + +The `#[test_matrix(...)]` macro allows generating multiple test cases from the +Cartesian product of one or more possible values for each test function argument. The +number of arguments to the `test_matrix` macro must be the same as the number of arguments to +the test function. Each macro argument can be: + +1. A list in array (`[x, y, ...]`) or tuple (`(x, y, ...)`) syntax. The values can be any + valid [expression](https://doc.rust-lang.org/reference/expressions.html). +2. A closed numeric range expression (e.g. `0..100` or `1..=99`), which will generate + argument values for all integers in the range. +3. A single expression, which can be used to keep one argument constant while varying the + other test function arguments using a list or range. + +#### Example usage: + +```rust +#[cfg(test)] +mod tests { + use test_case::test_matrix; + + #[test_matrix( + [-2, 2], + [-4, 4] + )] + fn multiplication_tests(x: i8, y: i8) { + let actual = (x * y).abs(); + + assert_eq!(8, actual) + } +} +``` + ## MSRV Policy Starting with version 3.0 and up `test-case` introduces policy of only supporting latest stable Rust. diff --git a/crates/test-case-core/src/comment.rs b/crates/test-case-core/src/comment.rs index aa52503..610c970 100644 --- a/crates/test-case-core/src/comment.rs +++ b/crates/test-case-core/src/comment.rs @@ -1,7 +1,9 @@ +use crate::TokenStream2; +use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::{LitStr, Token}; -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct TestCaseComment { _semicolon: Token![;], pub comment: LitStr, @@ -16,6 +18,13 @@ impl Parse for TestCaseComment { } } +impl ToTokens for TestCaseComment { + fn to_tokens(&self, tokens: &mut TokenStream2) { + self._semicolon.to_tokens(tokens); + self.comment.to_tokens(tokens); + } +} + #[cfg(test)] mod tests { use crate::comment::TestCaseComment; diff --git a/crates/test-case-core/src/complex_expr.rs b/crates/test-case-core/src/complex_expr.rs index 2fabde2..e708f2e 100644 --- a/crates/test-case-core/src/complex_expr.rs +++ b/crates/test-case-core/src/complex_expr.rs @@ -38,7 +38,7 @@ mod kw { syn::custom_keyword!(matches_regex); } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum OrderingToken { Eq, Lt, @@ -47,57 +47,57 @@ pub enum OrderingToken { Geq, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum PathToken { Any, Dir, File, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Ord { pub token: OrderingToken, pub expected_value: Box, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct AlmostEqual { pub expected_value: Box, pub precision: Box, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Path { pub token: PathToken, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Contains { pub expected_element: Box, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ContainsInOrder { pub expected_slice: Box, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Len { pub expected_len: Box, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Count { pub expected_len: Box, } #[cfg(feature = "with-regex")] -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Regex { pub expected_regex: Box, } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum ComplexTestCase { Not(Box), And(Vec), diff --git a/crates/test-case-core/src/expr.rs b/crates/test-case-core/src/expr.rs index 8747399..f93fe3c 100644 --- a/crates/test-case-core/src/expr.rs +++ b/crates/test-case-core/src/expr.rs @@ -18,14 +18,14 @@ pub mod kw { syn::custom_keyword!(panics); } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct TestCaseExpression { _token: Token![=>], pub extra_keywords: HashSet, pub result: TestCaseResult, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum TestCaseResult { // test_case(a, b, c => keywords) Empty, diff --git a/crates/test-case-core/src/lib.rs b/crates/test-case-core/src/lib.rs index eef3031..d127418 100644 --- a/crates/test-case-core/src/lib.rs +++ b/crates/test-case-core/src/lib.rs @@ -5,6 +5,8 @@ mod complex_expr; mod expr; mod modifier; mod test_case; +mod test_matrix; mod utils; pub use test_case::TestCase; +pub use test_matrix::TestMatrix; diff --git a/crates/test-case-core/src/modifier.rs b/crates/test-case-core/src/modifier.rs index f78da15..42417dc 100644 --- a/crates/test-case-core/src/modifier.rs +++ b/crates/test-case-core/src/modifier.rs @@ -9,7 +9,7 @@ mod kw { syn::custom_keyword!(ignore); } -#[derive(PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash)] pub enum Modifier { Inconclusive, InconclusiveWithReason(LitStr), diff --git a/crates/test-case-core/src/test_case.rs b/crates/test-case-core/src/test_case.rs index a0cd262..b9b643a 100644 --- a/crates/test-case-core/src/test_case.rs +++ b/crates/test-case-core/src/test_case.rs @@ -10,8 +10,8 @@ use syn::{parse_quote, Error, Expr, Ident, ItemFn, ReturnType, Token}; #[derive(Debug)] pub struct TestCase { args: Punctuated, - expression: Option, - comment: Option, + pub(crate) expression: Option, + pub(crate) comment: Option, } impl Parse for TestCase { @@ -24,6 +24,19 @@ impl Parse for TestCase { } } +impl From for TestCase +where + I: IntoIterator, +{ + fn from(into_iter: I) -> Self { + Self { + args: into_iter.into_iter().collect(), + expression: None, + comment: None, + } + } +} + impl TestCase { pub fn test_case_name(&self) -> Ident { let case_desc = self diff --git a/crates/test-case-core/src/test_matrix/matrix_product.rs b/crates/test-case-core/src/test_matrix/matrix_product.rs new file mode 100644 index 0000000..abfb41c --- /dev/null +++ b/crates/test-case-core/src/test_matrix/matrix_product.rs @@ -0,0 +1,283 @@ +//! Copied with minor modifications from itertools v0.11.0 +//! under MIT License +//! +//! Modifications called out in "(MOD)" comments, below +//! +//! Source file and commit hash: +//! https://github.com/rust-itertools/itertools/blob/v0.11.0/src/adaptors/multi_product.rs +//! ed6fbda086a913a787450a642acfd4d36dc07c3b + +#[derive(Clone)] +/// An iterator adaptor that iterates over the cartesian product of +/// multiple iterators of type `I`. +/// +/// An iterator element type is `Vec`. +/// +/// See [`.multi_cartesian_product()`](crate::Itertools::multi_cartesian_product) +/// for more information. +#[must_use = "iterator adaptors are lazy and do nothing unless consumed"] +pub struct MultiProduct(Vec>) +where + I: Iterator + Clone, + I::Item: Clone; + +// (MOD) Omit `impl Debug for MultiProduct` +// Not needed here and relies on a macro from itertools + +/// Create a new cartesian product iterator over an arbitrary number +/// of iterators of the same type. +/// +/// Iterator element is of type `Vec`. +pub fn multi_cartesian_product(iters: H) -> MultiProduct<::IntoIter> +where + H: Iterator, + H::Item: IntoIterator, + ::IntoIter: Clone, + ::Item: Clone, +{ + MultiProduct( + iters + .map(|i| MultiProductIter::new(i.into_iter())) + .collect(), + ) +} + +#[derive(Clone, Debug)] +/// Holds the state of a single iterator within a `MultiProduct`. +struct MultiProductIter +where + I: Iterator + Clone, + I::Item: Clone, +{ + cur: Option, + iter: I, + iter_orig: I, +} + +/// Holds the current state during an iteration of a `MultiProduct`. +#[derive(Debug)] +enum MultiProductIterState { + StartOfIter, + MidIter { on_first_iter: bool }, +} + +impl MultiProduct +where + I: Iterator + Clone, + I::Item: Clone, +{ + /// Iterates the rightmost iterator, then recursively iterates iterators + /// to the left if necessary. + /// + /// Returns true if the iteration succeeded, else false. + fn iterate_last( + multi_iters: &mut [MultiProductIter], + mut state: MultiProductIterState, + ) -> bool { + use self::MultiProductIterState::*; + + if let Some((last, rest)) = multi_iters.split_last_mut() { + let on_first_iter = match state { + StartOfIter => { + let on_first_iter = !last.in_progress(); + state = MidIter { on_first_iter }; + on_first_iter + } + MidIter { on_first_iter } => on_first_iter, + }; + + if !on_first_iter { + last.iterate(); + } + + if last.in_progress() { + true + } else if MultiProduct::iterate_last(rest, state) { + last.reset(); + last.iterate(); + // If iterator is None twice consecutively, then iterator is + // empty; whole product is empty. + last.in_progress() + } else { + false + } + } else { + // Reached end of iterator list. On initialisation, return true. + // At end of iteration (final iterator finishes), finish. + match state { + StartOfIter => false, + MidIter { on_first_iter } => on_first_iter, + } + } + } + + /// Returns the unwrapped value of the next iteration. + fn curr_iterator(&self) -> Vec { + self.0 + .iter() + .map(|multi_iter| multi_iter.cur.clone().unwrap()) + .collect() + } + + /// Returns true if iteration has started and has not yet finished; false + /// otherwise. + fn in_progress(&self) -> bool { + if let Some(last) = self.0.last() { + last.in_progress() + } else { + false + } + } +} + +impl MultiProductIter +where + I: Iterator + Clone, + I::Item: Clone, +{ + fn new(iter: I) -> Self { + MultiProductIter { + cur: None, + iter: iter.clone(), + iter_orig: iter, + } + } + + /// Iterate the managed iterator. + fn iterate(&mut self) { + self.cur = self.iter.next(); + } + + /// Reset the managed iterator. + fn reset(&mut self) { + self.iter = self.iter_orig.clone(); + } + + /// Returns true if the current iterator has been started and has not yet + /// finished; false otherwise. + fn in_progress(&self) -> bool { + self.cur.is_some() + } +} + +impl Iterator for MultiProduct +where + I: Iterator + Clone, + I::Item: Clone, +{ + type Item = Vec; + + fn next(&mut self) -> Option { + if MultiProduct::iterate_last(&mut self.0, MultiProductIterState::StartOfIter) { + Some(self.curr_iterator()) + } else { + None + } + } + + fn count(self) -> usize { + if self.0.is_empty() { + return 0; + } + + if !self.in_progress() { + return self + .0 + .into_iter() + .fold(1, |acc, multi_iter| acc * multi_iter.iter.count()); + } + + self.0.into_iter().fold( + 0, + |acc, + MultiProductIter { + iter, + iter_orig, + cur: _, + }| { + let total_count = iter_orig.count(); + let cur_count = iter.count(); + acc * total_count + cur_count + }, + ) + } + + fn size_hint(&self) -> (usize, Option) { + // Not ExactSizeIterator because size may be larger than usize + if self.0.is_empty() { + return (0, Some(0)); + } + + if !self.in_progress() { + return self.0.iter().fold((1, Some(1)), |acc, multi_iter| { + size_hint::mul(acc, multi_iter.iter.size_hint()) + }); + } + + // (MOD) Clippy warning about unnecessary `ref` destructuring of `MultiProductIter` + // Removed redundant `&` and `ref` + self.0.iter().fold( + (0, Some(0)), + |acc, + MultiProductIter { + iter, + iter_orig, + cur: _, + }| { + let cur_size = iter.size_hint(); + let total_size = iter_orig.size_hint(); + size_hint::add(size_hint::mul(acc, total_size), cur_size) + }, + ) + } + + fn last(self) -> Option { + let iter_count = self.0.len(); + + // (MOD) Replaced `itertools::Itertools::while_some()` + // `std::iter::Iterator::filter_map()` does the same + // thing in this case, and doesn't require the `Itertools` trait + let lasts: Self::Item = self + .0 + .into_iter() + .filter_map(|multi_iter| multi_iter.iter.last()) + .collect(); + + if lasts.len() == iter_count { + Some(lasts) + } else { + None + } + } +} + +// (MOD) Copied two required functions and type alias +// From itertools::size_hint module +mod size_hint { + /// `SizeHint` is the return type of `Iterator::size_hint()`. + pub type SizeHint = (usize, Option); + + /// Add `SizeHint` correctly. + #[inline] + pub fn add(a: SizeHint, b: SizeHint) -> SizeHint { + let min = a.0.saturating_add(b.0); + let max = match (a.1, b.1) { + (Some(x), Some(y)) => x.checked_add(y), + _ => None, + }; + + (min, max) + } + + /// Multiply `SizeHint` correctly + #[inline] + pub fn mul(a: SizeHint, b: SizeHint) -> SizeHint { + let low = a.0.saturating_mul(b.0); + let hi = match (a.1, b.1) { + (Some(x), Some(y)) => x.checked_mul(y), + (Some(0), None) | (None, Some(0)) => Some(0), + _ => None, + }; + (low, hi) + } +} diff --git a/crates/test-case-core/src/test_matrix/mod.rs b/crates/test-case-core/src/test_matrix/mod.rs new file mode 100644 index 0000000..7fdd101 --- /dev/null +++ b/crates/test-case-core/src/test_matrix/mod.rs @@ -0,0 +1,111 @@ +use std::{iter, mem}; + +use proc_macro2::{Literal, Span}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + Expr, ExprLit, ExprRange, Lit, RangeLimits, Token, +}; + +use crate::{comment::TestCaseComment, expr::TestCaseExpression, TestCase}; + +mod matrix_product; + +#[derive(Debug, Default)] +pub struct TestMatrix { + variables: Vec>, + expression: Option, +} + +impl TestMatrix { + pub fn push_argument(&mut self, values: Vec) { + self.variables.push(values); + } + + pub fn cases(&self) -> impl Iterator { + let expression = self.expression.clone(); + + matrix_product::multi_cartesian_product(self.variables.iter().cloned()).map(move |v| { + let mut case = TestCase::from(v); + case.expression = expression.clone(); + case + }) + } +} + +impl Parse for TestMatrix { + fn parse(input: ParseStream) -> syn::Result { + let args: Punctuated = Punctuated::parse_separated_nonempty(input)?; + + let mut matrix = TestMatrix { + expression: input.parse().ok(), + ..Default::default() + }; + + if let Ok(c) = input.parse::() { + return Err(syn::Error::new( + c.span(), + "Comments are not allowed in #[test_matrix]", + )); + } + + for arg in args { + let values: Vec = match &arg { + Expr::Array(v) => v.elems.iter().cloned().collect(), + Expr::Tuple(v) => v.elems.iter().cloned().collect(), + Expr::Range(ExprRange { + from, limits, to, .. + }) => { + let start = isize_from_range_expr(limits.span(), from.as_deref())?; + let end = isize_from_range_expr(limits.span(), to.as_deref())?; + let range: Box> = match limits { + RangeLimits::HalfOpen(_) => Box::from(start..end), + RangeLimits::Closed(_) => Box::from(start..=end), + }; + range + .map(|n| { + let mut lit = Lit::new(Literal::isize_unsuffixed(n)); + lit.set_span(arg.span()); + Expr::from(ExprLit { lit, attrs: vec![] }) + }) + .collect() + } + v => iter::once(v.clone()).collect(), + }; + + let mut value_literal_type = None; + for expr in &values { + if let Expr::Lit(ExprLit { lit, .. }) = expr { + let first_literal_type = + *value_literal_type.get_or_insert_with(|| mem::discriminant(lit)); + if first_literal_type != mem::discriminant(lit) { + return Err(syn::Error::new( + lit.span(), + "All literal values must be of the same type", + )); + } + } + } + matrix.push_argument(values); + } + + Ok(matrix) + } +} + +fn isize_from_range_expr(limits_span: Span, expr: Option<&Expr>) -> syn::Result { + match expr { + Some(Expr::Lit(ExprLit { + lit: Lit::Int(n), .. + })) => n.base10_parse(), + Some(e) => Err(syn::Error::new( + e.span(), + "Range bounds can only be an integer literal", + )), + None => Err(syn::Error::new( + limits_span, + "Unbounded ranges are not supported", + )), + } +} diff --git a/crates/test-case-macros/src/lib.rs b/crates/test-case-macros/src/lib.rs index eeb2b63..0305572 100644 --- a/crates/test-case-macros/src/lib.rs +++ b/crates/test-case-macros/src/lib.rs @@ -8,7 +8,7 @@ use syn::{parse_macro_input, ItemFn}; use quote::quote; use syn::parse_quote; use syn::spanned::Spanned; -use test_case_core::TestCase; +use test_case_core::{TestCase, TestMatrix}; /// Generates tests for given set of data /// @@ -28,6 +28,50 @@ pub fn test_case(args: TokenStream, input: TokenStream) -> TokenStream { let mut item = parse_macro_input!(input as ItemFn); let mut test_cases = vec![(test_case, Span2::call_site())]; + + match expand_additional_test_case_macros(&mut item) { + Ok(cases) => test_cases.extend(cases), + Err(err) => return err.into_compile_error().into(), + } + + render_test_cases(&test_cases, item) +} + +/// Generates tests for the cartesian product of a given set of data +/// +/// A test matrix consists of three elements: +/// +/// 1. _(Required)_ Sets of values to combine; the nubmer of sets must be the same as the number of +/// arguments to the test body function +/// 2. _(Optional)_ Expected result (for all combinations of values) +/// 3. _(Required)_ Test body +/// +/// _Expected result_ and _Test body_ are the same as they are for the singular `#[test_case(...)]` +/// macro but are applied to every case generated by `#[test_matrix(...)]`. `Test case description` +/// is not allowed for `test_matrix`, because test case names are auto-generated from the test body +/// function name and matrix values. +#[proc_macro_attribute] +#[proc_macro_error::proc_macro_error] +pub fn test_matrix(args: TokenStream, input: TokenStream) -> TokenStream { + let matrix = parse_macro_input!(args as TestMatrix); + let mut item = parse_macro_input!(input as ItemFn); + + let mut test_cases = expand_test_matrix(&matrix, Span2::call_site()); + + match expand_additional_test_case_macros(&mut item) { + Ok(cases) => test_cases.extend(cases), + Err(err) => return err.into_compile_error().into(), + } + + render_test_cases(&test_cases, item) +} + +fn expand_test_matrix(matrix: &TestMatrix, span: Span2) -> Vec<(TestCase, Span2)> { + matrix.cases().map(|c| (c, span)).collect() +} + +fn expand_additional_test_case_macros(item: &mut ItemFn) -> syn::Result> { + let mut additional_cases = vec![]; let mut attrs_to_remove = vec![]; let legal_test_case_names = [ parse_quote!(test_case), @@ -35,21 +79,35 @@ pub fn test_case(args: TokenStream, input: TokenStream) -> TokenStream { parse_quote!(test_case::case), parse_quote!(case), ]; + let legal_test_matrix_names = [ + parse_quote!(test_matrix), + parse_quote!(test_case::test_matrix), + ]; for (idx, attr) in item.attrs.iter().enumerate() { if legal_test_case_names.contains(&attr.path) { let test_case = match attr.parse_args::() { Ok(test_case) => test_case, Err(err) => { - return syn::Error::new( + return Err(syn::Error::new( attr.span(), format!("cannot parse test_case arguments: {err}"), - ) - .to_compile_error() - .into() + )) } }; - test_cases.push((test_case, attr.span())); + additional_cases.push((test_case, attr.span())); + attrs_to_remove.push(idx); + } else if legal_test_matrix_names.contains(&attr.path) { + let test_matrix = match attr.parse_args::() { + Ok(test_matrix) => test_matrix, + Err(err) => { + return Err(syn::Error::new( + attr.span(), + format!("cannot parse test_matrix arguments: {err}"), + )) + } + }; + additional_cases.extend(expand_test_matrix(&test_matrix, attr.span())); attrs_to_remove.push(idx); } } @@ -58,7 +116,7 @@ pub fn test_case(args: TokenStream, input: TokenStream) -> TokenStream { item.attrs.swap_remove(i); } - render_test_cases(&test_cases, item) + Ok(additional_cases) } #[allow(unused_mut)] diff --git a/src/lib.rs b/src/lib.rs index feb0a2c..2374576 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,39 @@ //! test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out //! ``` //! +//! ## Test Matrix +//! +//! The `#[test_matrix(...)]` macro allows generating multiple test cases from the +//! Cartesian product of one or more possible values for each test function argument. The +//! number of arguments to the `test_matrix` macro must be the same as the number of arguments to +//! the test function. Each macro argument can be: +//! +//! 1. A list in array (`[x, y, ...]`) or tuple (`(x, y, ...)`) syntax. The values can be any +//! valid [expression](https://doc.rust-lang.org/reference/expressions.html). +//! 2. A closed numeric range expression (e.g. `0..100` or `1..=99`), which will generate +//! argument values for all integers in the range. +//! 3. A single expression, which can be used to keep one argument constant while varying the +//! other test function arguments using a list or range. +//! +//! ### Example usage: +//! +//! ```rust +//! #[cfg(test)] +//! mod tests { +//! use test_case::test_matrix; +//! +//! #[test_matrix( +//! [-2, 2], +//! [-4, 4] +//! )] +//! fn multiplication_tests(x: i8, y: i8) { +//! let actual = (x * y).abs(); +//! +//! assert_eq!(8, actual) +//! } +//! } +//! ``` +//! //! # MSRV Policy //! //! Starting with version 3.0 and up `test-case` introduces policy of only supporting latest stable Rust. @@ -59,6 +92,7 @@ //! Most up to date documentation is available in our [wiki](https://github.com/frondeus/test-case/wiki). pub use test_case_macros::test_case; pub use test_case_macros::test_case as case; +pub use test_case_macros::test_matrix; #[cfg(feature = "with-regex")] pub use regex::*; diff --git a/tests/acceptance_cases/matrices_compilation_errors/Cargo.toml b/tests/acceptance_cases/matrices_compilation_errors/Cargo.toml new file mode 100644 index 0000000..2cf4d79 --- /dev/null +++ b/tests/acceptance_cases/matrices_compilation_errors/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "matrices_compilation_errors" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +[lib] +name = "matrices_compilation_errors" +path = "src/lib.rs" +doctest = false + +[dev-dependencies] +test-case = { path = "../../../" } + +[workspace] diff --git a/tests/acceptance_cases/matrices_compilation_errors/src/lib.rs b/tests/acceptance_cases/matrices_compilation_errors/src/lib.rs new file mode 100644 index 0000000..93337a0 --- /dev/null +++ b/tests/acceptance_cases/matrices_compilation_errors/src/lib.rs @@ -0,0 +1,41 @@ +#![cfg(test)] +use test_case::test_matrix; + +#[test_matrix( + ["one", 1, true,] +)] +fn mixed_literals(x: u32) { + unreachable!("Should never compile") +} + +const END: u32 = 1; + +#[test_matrix(1..END)] +fn non_literal_range(x: u32) { + unreachable!("Should never compile") +} + +#[test_matrix(0..9_223_372_036_854_775_808)] +fn range_outside_isize_bounds(x: u32) { + unreachable!("Should never compile") +} + +#[test_matrix(1..)] +fn unbounded_range(x: u32) { + unreachable!("Should never compile") +} + +#[test_matrix( + [1, 2, 3] + ; "Illegal comment" +)] +fn illegal_comment(x: u32) { + unreachable!("Should never compile") +} + +const USIZE_CONST: usize = 0; + +#[test_matrix(USIZE_CONST)] +fn wrong_argument_type(x: i8) { + unreachable!("Should never compile") +} diff --git a/tests/acceptance_cases/matrices_support_basic_features/Cargo.toml b/tests/acceptance_cases/matrices_support_basic_features/Cargo.toml new file mode 100644 index 0000000..8464f93 --- /dev/null +++ b/tests/acceptance_cases/matrices_support_basic_features/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "matrices_support_basic_features" +version = "0.1.0" +authors = ["Ɓukasz Biel ", "Ross Williams "] +edition = "2021" + +[lib] +name = "matrices_support_basic_features" +path = "src/lib.rs" +doctest = false + +[dev-dependencies] +test-case = { path = "../../../" } + +[workspace] diff --git a/tests/acceptance_cases/matrices_support_basic_features/src/lib.rs b/tests/acceptance_cases/matrices_support_basic_features/src/lib.rs new file mode 100644 index 0000000..13b91c6 --- /dev/null +++ b/tests/acceptance_cases/matrices_support_basic_features/src/lib.rs @@ -0,0 +1,110 @@ +#[cfg(test)] +mod test_cases { + use test_case::{test_case, test_matrix}; + + #[test_matrix( + [1, 2], + [11, 12] + )] + fn numeric_values_array(x: u32, y: u32) { + assert!(x < 10); + assert!(y > 10); + } + + #[test_matrix( + 1..10, + [11, 12] + )] + fn matrix_with_range(x: u32, y: u32) { + assert!(x < 10); + assert!(y > 10); + } + + #[test_matrix( + ("one", "two"), + ("yellow", "blue") + )] + fn str_values_tuple(a: &str, b: &str) { + assert!(a.len() == 3); + assert!(b.len() > 3); + } + + #[test_matrix( + "just", + (1, 2, 3) + )] + fn matrix_with_singleton(a: &str, b: u32) { + assert_eq!(a, "just"); + assert!(b < 10); + } + + #[test_matrix("alone")] + fn only_singleton(a: &str) { + assert_eq!(a, "alone"); + } + + const TWO: u32 = 2; + + fn double(x: u32) -> u32 { + x * TWO + } + + #[test_matrix( + 2, + [double(2), 2 * TWO, 4] + )] + fn matrix_with_expressions(x: u32, two_x: u32) { + assert_eq!(2 * x, two_x); + } + + #[test_matrix(["foo", "bar", "baz"])] + fn impl_trait(x: impl AsRef) { + assert_eq!(3, x.as_ref().len()); + } + + #[test_matrix( + true, + [true, false] + )] + fn matrix_with_keywords(x: bool, y: bool) { + assert!(x || y) + } + + #[test_matrix( + [1, 2, 3] + )] + #[test_case(4)] + fn case_after_matrix(x: u32) { + assert!(x < 10); + } + + #[test_case(5)] + #[test_matrix( + [6, 7, 8] + )] + fn case_before_matrix(x: u32) { + assert!(x < 10); + } + + #[test_matrix( + [1, 2,], + [11, 12,] + )] + #[should_panic(expected = "Always panics")] + fn matrix_with_should_panic(_x: u32, _y: u32) { + panic!("Always panics") + } + + #[test_matrix( + [1, 2,], + [11, 12,] + => panics "Always panics" + )] + fn matrix_with_panics(_x: u32, _y: u32) { + panic!("Always panics") + } + + // tests from documentation + + // TODO +} diff --git a/tests/acceptance_tests.rs b/tests/acceptance_tests.rs index 065562d..eca7e10 100644 --- a/tests/acceptance_tests.rs +++ b/tests/acceptance_tests.rs @@ -103,6 +103,11 @@ fn cases_support_basic_features() { run_acceptance_test!("cases_support_basic_features") } +#[test] +fn matrices_support_basic_features() { + run_acceptance_test!("matrices_support_basic_features") +} + #[test] fn cases_support_complex_assertions() { run_acceptance_test!("cases_support_complex_assertions") @@ -147,3 +152,8 @@ fn features_produce_human_readable_errors() { fn allow_stays_on_fn() { run_acceptance_test!("allow_stays_on_fn") } + +#[test] +fn matrices_compilation_errors() { + run_acceptance_test!("matrices_compilation_errors") +} diff --git a/tests/snapshots/rust-nightly/acceptance__matrices_compilation_errors.snap b/tests/snapshots/rust-nightly/acceptance__matrices_compilation_errors.snap new file mode 100644 index 0000000..344caa1 --- /dev/null +++ b/tests/snapshots/rust-nightly/acceptance__matrices_compilation_errors.snap @@ -0,0 +1,48 @@ +--- +source: tests/acceptance_tests.rs +expression: output +--- + | + | + | + | + | + | + | + | + | ++++++++++++++++++++ + | ^^^ + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^^ + | ^^^^^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^^^^^ ----- + | --------------^^^^^^^^^^^-- + | arguments to this function are incorrect + | | expected `i8`, found `usize` + | | | + --> src/lib.rs:13:18 + --> src/lib.rs:18:18 + --> src/lib.rs:23:16 + --> src/lib.rs:30:5 + --> src/lib.rs:38:15 + --> src/lib.rs:39:4 + | + | ^ + --> src/lib.rs:5:13 +13 | #[test_matrix(1..END)] +18 | #[test_matrix(0..9_223_372_036_854_775_808)] +23 | #[test_matrix(1..)] +30 | ; "Illegal comment" +38 | #[test_matrix(USIZE_CONST)] +38 | #[test_matrix(USIZE_CONST.try_into().unwrap())] +39 | fn wrong_argument_type(x: i8) { +5 | ["one", 1, true,] +For more information about this error, try `rustc --explain E0308`. +error: All literal values must be of the same type +error: Comments are not allowed in #[test_matrix] +error: Range bounds can only be an integer literal +error: Unbounded ranges are not supported +error: could not compile `matrices_compilation_errors` (lib test) due to 6 previous errors +error: number too large to fit in target type +error[E0308]: mismatched types +help: you can convert a `usize` to an `i8` and panic if the converted value doesn't fit diff --git a/tests/snapshots/rust-nightly/acceptance__matrices_support_basic_features.snap b/tests/snapshots/rust-nightly/acceptance__matrices_support_basic_features.snap new file mode 100644 index 0000000..68b4f74 --- /dev/null +++ b/tests/snapshots/rust-nightly/acceptance__matrices_support_basic_features.snap @@ -0,0 +1,60 @@ +--- +source: tests/acceptance_tests.rs +expression: output +--- +running 54 tests +test result: ok. 54 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s +test test_cases::case_after_matrix::_1_expects ... ok +test test_cases::case_after_matrix::_2_expects ... ok +test test_cases::case_after_matrix::_3_expects ... ok +test test_cases::case_after_matrix::_4_expects ... ok +test test_cases::case_before_matrix::_5_expects ... ok +test test_cases::case_before_matrix::_6_expects ... ok +test test_cases::case_before_matrix::_7_expects ... ok +test test_cases::case_before_matrix::_8_expects ... ok +test test_cases::impl_trait::_bar_expects ... ok +test test_cases::impl_trait::_baz_expects ... ok +test test_cases::impl_trait::_foo_expects ... ok +test test_cases::matrix_with_expressions::_2_2_two_expects ... ok +test test_cases::matrix_with_expressions::_2_4_expects ... ok +test test_cases::matrix_with_expressions::_2_double_2_expects ... ok +test test_cases::matrix_with_keywords::true_false_expects ... ok +test test_cases::matrix_with_keywords::true_true_expects ... ok +test test_cases::matrix_with_panics::_1_11_expects_panicking_some_always_panics_ - should panic ... ok +test test_cases::matrix_with_panics::_1_12_expects_panicking_some_always_panics_ - should panic ... ok +test test_cases::matrix_with_panics::_2_11_expects_panicking_some_always_panics_ - should panic ... ok +test test_cases::matrix_with_panics::_2_12_expects_panicking_some_always_panics_ - should panic ... ok +test test_cases::matrix_with_range::_1_11_expects ... ok +test test_cases::matrix_with_range::_1_12_expects ... ok +test test_cases::matrix_with_range::_2_11_expects ... ok +test test_cases::matrix_with_range::_2_12_expects ... ok +test test_cases::matrix_with_range::_3_11_expects ... ok +test test_cases::matrix_with_range::_3_12_expects ... ok +test test_cases::matrix_with_range::_4_11_expects ... ok +test test_cases::matrix_with_range::_4_12_expects ... ok +test test_cases::matrix_with_range::_5_11_expects ... ok +test test_cases::matrix_with_range::_5_12_expects ... ok +test test_cases::matrix_with_range::_6_11_expects ... ok +test test_cases::matrix_with_range::_6_12_expects ... ok +test test_cases::matrix_with_range::_7_11_expects ... ok +test test_cases::matrix_with_range::_7_12_expects ... ok +test test_cases::matrix_with_range::_8_11_expects ... ok +test test_cases::matrix_with_range::_8_12_expects ... ok +test test_cases::matrix_with_range::_9_11_expects ... ok +test test_cases::matrix_with_range::_9_12_expects ... ok +test test_cases::matrix_with_should_panic::_1_11_expects - should panic ... ok +test test_cases::matrix_with_should_panic::_1_12_expects - should panic ... ok +test test_cases::matrix_with_should_panic::_2_11_expects - should panic ... ok +test test_cases::matrix_with_should_panic::_2_12_expects - should panic ... ok +test test_cases::matrix_with_singleton::_just_1_expects ... ok +test test_cases::matrix_with_singleton::_just_2_expects ... ok +test test_cases::matrix_with_singleton::_just_3_expects ... ok +test test_cases::numeric_values_array::_1_11_expects ... ok +test test_cases::numeric_values_array::_1_12_expects ... ok +test test_cases::numeric_values_array::_2_11_expects ... ok +test test_cases::numeric_values_array::_2_12_expects ... ok +test test_cases::only_singleton::_alone_expects ... ok +test test_cases::str_values_tuple::_one_blue_expects ... ok +test test_cases::str_values_tuple::_one_yellow_expects ... ok +test test_cases::str_values_tuple::_two_blue_expects ... ok +test test_cases::str_values_tuple::_two_yellow_expects ... ok diff --git a/tests/snapshots/rust-stable/acceptance__matrices_compilation_errors.snap b/tests/snapshots/rust-stable/acceptance__matrices_compilation_errors.snap new file mode 100644 index 0000000..580627f --- /dev/null +++ b/tests/snapshots/rust-stable/acceptance__matrices_compilation_errors.snap @@ -0,0 +1,48 @@ +--- +source: tests/acceptance_tests.rs +expression: output +--- + | + | + | + | + | + | + | + | + | ++++++++++++++++++++ + | ^^^ + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^ + | ^ + | ^^^^^^^^^^^^^^^^^^^ ----- + | --------------^^^^^^^^^^^-- + | arguments to this function are incorrect + | | expected `i8`, found `usize` + | | | + --> src/lib.rs:13:18 + --> src/lib.rs:18:18 + --> src/lib.rs:23:16 + --> src/lib.rs:30:5 + --> src/lib.rs:38:15 + --> src/lib.rs:39:4 + | + | ^ + --> src/lib.rs:5:13 +13 | #[test_matrix(1..END)] +18 | #[test_matrix(0..9_223_372_036_854_775_808)] +23 | #[test_matrix(1..)] +30 | ; "Illegal comment" +38 | #[test_matrix(USIZE_CONST)] +38 | #[test_matrix(USIZE_CONST.try_into().unwrap())] +39 | fn wrong_argument_type(x: i8) { +5 | ["one", 1, true,] +For more information about this error, try `rustc --explain E0308`. +error: All literal values must be of the same type +error: Comments are not allowed in #[test_matrix] +error: Range bounds can only be an integer literal +error: Unbounded ranges are not supported +error: could not compile `matrices_compilation_errors` (lib test) due to 6 previous errors +error: number too large to fit in target type +error[E0308]: mismatched types +help: you can convert a `usize` to an `i8` and panic if the converted value doesn't fit diff --git a/tests/snapshots/rust-stable/acceptance__matrices_support_basic_features.snap b/tests/snapshots/rust-stable/acceptance__matrices_support_basic_features.snap new file mode 100644 index 0000000..68b4f74 --- /dev/null +++ b/tests/snapshots/rust-stable/acceptance__matrices_support_basic_features.snap @@ -0,0 +1,60 @@ +--- +source: tests/acceptance_tests.rs +expression: output +--- +running 54 tests +test result: ok. 54 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s +test test_cases::case_after_matrix::_1_expects ... ok +test test_cases::case_after_matrix::_2_expects ... ok +test test_cases::case_after_matrix::_3_expects ... ok +test test_cases::case_after_matrix::_4_expects ... ok +test test_cases::case_before_matrix::_5_expects ... ok +test test_cases::case_before_matrix::_6_expects ... ok +test test_cases::case_before_matrix::_7_expects ... ok +test test_cases::case_before_matrix::_8_expects ... ok +test test_cases::impl_trait::_bar_expects ... ok +test test_cases::impl_trait::_baz_expects ... ok +test test_cases::impl_trait::_foo_expects ... ok +test test_cases::matrix_with_expressions::_2_2_two_expects ... ok +test test_cases::matrix_with_expressions::_2_4_expects ... ok +test test_cases::matrix_with_expressions::_2_double_2_expects ... ok +test test_cases::matrix_with_keywords::true_false_expects ... ok +test test_cases::matrix_with_keywords::true_true_expects ... ok +test test_cases::matrix_with_panics::_1_11_expects_panicking_some_always_panics_ - should panic ... ok +test test_cases::matrix_with_panics::_1_12_expects_panicking_some_always_panics_ - should panic ... ok +test test_cases::matrix_with_panics::_2_11_expects_panicking_some_always_panics_ - should panic ... ok +test test_cases::matrix_with_panics::_2_12_expects_panicking_some_always_panics_ - should panic ... ok +test test_cases::matrix_with_range::_1_11_expects ... ok +test test_cases::matrix_with_range::_1_12_expects ... ok +test test_cases::matrix_with_range::_2_11_expects ... ok +test test_cases::matrix_with_range::_2_12_expects ... ok +test test_cases::matrix_with_range::_3_11_expects ... ok +test test_cases::matrix_with_range::_3_12_expects ... ok +test test_cases::matrix_with_range::_4_11_expects ... ok +test test_cases::matrix_with_range::_4_12_expects ... ok +test test_cases::matrix_with_range::_5_11_expects ... ok +test test_cases::matrix_with_range::_5_12_expects ... ok +test test_cases::matrix_with_range::_6_11_expects ... ok +test test_cases::matrix_with_range::_6_12_expects ... ok +test test_cases::matrix_with_range::_7_11_expects ... ok +test test_cases::matrix_with_range::_7_12_expects ... ok +test test_cases::matrix_with_range::_8_11_expects ... ok +test test_cases::matrix_with_range::_8_12_expects ... ok +test test_cases::matrix_with_range::_9_11_expects ... ok +test test_cases::matrix_with_range::_9_12_expects ... ok +test test_cases::matrix_with_should_panic::_1_11_expects - should panic ... ok +test test_cases::matrix_with_should_panic::_1_12_expects - should panic ... ok +test test_cases::matrix_with_should_panic::_2_11_expects - should panic ... ok +test test_cases::matrix_with_should_panic::_2_12_expects - should panic ... ok +test test_cases::matrix_with_singleton::_just_1_expects ... ok +test test_cases::matrix_with_singleton::_just_2_expects ... ok +test test_cases::matrix_with_singleton::_just_3_expects ... ok +test test_cases::numeric_values_array::_1_11_expects ... ok +test test_cases::numeric_values_array::_1_12_expects ... ok +test test_cases::numeric_values_array::_2_11_expects ... ok +test test_cases::numeric_values_array::_2_12_expects ... ok +test test_cases::only_singleton::_alone_expects ... ok +test test_cases::str_values_tuple::_one_blue_expects ... ok +test test_cases::str_values_tuple::_one_yellow_expects ... ok +test test_cases::str_values_tuple::_two_blue_expects ... ok +test test_cases::str_values_tuple::_two_yellow_expects ... ok