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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
//! # Reuse `rstest`'s parametrized cases
//!
//! This crate give a way to define a tests set and apply them to every case you need to
//! test.
//!
//! With `rstest` crate you can define a tests list but if you want to apply the same tests
//! to another test function you must rewrite all cases or write some macros that do the job.
//! Both solutions have some drawbreak:
//!
//! - introduce duplication
//! - macros makes code harder to read and shift out the focus from tests core
//!
//! The aim of this crate is solve this problem. `rstest_reuse` expose two attributes:
//!
//! - `#[template]`: to define a template
//! - `#[apply]`: to apply a defined template to create tests
//!
//! Here is a simple example:
//!
//! ```
//! use rstest::rstest;
//! use rstest_reuse::{self, *};
//!
//! // Here we define the template. This define
//! // * The test list name to `two_simple_cases`
//! // * cases: here two cases that feed the `a`, `b` values
//! #[template]
//! #[rstest]
//! #[case(2, 2)]
//! #[case(4/2, 2)]
//! fn two_simple_cases(#[case] a: u32,#[case] b: u32) {}
//!
//! // Here we apply the `two_simple_cases` template: That is expanded in
//! // #[rstest]
//! // #[case(2, 2)]
//! // #[case(4/2, 2)]
//! // fn it_works(#[case] a: u32,#[case] b: u32) {
//! //     assert!(a == b);
//! // }
//! #[apply(two_simple_cases)]
//! fn it_works(a: u32, b: u32) {
//!     assert!(a == b);
//! }
//!
//!
//! // Here we reuse the `two_simple_cases` template to create two
//! // other tests
//! #[apply(two_simple_cases)]
//! fn it_fail(a: u32, b: u32) {
//!     assert!(a != b);
//! }
//! ```
//! If we run `cargo test` we have:
//!
//! ```text
//!     Finished test [unoptimized + debuginfo] target(s) in 0.05s
//!      Running target/debug/deps/playground-8a1212f8b5eb00ce
//!
//! running 4 tests
//! test it_fail::case_1 ... FAILED
//! test it_works::case_1 ... ok
//! test it_works::case_2 ... ok
//! test it_fail::case_2 ... FAILED
//!
//! failures:
//!
//! ---- it_fail::case_1 stdout ----
//! thread 'it_fail::case_1' panicked at 'assertion failed: a != b', src/main.rs:34:5
//! note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
//!
//! ---- it_fail::case_2 stdout ----
//! thread 'it_fail::case_2' panicked at 'assertion failed: a != b', src/main.rs:34:5
//!
//!
//! failures:
//!     it_fail::case_1
//!     it_fail::case_2
//!
//! test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out
//!
//! error: test failed, to rerun pass '--bin playground'
//! ```
//!
//! Simple and neat!
//!
//! Note that if the test arguments names match the template's ones you can don't
//! repeate the arguments attributes.
//!
//! ## Composition and Values
//!
//! If you need to add some cases or values when apply a template you can leverage on
//! composition. Here a simple example:
//!
//! ```
//! use rstest::rstest;
//! use rstest_reuse::{self, *};
//!
//! #[template]
//! #[rstest]
//! #[case(2, 2)]
//! #[case(4/2, 2)]
//! fn base(#[case] a: u32, #[case] b: u32) {}
//!
//! // Here we add a new case and an argument in a value list:
//! #[apply(base)]
//! #[case(9/3, 3)]
//! fn it_works(a: u32, b: u32, #[values("a", "b")] t: &str) {
//!     assert!(a == b);
//!     assert!("abcd".contains(t))
//! }
//! ```
//!
//! `cargo test` runs 6 tests:
//!
//! ```text
//! running 6 tests
//! test it_works::case_1::t_2 ... ok
//! test it_works::case_2::t_2 ... ok
//! test it_works::case_2::t_1 ... ok
//! test it_works::case_3::t_2 ... ok
//! test it_works::case_3::t_1 ... ok
//! test it_works::case_1::t_1 ... ok
//! ```
//!
//! Template can also used for `#[values]` and `#[with]` arguments if you need:
//!
//! ```
//! use rstest::*;
//! use rstest_reuse::{self, *};
//!
//! #[template]
//! #[rstest]
//! fn base(#[with(42)] fix: u32, #[values(1,2,3)] v: u32) {}
//!
//! #[fixture]
//! fn fix(#[default(0)] inner: u32) -> u32 {
//!     inner
//! }
//!
//! #[apply(base)]
//! fn use_it_with_fixture(fix: u32, v: u32) {
//!     assert!(fix%v == 0);
//! }
//!
//! #[apply(base)]
//! fn use_it_without_fixture(v: u32) {
//!     assert!(24 % v == 0);
//! }
//! ```
//!
//! `cargo test` runs 6 tests:
//!
//! ```text
//! running 6 tests
//! test use_it_with_fixture::v_1 ... ok
//! test use_it_without_fixture::v_1 ... ok
//! test use_it_with_fixture::v_3 ... ok
//! test use_it_without_fixture::v_2 ... ok
//! test use_it_without_fixture::v_3 ... ok
//! test use_it_with_fixture::v_2 ... ok
//! ```
//!
//!
//! ## Cavelets
//!
//! ### `use rstest_reuse` at the top of your crate
//!
//! You **should** add `use rstest_reuse` at the top of your crate:
//!
//! ```
//! #[cfg(test)]
//! use rstest_reuse;
//! ```
//!
//! This is due `rstest_reuse::template` define a macro that need to call a `rstest_reuse`'s macro.
//! I hope to remove this in the future but for now we should live with it.
//!
//! Note that
//! ```
//! use rstest_reuse::*;
//! ```
//! is not enougth: this statment doesn't include `rstest_reuse` but just its public items.
//!
//! ## Disclamer
//!
//! This crate is in developer stage. I don't know if I'll include it in `rstest` or changing some syntax in
//! the future.
//!
//! I did't test it in a lot of cases: if you have some cases where it doesn't works file a ticket on
//! [`rstest`](https://github.com/la10736/rstest)

extern crate proc_macro;
use std::collections::HashMap;

use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{
    self, parse, parse::Parse, parse_macro_input, Attribute, Ident, ItemFn, PatType, Path, Token,
};

struct MergeAttrs {
    template: ItemFn,
    function: ItemFn,
}

impl Parse for MergeAttrs {
    fn parse(input: parse::ParseStream) -> syn::Result<Self> {
        let template = input.parse()?;
        let _comma: Token![,] = input.parse()?;
        let function = input.parse()?;
        Ok(Self { template, function })
    }
}

#[cfg(sanitize_multiple_should_panic_compiler_bug)]
fn is_should_panic(attr: &syn::Attribute) -> bool {
    let should_panic: Ident = syn::parse_str("should_panic").unwrap();
    attr.path.is_ident(&should_panic)
}

#[cfg(sanitize_multiple_should_panic_compiler_bug)]
fn sanitize_should_panic_duplication_bug(
    mut attributes: Vec<syn::Attribute>,
) -> Vec<syn::Attribute> {
    if attributes.len() != 2 || attributes[0] != attributes[1] || !is_should_panic(&attributes[0]) {
        // Nothing to do
        return attributes;
    }
    attributes.pop();
    attributes
}

fn collect_template_args(template: &ItemFn) -> HashMap<&Ident, &PatType> {
    template
        .sig
        .inputs
        .iter()
        .filter_map(|arg| match arg {
            syn::FnArg::Typed(a) => Some(a),
            _ => None,
        })
        .filter_map(|arg| match *arg.pat {
            syn::Pat::Ident(ref id) => Some((&id.ident, arg)),
            _ => None,
        })
        .collect()
}

fn merge_arg_attributes(dest: &mut Vec<Attribute>, source: &[Attribute]) {
    for s in source.iter() {
        if !dest.contains(s) {
            dest.push(s.clone())
        }
    }
}

fn resolve_template_arg<'a>(
    template: &HashMap<&'a Ident, &'a PatType>,
    arg: &Ident,
) -> Option<&'a PatType> {
    let id_name = arg.to_string();
    match (template.get(arg), id_name.starts_with('_')) {
        (Some(&arg), _) => Some(arg),
        (None, true) => template.get(&format_ident!("{}", id_name[1..])).copied(),
        _ => None,
    }
}

fn expand_function_arguments(dest: &mut ItemFn, source: &ItemFn) {
    let to_merge_args = collect_template_args(source);

    for arg in dest.sig.inputs.iter_mut() {
        if let syn::FnArg::Typed(a) = arg {
            if let syn::Pat::Ident(ref id) = *a.pat {
                if let Some(source_arg) = resolve_template_arg(&to_merge_args, &id.ident) {
                    merge_arg_attributes(&mut a.attrs, &source_arg.attrs);
                }
            }
        }
    }
}

#[doc(hidden)]
#[proc_macro]
pub fn merge_attrs(item: TokenStream) -> TokenStream {
    let MergeAttrs {
        template,
        mut function,
    } = parse_macro_input!(item as MergeAttrs);

    expand_function_arguments(&mut function, &template);

    let mut attrs = template.attrs;
    #[cfg(sanitize_multiple_should_panic_compiler_bug)]
    {
        function.attrs = sanitize_should_panic_duplication_bug(function.attrs);
    }
    attrs.append(&mut function.attrs);
    function.attrs = attrs;

    let tokens = quote! {
        #function
    };
    tokens.into()
}

fn get_export(attributes: &[Attribute]) -> Option<&Attribute> {
    attributes
        .iter()
        .find(|&attr| attr.path().is_ident(&format_ident!("export")))
}

/// Define a template where the name is given from the function name. This attribute register all
/// attributes. The function signature don't really mater but to make it clear is better that you
/// use a signature like if you're wrinting a standard `rstest`.
///
/// If you need to export the template at the root of your crate or use it from another crate you
/// should annotate it with `#[export]` attribute. This attribute add `#[macro_export]` attribute to
/// the template macro and make possible to use it from another crate.
///
/// When define a template you can also set the arguments attributes like `#[case]`, `#[values]`
/// and `#[with]`: when you apply it attributes will be copied to the matched by name arguments.
///
#[proc_macro_attribute]
pub fn template(_args: proc_macro::TokenStream, input: proc_macro::TokenStream) -> TokenStream {
    let mut template: ItemFn = parse(input).unwrap();

    let rstest_index = template
        .attrs
        .iter()
        .position(|attr| attr.path().is_ident(&format_ident!("rstest")));

    let mut attributes = template.attrs;

    template.attrs = match rstest_index {
        Some(idx) => attributes.split_off(idx),
        None => std::mem::take(&mut attributes),
    };

    let (macro_attribute, visibility) = match get_export(&attributes) {
        Some(_) => (
            quote! {
                #[macro_export]
            },
            quote! {
                pub
            },
        ),
        None => (
            quote! {},
            quote! {
                pub(crate)
            },
        ),
    };

    let macro_name = template.sig.ident.clone();
    let macro_name_rand = format_ident!("{}_{}", macro_name, rand::random::<u64>());

    let tokens = quote! {
        /// Apply #macro_name template to given body
        #macro_attribute
        macro_rules! #macro_name_rand {
            ( $test:item ) => {
                        $crate::rstest_reuse::merge_attrs! {
                            #template,
                            $test
                        }
                    }
        }
        #[allow(unused_imports)]
        #visibility use #macro_name_rand as #macro_name;
    };
    tokens.into()
}

/// Apply a defined template. The function signature should satisfy the template attributes
/// but can also add some other fixtures.
/// Example:
///
/// ```
/// use rstest::{rstest, fixture};
/// use rstest_reuse::{self, *};
///
/// #[fixture]
/// fn empty () -> Vec<u32> {
///     Vec::new()    
/// }
///
/// #[template]
/// #[rstest]
/// #[case(2, 2)]
/// #[case(4/2, 2)]
/// fn two_simple_cases(#[case] a: u32, #[case] b: u32) {}
///
/// #[apply(two_simple_cases)]
/// fn it_works(mut empty: Vec<u32>, a: u32, b: u32) {
///     empty.append(a);
///     assert!(empty.last() == b);
/// }
/// ```
/// When use `#[apply]` you can also
/// 1. Use a path for template
/// 2. Ignore an argument by underscore
/// 3. add some cases
/// 4. add some values
///
///
/// ```
/// use rstest::{rstest, fixture};
/// use rstest_reuse::{self, *};
///
/// #[fixture]
/// fn fix (#[default(0)] inner: u32) -> u32 {
///     inner
/// }
///
/// mod outer {
///     pub(crate) mod inner {
///         use rstest_reuse::template;
///
///         #[template]
///         #[rstest]
///         #[case(2, 2)]
///         #[case(4/2, 2)]
///         fn two_simple_cases(#[case] a: u32, #[case] b: u32) {}
///     }
/// }
///
///
/// #[apply(outer::inner::two_simple_cases)]
/// // Add a case
/// #[case(9/3, 3)]
/// // Use fixture with 42 as argument
/// // Ignore b case values
/// // add 2 cases with other in 4, 5 for each case
/// fn lot_of_tests(fix: u32, a: u32, _b: u32, #[values(4, 5)] other: u32) {
///     assert_eq!(fix, 42);
///     assert_eq!(a, 2);
///     assert!([4, 5].contains(other));
/// }
/// ```
///

#[proc_macro_attribute]
pub fn apply(args: proc_macro::TokenStream, input: proc_macro::TokenStream) -> TokenStream {
    let template: Path = parse(args).unwrap();
    let test: ItemFn = parse(input).unwrap();
    let tokens = quote! {
        #template! {
            #test
        }
    };
    tokens.into()
}