test-case 1.1.0

Provides #[test_case(...)] procedural macro attribute for generating parametrized test cases easily
Documentation
//! # Overview
//! This crate provides `#[test_case]` procedural macro attribute that generates multiple parametrized tests using one body with different input parameters.
//! A test is generated for each data set passed in `test_case` attribute.
//! Under the hood, all test cases that share same body are grouped into `mod`, giving clear and readable test results.
//!
//! # Getting Started
//!
//! First of all you have to add this dependency to your `Cargo.toml`:
//!
//! ```toml
//! [dev-dependencies]
//! test-case = "1.1.0"
//! ```
//!
//! Additionally, you have to import the procedural macro with `use` statement:
//!
//! ```rust
//! use test_case::test_case;
//! ```
//!
//! # Example usage:
//!
//! ```rust
//! // The next two lines are not needed for 2018 edition or newer
//! #[cfg(test)]
//! extern crate test_case;
//!
//! #[cfg(test)]
//! mod tests {
//!     use test_case::test_case;
//!
//!     // Not needed for this example, but useful in general
//!     use super::*;
//!
//!     #[test_case(4,  2  ; "when operands are swapped")]
//!     #[test_case(-2, -4 ; "when both operands are negative")]
//!     #[test_case(2,  4  ; "when both operands are positive")]
//!     fn multiplication_tests(x: i8, y: i8) {
//!         let actual = (x * y).abs();
//!
//!         assert_eq!(8, actual)
//!     }
//!
//!     // You can still use regular tests too
//!     #[test]
//!     fn addition_test() {
//!         let actual = -2 + 8;
//!         assert_eq!(6, actual)
//!     }
//! }
//! ```
//!
//! Output from `cargo test` for this example:
//!
//! ```sh
//! $ cargo test
//!
//! running 4 tests
//! test tests::addition_test ... ok
//! test tests::multiplication_tests::when_both_operands_are_negative ... ok
//! test tests::multiplication_tests::when_both_operands_are_positive ... ok
//! test tests::multiplication_tests::when_operands_are_swapped ... ok
//!
//! test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
//! ```
//!
//! # Examples
//!
//! If your only assertion is just `assert_eq!`, you can pass the expectation as macro attribute using `=>` syntax:
//!
//! ```rust
//! # use test_case::test_case;
//! #[test_case( 2 => 2 ; "returns given number for positive input")]
//! #[test_case(-2 => 2 ; "returns opposite number for non-positive input")]
//! #[test_case( 0 => 0 ; "returns 0 for 0")]
//! fn abs_tests(x: i8) -> i8 {
//!    if x > 0 { x } else { -x }
//! }
//! ```
//!
//! Which is equivalent to
//!
//! ```rust
//! # use test_case::test_case;
//! #[test_case( 2, 2 ; "returns given number for positive input")]
//! #[test_case(-2, 2 ; "returns opposite number for non-positive input")]
//! #[test_case( 0, 0 ; "returns 0 for 0")]
//! fn abs_tests(x: i8, expected: i8){
//!    let actual = if x > 0 { x } else { -x };
//!
//!    assert_eq!(expected, actual);
//! }
//! ```
//!
//! Attributes and expectation may be any expresion unless they contain `=>`, e.g.
//!
//! ```rust
//! # use test_case::test_case;
//! #[test_case(None,        None    => 0 ; "treats none as 0")]
//! #[test_case(Some(2),     Some(3) => 5)]
//! #[test_case(Some(2 + 3), Some(4) => 2 + 3 + 4)]
//! fn fancy_addition(x: Option<i8>, y: Option<i8>) -> i8 {
//!     x.unwrap_or(0) + y.unwrap_or(0)
//! }
//! ```
//!
//! Note: in fact, `=>` is not prohibited, but the parser will always treat last `=>` sign as beginning of expectation definition.
//!
//! Test case names are optional. They are set using `;` followed by string literal at the end of macro attributes.
//!
//! Example generated code:
//!
//! ```rust
//! mod fancy_addition {
//!     #[allow(unused_imports)]
//!     use super::*;
//!
//!     fn fancy_addition(x: Option<i8>, y: Option<i8>) -> i8 {
//!         x.unwrap_or(0) + y.unwrap_or(0)
//!     }
//!
//!     #[test]
//!     fn treats_none_as_0() {
//!         let expected = 0;
//!         let actual = fancy_addition(None, None);
//!
//!         assert_eq!(expected, actual);
//!     }
//!
//!     #[test]
//!     fn some_2_some_3() {
//!         let expected = 5;
//!         let actual = fancy_addition(Some(2), Some(3));
//!
//!         assert_eq!(expected, actual);
//!     }
//!
//!     #[test]
//!     fn some_2_3_some_4() {
//!         let expected = 2 + 3 + 4;
//!         let actual = fancy_addition(Some(2 + 3), Some(4));
//!
//!         assert_eq!(expected, actual);
//!     }
//! }
//! ```
//!
//! # Modifiers
//!
//! ## inconclusive
//!
//! ### Context ignored test cases (deprecated, will be dropped in 2.0.0)
//!
//! If test case name (passed using `;` syntax described above) contains a word "inconclusive", generated test will be marked with `#[ignore]`.
//!
//! ### Keyword 'inconclusive'
//!
//! If test expectation is preceded by keyword `inconclusive` the test will be ignored as if it's description would contain word `inconclusive`
//!
//! ```rust
//! # use test_case::test_case;
//! #[test_case("42")]
//! #[test_case("XX" ; "inconclusive - parsing letters temporarily doesn't work, but it's ok")]
//! #[test_case("na" => inconclusive ())]
//! fn parses_input(input: &str) {
//!     // ...
//! }
//! ```
//!
//! Generated code:
//! ```ignore
//! mod parses_input {
//!     // ...
//!
//!     #[test]
//!     pub fn _42() {
//!         // ...
//!     }
//!
//!     #[test]
//!     #[ignore]
//!     pub fn inconclusive_parsing_letters_temporarily_doesn_t_work_but_it_s_ok() {
//!         // ...
//!     }
//!
//! ```
//! ## matches
//!
//! If test expectation is preceded by `matches` keyword, the result will be tested whether it fits within provided pattern.
//!
//! ```rust
//! # use test_case::test_case;
//! #[test_case("foo", "bar" => matches ("foo", _) ; "first element of zipped tuple is correct")]
//! #[test_case("foo", "bar" => matches (_, "bar") ; "second element of zipped tuple is correct")]
//! fn zip_test<'a>(left: &'a str, right: &'a str) -> (&'a str, &'a str) {
//!     (left, right)
//! }
//! ```
//!
//! ## panics
//!
//! If test case expectation is preceded by `panics` keyword and the expectation itself is `&str` **or** expresion that evaluates to `&str` then test case will be expected to panic during execution.
//!
//! ```rust
//! # use test_case::test_case;
//!
//! #[test_case("foo" => panics "invalid input")]
//! #[test_case("bar")]
//! fn test_panicking(input: &str) {
//!     if input == "foo" {
//!         panic!("invalid input")
//!     }
//! }
//! ```
//!
//! ## is|it (feature = "hamcrest_assertions")
//!
//! This feature requires addition of hamcrest2 crate to your Cargo.toml:
//!
//! ```toml
//! test-case = { version = "1.1.0", features = ["hamcrest_assertions"] }
//! hamcrest2 = "0.3.0"
//! ```
//!
//! After that you can use test cases with new keywords `is` and `it` which will allow you to use hamcrest2 assertions ([doc](https://docs.rs/hamcrest2/0.3.0/hamcrest2/))
//!
//! ```rust
//! # use test_case::test_case;
//!
//! #[test_case(&[1, 3] => is empty())]
//! #[test_case(&[2, 3] => it contains(2))]
//! #[test_case(&[2, 3] => it not(contains(3)))]
//! #[test_case(&[2, 4] => it contains(vec!(2, 4)))]
//! #[test_case(&[2, 3] => is len(1))]
//! fn removes_odd_numbers(collection: &[u8]) -> &Vec<u8> {
//!     Box::leak(Box::new(collection.into_iter().filter(|x| *x % 2 == 0).copied().collect()))
//! }
//! ```
//!
//! # async in test cases
//!
//! Test cases can work with `tokio`, `async-std` and other runtimes, provided `#[test...]` attribute from mentioned libraries is used as a last attribute.
//!
//! eg.
//!
//! ```rust
//! # use test_case::test_case;
//!
//! #[test_case("Hello, world" => true)]
//! #[tokio::test]
//! async fn runs_async_task(input: &str) -> bool {
//!     some_async_fn(input).await
//! }
//! ```
//!

extern crate proc_macro;

use proc_macro::TokenStream;

use syn::{parse_macro_input, ItemFn};

use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::parse_quote;
use syn::spanned::Spanned;
use test_case::TestCase;

mod expected;
mod test_case;
mod utils;

/// Generates tests for given set of data
///
/// In general, test case consists of four elements:
///
/// 1. _(Required)_ Arguments passed to test body
/// 2. _(Optional)_ Expected result
/// 3. _(Optional)_ Test case name
/// 4. _(Required)_ Test body
///
///  When _expected result_ is provided, it is compared against the actual value generated with _test body_ using `assert_eq!`.
/// _Test cases_ that don't provide _expected result_ should contain custom assertions inside _test body_.
///
/// # Examples
///
/// - Without result and name
///
/// ```rust
/// # use test_case::test_case;
/// #[test_case(5)]
/// #[test_case(10)]
/// fn is_positive(x: i8) {
///     assert!(x > 0)
/// }
/// ```
///
/// - With name, without result
///
/// ```rust
/// # use test_case::test_case;
/// #[test_case(1   ; "little number")]
/// #[test_case(100 ; "big number")]
/// #[test_case(5)] // some tests may use default name generated from arguments list
/// fn is_positive(x: i8) {
///     assert!(x > 0)
/// }
/// ```
///
/// - With result, without name
///
/// ```rust
/// # use test_case::test_case;
/// #[test_case(1,   2 =>  3)]
/// #[test_case(-1, -2 => -3)]
/// fn addition(x: i8, y: i8) -> i8 {
///     x + y
/// }
/// ```
///
/// - With result and name
///
/// ```rust
/// # use test_case::test_case;
/// #[test_case(1,   2 =>  3 ; "both numbers possitive")]
/// #[test_case(-1, -2 => -3 ; "both numbers negative")]
/// fn addition(x: i8, y: i8) -> i8 {
///     x + y
/// }
/// ```
#[proc_macro_attribute]
pub fn test_case(args: TokenStream, input: TokenStream) -> TokenStream {
    let test_case = parse_macro_input!(args as TestCase);
    let mut item = parse_macro_input!(input as ItemFn);

    let mut test_cases = vec![test_case];
    let mut attrs_to_remove = vec![];
    for (idx, attr) in item.attrs.iter().enumerate() {
        if attr.path == parse_quote!(test_case) {
            let test_case = match attr.parse_args::<TestCase>() {
                Ok(test_case) => test_case,
                Err(err) => {
                    return syn::Error::new(
                        attr.span(),
                        format!("cannot parse test_case arguments: {:?}", err),
                    )
                    .to_compile_error()
                    .into()
                }
            };
            test_cases.push(test_case);
            attrs_to_remove.push(idx);
        }
    }

    for i in attrs_to_remove.into_iter().rev() {
        item.attrs.swap_remove(i);
    }

    render_test_cases(&test_cases, item)
}

#[allow(unused_mut)]
fn render_test_cases(test_cases: &[TestCase], mut item: ItemFn) -> TokenStream {
    let mut rendered_test_cases = vec![];

    for test_case in test_cases {
        rendered_test_cases.push(test_case.render(item.clone()));
    }

    let mod_name = item.sig.ident.clone();

    let mut additional_usings: Vec<TokenStream2> = vec![];

    cfg_if::cfg_if! {
        if #[cfg(feature="hamcrest_assertions")] {
            additional_usings.push(quote! {
                #[allow(unused_imports)]
                use hamcrest2::*;
            })
        }
    }

    // We don't want any external crate to alter main fn code, we are passing them to each sub-function
    item.attrs.clear();

    let output = quote! {
        mod #mod_name {
            #[allow(unused_imports)]
            use super::*;

            #(#additional_usings)*

            #[allow(unused_attributes)]
            #item

            #(#rendered_test_cases)*
        }
    };

    output.into()
}