fixtures/
lib.rs

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
extern crate proc_macro;
use glob::glob;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Ident, ItemFn, Lit, LitStr};

#[proc_macro_attribute]
pub fn fixtures(args: TokenStream, input: TokenStream) -> TokenStream {
    let glob_lit = parse_macro_input!(args as Lit);
    let glob_path = if let Lit::Str(ref glob_path) = glob_lit {
        glob_path
    } else {
        return syn::Error::new(glob_lit.span(), "Expected a string literal")
            .to_compile_error()
            .into();
    };
    let test_fn = parse_macro_input!(input as ItemFn);

    let fn_name = &test_fn.sig.ident;
    let fn_args = &test_fn.sig.inputs;
    let fn_block = &test_fn.block;

    let paths = match glob(glob_path.value().as_str()) {
        Err(err) => {
            return syn::Error::new(
                glob_lit.span(),
                format!("Failed to read glob pattern: {}", err),
            )
            .into_compile_error()
            .into();
        }
        Ok(paths) => paths,
    };

    let mut file_names = std::collections::HashMap::new();

    let expanded = paths
        .filter_map(Result::ok)
        .filter_map(|path| {
            let file_name = path
                .file_name()
                .expect("Failed to get file name")
                .to_str()?
                .to_owned()
                .replace('.', "_dot_")
                .replace(|c: char| !c.is_ascii_alphanumeric(), "_");
            let lit_file_path = LitStr::new(path.to_str()?, glob_path.span());
            let similar_file_names = file_names.entry(file_name.clone()).or_insert(0usize);
            *similar_file_names += 1;
            let lit_test_name = Ident::new(
                &format!("{fn_name}_{file_name}_{similar_file_names}"),
                fn_name.span(),
            );

            Some(quote! {
                #[test]
                fn #lit_test_name() {
                    #fn_name(::std::path::Path::new(#lit_file_path));
                }
            })
        })
        .collect::<Vec<_>>();

    if expanded.is_empty() {
        return syn::Error::new(
            glob_lit.span(),
            format!(
                "No valid files found for glob pattern: {}",
                glob_path.value()
            ),
        )
        .into_compile_error()
        .into();
    }

    quote! {
        fn #fn_name(#fn_args) #fn_block
        #(#expanded)*
    }
    .into()
}