proptest-rs / proptest

Hypothesis-like property testing for Rust
Apache License 2.0
1.69k stars 158 forks source link

declaring necessary `proptest` for any implementation of a trait #372

Open yatesco opened 1 year ago

yatesco commented 1 year ago

Hi, I generally have the a thing-api and thing-lib, where the -api contains the public trait capturing the behaviours that thing offers. The actual implementation (e.g. struct TheRealThing) is in thing-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 with proptest.

I want to define a set of property tests in thing-api which thing-lib can call as separate property tests, providing the actual implementation, but I can't see how to inject the implementation from thing-lib into the proptests defined in thing-api without resorting to a macro.

I really want to write something like the following:

// thing-api
pub trait ICanThing {
  fn do_your_thing(&self);
}

pub fn assert_invariants(thing: impl ICanThing) {
  #proptest! {
    #[test]
    fn assert_invariant_one() { ... }
    #[test]
    fn assert_invariant_two() { ... }
  }
}

// thing-lib
pub struct TheRealThing {}
impl api::ICanThing for TheRealThing { ... }

#[test]
fn assert_invariants() {
  api::assert_invariants(TheRealThing {})
}

Is my only choice a macro in thing-api?

Thanks!

NOTE: at the moment the proptest stuff lives in thing-lib and works beautifully, but it feels like it is in the wrong place and really needs to be in thing-api

cameron1024 commented 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

yatesco commented 1 year ago

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 :-).

cameron1024 commented 1 year ago

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

yatesco commented 1 year ago

yes - that looks much more composable. Nice. Thanks @cameron1024