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
// Copyright (C) 2019  Frank Rehberger
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0>
extern crate glob;
extern crate proc_macro;

use proc_macro::TokenStream;

use self::glob::{glob, Paths};
use quote::quote;
use syn::parse::{Parse, ParseStream, Result};
use syn::{parse_macro_input, Lit, Ident, Token};


/// Parser elements
struct GlobExpand {
    glob_pattern: Lit,
    lambda: Ident,
}

/// Parser reading the Literal and function-identifier from token-stream
impl Parse for GlobExpand {
    fn parse(input: ParseStream) -> Result<Self> {
        let glob_pattern: Lit = input.parse()?;
        input.parse::<Token![;]>()?;
        let lambda: Ident = input.parse()?;

        Ok(GlobExpand {
            glob_pattern,
            lambda,
        })
    }
}

/// Prefix for each generated test-function
const PREFIX: &str = "gen_";

/// Function-Attribute macro expanding glob-file-pattern to a list of directories
/// and generating a test-function for each one.
///
/// ```
/// extern crate test_generator;
/// #[cfg(test)]
/// mod tests {
///   use std::path::Path;
///   use test_generator::glob_expand;
///
///   glob_expand! { "data/*"; generic_test }
///
///   fn generic_test(dir_name: &str) { assert!(Path::new(dir_name).exists()); }
/// }
/// ```
/// The macro will expand the code for each subfolder in `"data/*"`, generating the following
/// code. This code is not visible in IDE. Every build-time, the code will be newly generated.
///
///```
///mod tests {
///    ///
///    ///
///    #[test]
///    fn gen_data_testset1() {
///        generic_test("data/testset1");
///    }
///
///    ///
///    ///
///    #[test]
///    fn gen_data_testset2() {
///        generic_test("data/testset2");
///    }
///}
///
///```
#[proc_macro]
pub fn glob_expand(item: TokenStream) -> TokenStream {
    println!("item: \"{}\"", item.to_string());
    let GlobExpand {
        glob_pattern,
        lambda,
    } = parse_macro_input!(item as GlobExpand);

    let pattern = if let Lit::Str(s) = glob_pattern {
        s.value()
    } else {
        panic!();
    };

    let empty_ts: proc_macro2::TokenStream = "".parse().unwrap();

    let paths: Paths = glob(&pattern).expect("Failed to read testdata dir.");

    /// helper, concatting two token-streams
    fn concat(accu: proc_macro2::TokenStream, ts: proc_macro2::TokenStream)
              -> proc_macro2::TokenStream {
        quote! { #accu #ts }
    }

    // for each path generate a test-function and fold them to single tokenstream
    let result = paths.map(|path| {
        let path_as_str = path.expect("No such file or directory")
            .into_os_string().into_string().expect("bad encoding");

        // remove delimiters and special characters
        let canonical_name = path_as_str
            .replace("\"", " ")
            .replace(" ", "_")
            .replace("-", "_")
            .replace("*", "_")
            .replace("/", "_");

        // form an identifier with prefix
        let mut func_name = PREFIX.to_string();
        func_name.push_str(&canonical_name);
        let func_ident = proc_macro2::Ident::new(&func_name,
                                                 proc_macro2::Span::call_site());

        let item = quote! {
            #[test]
            fn #func_ident () {
               println!("path: {}", #path_as_str);
               let f = #lambda ;
               f( #path_as_str );
            }
        };

        item
    }).fold(empty_ts, concat);

    result.into()
}