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}