Define a proc_macro_attribute that pastes a set_up function block at the start of each decorated test function

Hello!

I'm trying to develop a testing framework in Rust which should have few capabilities to reduce boilerplate:

  • Has some necessary stuff pasted at the start of each function
  • The user can execute more operations by defining a fn set_up() that also get pasted at the start of each function

This is what I wanted to start with, yet it's probably the most challenging one.
I learnt about proc-macro which seemed perfect for that use-case, but I'll explain why they don't work very well (at least with the tried implementation).

tldr; the aim is to be able to write:

fn set_up() {
    let val = 2;
}
                                                                               
#[rustry_test]
fn test_works() {
    // if annotated with `#[rustry_test]` and that there is a set_up function,
    // the content of the `set_up` will be copy/pasted to each rustry_test.
    assert_eq!(val + 2, 4);
}
                                                                               
#[rustry_test]
fn test_very_works(x: U256) {
    // do more stuff
}

Latest try

I attempted to define a proc_macro_attribute to seek for a function called set_up and paste its content at the start of each annotated test function with #[rustry_test], but because it needs to call file()! in order to determine in which file path the attribute has been invoked from, we need to conditionally generate tokens at runtime, which is not something that is supported by Rust. Here is a code so you get an idea:

#[proc_macro_attribute]
pub fn rustry_test(_args: TokenStream, input: TokenStream) -> TokenStream {
    let fun = parse_macro_input!(input as ItemFn);
    let fname = fun.sig.ident;
    let block = fun.block;
    let hash_symbol = proc_macro2::Punct::new('#', Spacing::Joint);
    let set_up_block = {
        quote! {
            let filepath = std::path::PathBuf::from(file!());
            let code = std::fs::read_to_string(filepath).unwrap();
            let syntax = syn::parse_file(&code).unwrap();

            let _set_up_block = if let Some(set_up_fn) = syntax.items.into_iter().find(|item| {
                if let syn::Item::Fn(_fn) = item {
                    _fn.sig.ident == "set_up"
                } else {
                    false
                }
            }) {
                match set_up_fn {
                    syn::Item::Fn(syn::ItemFn { block, .. }) => {
                        let block: syn::Block = *block;
                        block.into_token_stream()
                    },
                    _ => unreachable!(),
                }
            } else {
                proc_macro2::TokenStream::new()
            };

            #hash_symbol _set_up_block
        }
    };

    quote! {
        #[test]
        pub fn #fname() {
            #set_up_block
            #block
        }
    }
    .into()
}

When annotating a function with the attribute, there is an error

Diagnostics:
expected one of `!` or `[`, found `_set_up_block`
expected one of `!` or `[`

Which may or may not be useful in this context, but again there is no way to expand the macro at runtime.

Is there any way to hack this please ?

Thanks a lot, and take care !

Procedural macros don’t have access to function bodies; they also cannot check whether something is defined or not, so even the ability to make defining fn set_up optional is nontrivial.

An approach that could work might look something like the following for the user:

macro_rules! set_up {
    () => {
        let val = 2;
    }
}
                                                                               
#[rustry_test(set_up)]
fn test_works() {
    // if annotated with `#[rustry_test(set_up)]` and that there is a “set_up” macro,
    // the expantion of the `set_up!()` will be inserted to each rustry_test(set_up).
    assert_eq!(val + 2, 4);
}

// the name could be choosen by the user, even
macro_rules! set_up2 {
    () => {
        let value_wow = 2;
    }
}
                                                                               
#[rustry_test(set_up2)]
fn test_works_2() {
    assert_eq!(value_wow + 2, 4);
}

// without “(set_up)”, no “set_up” macro expansion is inserted
#[rustry_test]
fn test_very_works(x: U256) {
    // do more stuff
}
1 Like

Thanks !

That would have been a good solution, but sadly this doesn't work due to macro hygiene. Is the safest bet to add the $val:ident for each variable that will be reused from the set_up ?

For instance, this would compile

macro_rules! set_up {
    ($val:ident) => {
        let $val = 2;
    };
}

fn test_works() {
    set_up!(val);
    assert_eq!(val + 2, 4);
    let var = val + 3;
}

But at the same time it's also quite a lot of hand work.

Oh, of course, that’s an issue! Gotta consider hygiene all the way :sweat_smile: Yeah, I suppose one could pass the variable names, too, to make it work.

1 Like