fixtures/
lib.rs

1extern crate proc_macro;
2use glob::glob;
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{parse_macro_input, Ident, ItemFn, Lit, LitStr};
6
7#[proc_macro_attribute]
8pub fn fixtures(args: TokenStream, input: TokenStream) -> TokenStream {
9    let glob_lit = parse_macro_input!(args as Lit);
10    let glob_path = if let Lit::Str(ref glob_path) = glob_lit {
11        glob_path
12    } else {
13        return syn::Error::new(glob_lit.span(), "Expected a string literal")
14            .to_compile_error()
15            .into();
16    };
17    let test_fn = parse_macro_input!(input as ItemFn);
18
19    let fn_name = &test_fn.sig.ident;
20    let fn_args = &test_fn.sig.inputs;
21    let fn_block = &test_fn.block;
22
23    let paths = match glob(glob_path.value().as_str()) {
24        Err(err) => {
25            return syn::Error::new(
26                glob_lit.span(),
27                format!("Failed to read glob pattern: {}", err),
28            )
29            .into_compile_error()
30            .into();
31        }
32        Ok(paths) => paths,
33    };
34
35    let mut file_names = std::collections::HashMap::new();
36
37    let expanded = paths
38        .filter_map(Result::ok)
39        .filter_map(|path| {
40            let file_name = path
41                .file_name()
42                .expect("Failed to get file name")
43                .to_str()?
44                .to_owned()
45                .replace('.', "_dot_")
46                .replace(|c: char| !c.is_ascii_alphanumeric(), "_");
47            let lit_file_path = LitStr::new(path.to_str()?, glob_path.span());
48            let similar_file_names = file_names.entry(file_name.clone()).or_insert(0usize);
49            *similar_file_names += 1;
50            let lit_test_name = Ident::new(
51                &format!("{fn_name}_{file_name}_{similar_file_names}"),
52                fn_name.span(),
53            );
54
55            Some(quote! {
56                #[test]
57                fn #lit_test_name() {
58                    #fn_name(::std::path::Path::new(#lit_file_path));
59                }
60            })
61        })
62        .collect::<Vec<_>>();
63
64    if expanded.is_empty() {
65        return syn::Error::new(
66            glob_lit.span(),
67            format!(
68                "No valid files found for glob pattern: {}",
69                glob_path.value()
70            ),
71        )
72        .into_compile_error()
73        .into();
74    }
75
76    quote! {
77        fn #fn_name(#fn_args) #fn_block
78        #(#expanded)*
79    }
80    .into()
81}