mzabaluev / generic-tests

Rust procedural macro to define tests and benchmarks generically
Other
5 stars 1 forks source link

Use `generic-tests` along with `test-case` #6

Open gollth opened 1 year ago

gollth commented 1 year ago

Hi there and first of all thanks for this great library.

I want to reduce my test logic by using parameterized tests both on the type and value level. For that I'm already using test-case library, but I can't this to work together with generic-tests:

struct Foo;
struct Bar;

#[cfg(test)]
#[generic_tests::define]
mod tests {
    use super::*;
    use test_case::test_case;

    #[test]
    #[test_case(1 ; "one")]
    #[test_case(2 ; "two")]
    #[test_case(3 ; "three")]
    fn test_blubbi<T>(x: u32) {
        // test logic that uses x: {1,2,3} * T: {Foo, Bar} thus 6 test cases in total
    }

    #[instantiate_tests(<Foo>)]
    mod foo {}

    #[instantiate_tests(<Bar>)]
    mod bar {}
}

But I get the error, that generic test function signatures must not take any arguments

error: functions used as tests can not have any arguments
 --> src/main.rs:9:1
  |
9 | #[generic_tests::define]
  | ^^^^^^^^^^^^^^^^^^^^^^^^
  |

What shall I do?

mzabaluev commented 1 year ago

I think this interaction is going to be problematic. It's not entirely clear to me in which order the proc macros are applied. For this to work without special provisions in generic_tests, the test_case macro has to do its thing first and the expanded token stream should look like plain old test case functions with no arguments. But apparently the generic_tests macro is invoked at the topmost level and receives the code with test_case unexpanded.

I'll look into whether the macro can just instantiate the function's arguments, but I think this will require some sanitization or limitations: what to do if the argument types entangle generic parameters?

Edit: none of the above is actually a problem, read the comment below.

mzabaluev commented 12 months ago

There would be a way to make it work with already implemented macro parameters, but it does not work as intended:

#[generic_tests::define(attrs(test_case))]
mod tests {
    use super::*;
    use test_case::test_case;

    #[test_case(1 ; "one")]
    #[test_case(2 ; "two")]
    #[test_case(3 ; "three")]
    fn test_blubbi<T>(x: u32) {
        // test logic that uses x: {1,2,3} * T: {Foo, Bar} thus 6 test cases in total
    }

    // ...
}

The problem is, the test_case::test_case import only exists on the tests module level. In the instantiation modules, there is a use super::*; import expanded by the macro, but the test_case attribute name is then ambiguous between this import and the language prelude (i.e. built-in macros that can't be turned off) that also has test_case.

I've worked around this by importing the macro as an alias name that does not collide with a built-in macro, but ran into some other problems. Bear with me, I need to debug this.

mzabaluev commented 12 months ago

It seems test_case exploits an undocumented and likely unstable detail of the built-in test framework. I can't import the macro under an alias and make it work, even in code that does not invoke generic-tests.

This looks hackish to the extent I'm not sure the generic_tests macro should do something special to accommodate it. One thing I can think of is to allow pre-populated content in the instantiation modules, so that test_case::test_case can be imported in each module. This somewhat defeats the idea of writing test code once and have the generic substitutions generated with minimal boilerplate, though.

gollth commented 12 months ago

Thanks for looking into this. For now I made it work using test-case alone, which also supports generics out of the box which is good enough for my testcase