BurntSushi / quickcheck

Automated property based testing for Rust (with shrinking).
The Unlicense
2.4k stars 149 forks source link

Implement testable for `fn(&mut Gen) -> T` #246

Closed recmo closed 4 years ago

recmo commented 4 years ago

I'm currently testing code that (for sake of argument) combines tables. Let's say I have functions combine_horizontal(Table, Table), combine_vertical, etc. and I want to test them. Currently this would look like:

#[quickcheck]
fn test_combine_horizontal(left: Table, right: Table) -> TestResult {
   if a.rows() != right.rows() {
      return TestResult::discard()
   }
   // ...
}

The problem is that the non-elided results are a subset of measure zero in the total set of inputs. i.e. We are unlikely to test anything! (though in practice some small cases come through).

I would love for there to be something like

    impl<T: Testable> Testable for fn<G: Gen>(&mut G) -> T

so I can write

#[quickcheck]
fn test_combine_horizontal<G: Gen>(g: &mut Gen) -> bool {
   let rows = usize::arbitrary(g) % g.size();
   let left = arbitrary_table(rows, g);
   let right = arbitrary_table(rows, g);
   // ...
}

In general, the Arbitrary trait does not give a lot of control over the distribution of the values, but the desired distribution can depend on the specific test. The proposed solution would provide a simple way around this limitation.

BurntSushi commented 4 years ago

I don't see any particular reason why this is necessary. You should already be able to do this by creating, say, a TablePair type and implementing Arbitrary for it. Moreover, have you tested your proposal? I'm on mobile, but it seems very likely that your desired trait impl would not be allowed due to the preexisting blanket impls.

recmo commented 4 years ago

I tried implementing it as impl Testable for fn(&mut dyn Gen) -> Testable but that is not allowed due to the orphan rule. To properly test it out I'd have to either locally fork quickcheck or introduce new types. But introducing new types would defeat my goal of avoiding new-type boilerplate for one-off test distributions. My actual distributions are a bit more complicated than the simple example I gave, but basicaly I have one, two or more instances as input that need to be the same in per-test different aspects like number of rows, number of columns, and or some other aspects.

I've since solved this with proptest, which was marginally easier because the boilerplate for a new distribution is a single function, instead of a new type and a trait implementation.

Still I think an approach where the test function has access to a Gen, combined with utility functions and traits to produce various objects in various distributions would be most generic. I may retry this at some point.

BurntSushi commented 4 years ago

What I'm saying is that your proposed trait impl would overlap with existing trait impls on fn. It isn't about the orphan rule, but rather, overlapping impls, which aren't allowed without specialization.

This seems like a somewhat niche use case. If proptest works then that's great. Otherwise, I'm going to close this for now.