Open yatesco opened 1 year ago
Well the macro just creates a runner and calls it, it's certainly possible to do that in macro-less code. But it's probably more verbose than is ideal. (Though it's probably possible to pull a lot of this into a helper function)
Off the top of my head, I could imagine an attribute macro that you could apply to a module containing a bunch of property tests that would create a function that can be called with a concrete instance of that trait, and would run all the tests against it, though that approach isn't without its drawbacks. The main one I can think of is that you'd essentially lose the granularity of tests (which your example also suffers from - there's only 1 #[test]
).
Alternatively, such a macro could also generate a decl-macro that you could invoke in your lib crate that would expand into all the correct tests, and you could just provide a fn to get an implementor of the trait.
But I agree with you that the invariants make more sense being declared in the API crate, I just don't think it's worth losing the ability to cargo test invariant_1
. In some crates I work with, there are proptests that take multiple hours to run, while others take seconds. And I don't think a custom proptest CLI would be the way to go.
I'll try to come up with a draft design when I'm in front of my laptop
thanks @cameron1024 - I'm excited to see what you come up with, but please don't think this is a urgent need! I think it's a interesting design question, but the existing implementation is more than good enough :-).
So IMO this is something that should be done after the attribute macro work is finished (in case you haven't seen, it just means the ability to write #[proptest]
where you would usually write #[test]
instead of using the function-like macro proptest! { }
.
But I'd like to see something like this:
#[invariant_set(MyTrait)]
mod invariants {
#[proptest]
fn invariant_1(my_trait: impl MyTrait, s: String) {
// my_trait is "any struct which implements `MyTrait`
// s is an arbitrary string
let foo = my_trait.frobnicate(s);
assert!(foo.bar());
}
#[proptest]
fn invariant_2(my_trait: impl MyTrait, s: String) {
todo!()
}
}
This would expand to a macro_rules
invocation that can then be used like this (in the lib crate):
my_api_crate::my_trait_proptest_invariants!(MyTraitImplementor::new(1, 2, 3));
This generated macro would then expand to:
#[proptest]
fn invariant_1(s: String) {
let my_trait = MyTraitImplementor::new(1, 2, 3);
// my_trait is "any struct which implements `MyTrait`
// s is an arbitrary string
let foo = my_trait.frobnicate(s);
assert!(foo.bar());
}
#[proptest]
fn invariant_2(s: String) {
let my_trait = MyTraitImplementor::new(1, 2, 3);
todo!()
}
This way, we get to keep individual tests that cargo test
is aware of.
I don't think this would be too hard to implement, but I think it's not worth doing for the proptest! {}
syntax, which there seems to be a general sentiment to move away from
yes - that looks much more composable. Nice. Thanks @cameron1024
Hi, I generally have the a
thing-api
andthing-lib
, where the-api
contains the publictrait
capturing the behaviours thatthing
offers. The actual implementation (e.g.struct TheRealThing
) is inthing-lib
.I'd love to be able to define a bunch of invariants that any implementation of
thing-api\ICanThing
must implement, but I can't figure out how to do this withproptest
.I want to define a set of property tests in
thing-api
whichthing-lib
can call as separate property tests, providing the actual implementation, but I can't see how to inject the implementation fromthing-lib
into theproptest
s defined inthing-api
without resorting to a macro.I really want to write something like the following:
Is my only choice a macro in
thing-api
?Thanks!
NOTE: at the moment the
proptest
stuff lives inthing-lib
and works beautifully, but it feels like it is in the wrong place and really needs to be inthing-api