1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249

//! Adds parametrization capabilty to `#[test]` via `#[args]` attribute macro.
//!
//! # To install via umbrella devbox crate
//!
//! ```toml
//! [dev-dependencies]
//! devbox = { version = "0.1" }
//! ```
//!
//! # Simplest example
//!
//! ```rust
//! # use devbox_test_args::args;
//! #[args(
//!     char_a: 'a';
//!     char_b: 'b' ! "wrong char";
//! )]
//! #[test]
//! fn parametrized_test_for(character:_) {
//!     assert_eq!('a', character, "wrong char");
//! }
//! ```
//!
//! Check [#\[args\]] attribute for full example and usage specification.
//!
//! [#\[args\]]: https://doc.rust-lang.org/devbox_test_args/attr.args.html

use std::iter::FromIterator;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use proc_macro_error::{abort, emit_error, proc_macro_error};
use quote::quote;
use syn::{
    parse_macro_input, Block, Expr, FnArg, ItemFn, LitStr, Local, Pat, Result, Stmt, Token,
    parse::{Parse, ParseStream},
    punctuated::Punctuated,
    token::{Eq, Let, Semi},
};

//-- Macros ----------------------------------------------------------------------------------------

/// This is an attribute complementing Rust's standard `#[test]` attribute for test parametrization.
///
/// A test function can have any number of parameters which can have anonymouse types that will
/// be filled in by the attribute based it's arguments.
///
/// Make sure attribute is applied before the standard Rust `#[test]` attribute or you will
/// get *functions used as tests can not have any arguments* error. You can also use [`test_args`]
/// attribute instead which appends the `#[test]` automatically.
///
/// [`test_args`]: attr.test_args.html
///
/// # Test case
///
/// Macro emits a new standard Rust test for each named argument set (also called a case) by
/// suffixing function name with case name.
///
/// Cases are seperated by `;` and need to have unique names for particular test function.
/// Each case needs argument list seperated by `,` that consumes equal number of function parameters
/// when generating the actual test function.
///
/// To mark a case as one that should panic, add a suffix with a slice of expected message after `!`
///
/// Syntax for a case is ```<case-name>: <arg1>, <arg2> ... <argN> [! "<message slice>"];```
///
/// # Cartesian product
///
/// You can apply mutiple test macros to a single function with individual macro cases consuming
/// only a subset of function parameters. This forms a cartesian product of cases from each macro
/// instance. It is import that all cartesian products consume all parameters or you will end up
/// with a test function with nonzero parameters which is not supported by Rust built in test macro.
///
/// # Example
///
/// The following example have two cases named `char_a` and `char_b` in first attribute and
/// `offset_0` and `offset_1` in the second which combines into four tests:
///
/// ```rust
/// # use devbox_test_args::args;
///
/// #[args(
///     char_a: 97, 'a';
///     char_b: 98, 'b';
/// )]
/// #[args(
///     offset_0: 0;
///     offset_1: 1 ! "code incorrect";
/// )]
/// #[test]
/// fn parametrized_test_for(code:_, letter:_, offset:_) {
///     assert_eq!(code + offset, letter as u8, "Letter code incorrect");
/// }
/// ```
///
/// Should produce:
/// ```txt
/// test parametrized_test_for__char_a__offset_0 ... ok
/// test parametrized_test_for__char_b__offset_0 ... ok
/// test parametrized_test_for__char_a__offset_1 ... ok
/// test parametrized_test_for__char_b__offset_1 ... ok
/// ```
#[proc_macro_attribute]
#[proc_macro_error]
pub fn args(attr: TokenStream, input: TokenStream) -> TokenStream {
    apply_test_args(attr, input, false)
}


/// Same as [`args`] but applying standard Rust `#[test]` attribute automatically
///
/// [`args`]: attr.args.html
///
#[proc_macro_attribute]
#[proc_macro_error]
pub fn test_args(attr: TokenStream, input: TokenStream) -> TokenStream {
    apply_test_args(attr, input, true)
}

//-- Implemenatation -------------------------------------------------------------------------------

/// Main entry point for both macros
fn apply_test_args(attr: TokenStream, input: TokenStream, append_test_attr: bool) -> TokenStream {
    let cases = parse_macro_input!(attr as Cases);
    let input = parse_macro_input!(input as ItemFn);

    if cases.0.len() == 0 {
        let test = test_attribute(&input, append_test_attr);
        return quote!{
            #test
            #input
        }.into();
    }

    let mut output = quote!{};
    for case in cases.0 {
        let should_panic = case.panics.clone().map(|e| quote!{ #[should_panic(expected = #e)] });
        let func = make_case_function(&input, case);
        let test = test_attribute(&func, append_test_attr);

        output.extend(quote!{
            #test
            #should_panic
            #func
        });
    }
    output.into()
}

/// Checks if the test function already has the `#[test]` attribute applied
fn test_attribute(func: &ItemFn, add_if_needed: bool) -> Option<proc_macro2::TokenStream> {
    if func.sig.inputs.len() > 0 ||
       func.attrs.iter().any(|a| a.path.segments.last().map_or(false, |seg|seg.ident=="test"))
    {
        return None;
    }

    if add_if_needed {
        Some(quote!{ #[test] })
    } else {
        abort!(func, "Devbox: Function '{}' is missing '#[test]' attribute", func.sig.ident);
    }
}

/// Clones `input` function with arguments for attribute `case` applied
fn make_case_function(input: &ItemFn, case: Case) -> ItemFn {
    if case.values.len() > input.sig.inputs.len() {
        emit_error!(
            input,
            "Devbox: Test case '{}' arguments outnumber function '{}' parameters {} to {}",
            case.ident, input.sig.ident, case.values.len(), input.sig.inputs.len()
        );
    }

    let mut func = input.clone();
    let name = format!("{}__{}", func.sig.ident, case.ident.to_string());
    func.sig.ident = Ident::new(name.as_ref(), Span::call_site());

    let inputs = func.sig.inputs.clone();
    let mut args = inputs.iter().map(|t|t.clone());
    for expr in case.values {
        if let Some(arg) = args.next() {
            insert_param(&mut func.block, arg, expr);
        }
    }

    func.sig.inputs = syn::punctuated::Punctuated::from_iter(args);
    func
}

/// Replaces one function parameter with one attribute case argument
fn insert_param(block: &mut Box<Block>, arg: FnArg, init:Box<Expr>){
    match arg {
        FnArg::Typed(arg) => block.stmts.insert(0, Stmt::Local(Local {
            attrs: vec![],
            let_token: Let { span: Span::call_site() },
            pat: Pat::Type(arg),
            init: Some((Eq{ spans: [Span::call_site()] }, init)),
            semi_token: Semi { spans: [Span::call_site()] },
        })),
        FnArg::Receiver(_) => emit_error!(
            arg,
            "Devbox: Parametrized test applied to non-associated function"
        )
    }
}

//-- Attribute parser ------------------------------------------------------------------------------

struct Case {
    pub ident: Ident,
    pub colon: Token![:],
    pub values: Vec<Box<Expr>>,
    pub panics: Option<LitStr>,
}

impl Parse for Case {
    fn parse(input: ParseStream) -> Result<Self> {
        Ok(Case {
            ident: input.parse()?,
            colon: input.parse()?,
            values: {
                let mut result = vec![Box::new(input.parse()?)];
                let mut more: Option<Token![,]> = input.parse()?;
                while more.is_some() {
                    result.push(Box::new(input.parse()?));
                    more = input.parse()?;
                }
                result
            },
            panics: {
                let excl: Option<Token![!]> = input.parse()?;
                if excl.is_some() {
                    input.parse()?
                } else {
                    None
                }
            }
        })
    }
}

struct Cases(Punctuated<Case, Token![;]>);

impl Parse for Cases {
    fn parse(input: ParseStream) -> Result<Self> {
        Ok(Cases(input.parse_terminated(Case::parse)?))
    }
}