Closed sageserpent-open closed 2 years ago
The flat-mapping over a range of sizes is something that I've had to do in several occasions - let's bring in some convenience overloads for that and leave other use-cases for explicit flat-mapping.
A thought - from the final thoughts on #32 regarding the interaction of value shrinking with use of Trials.*OfSize
in the existing codebase, given that value shrinking can cause the complexity to go below a threshold where a failing test case can be generated, then it might be prudent to implement this with TrialsApi.choose
driving the sizes, rather than say, TrialsApi.integers
.
We would still get complexity shrinkage, so although it might become quite inefficient at the end of the shrinkage, we'd still get some results - this was observed when working in #32.
Then again, maybe the underlying issue of not having multivariate shrinkage should be addressed once and for all?
For now, let's bodge this by using TrialsApi.choose
and come back to this once multivariate shrinkage is addressed...
This turns out to be very difficult to do in a clean manner - perhaps the design of TrialsImplementation
needs a rethink...
To summarise, introducing an interface on the Java side that extends Trials<Character>
with the extra fluent methods requires either some considerable faffing around with an F-bounded form of TrialsImplementation
or use of ByteBuddy to wrap the usual TrialsImplementation
in the extended interface via delegation. Certainly possible, but this all feels like a sledgehammer to crack a nut.
I did try defining default implementations of the fluent methods in the extended interface and hoped to use an intersection type for the trials returned by TrialsApi.characters
, but Java doesn't stretch that far, which is what led to the F-bound foolishness.
This might be worth revisiting, and ByteBuddy (or for that matter Scala 3 exporting) would allow the delegation approach - or I could just write a delegating implementation by hand - but not now.
Instead of the fancy fluent syntax, what is hopefully good enough has been proffered:
private static final Trials<String> first =
api.integers(1, 10)
.flatMap(size -> api
.characters('a', 'z', 'a')
.collectionsOfSize(size, Builder::stringBuilder));
private static final Trials<String> second =
api.integers(0, 10)
.flatMap(size -> api
.characters('0', '9', '0')
.collectionsOfSize(size, Builder::stringBuilder));
@Disabled
// This now detects the 'failing' test case correctly - but it is still a
// test failure. Need to rethink what this test should look like....
@Test
void copiedFromJqwik() {
first.and(second)
.withLimit(50)
.supplyTo((String first, String second) -> {
final String concatenation = first + second;
assertThat("Strings aren't allowed to be of length 4" +
" or 5 characters" + " in this test.",
4 > concatenation.length() ||
5 < concatenation.length());
});
}
Here we use explicit flat-mapping of sizes; it isn't that painful, and re-use the existing collections support, treating a String
as a kind of collection of Character
. It seems OK for now.
On the Scala side we do have the fancy Trials.strings
/ Trials.stringsOfSize
syntax via an implicit syntax extension - in fact even the Trials.several
/ Trials.lotsOfSize
will do this by default if applied to a Trials[Char]
.
In passing, the Java incarnation of Trials
has gone back to being an interface. Good.
This has gone out in version 1.1.0, commit b9a6185bad06695736daaea5fbd5086e1d0ce90a .
Reading this blog post about Jqwik: Property-based Testing in Java: The Importance of Being Shrunk introduced me to Jqwik's rather nice fluent syntax for configuring how its
Arbitrary
instances are built forString
.Quoted from said blog post:
Writing an equivalent using
Trials
as of commit 190467f0e4ff3261d769a1dac83a0950c6581de6 (Release 1.0.0) is feasible, but results in very prolix code in comparison:One way to improve the user experience with
Trials
would be to have syntax like this:This would also work with the existing method
characters()
defined inTrialsApi
- effectively givingcharacters
the same overloads of say,integers
- and thus allowing shrinking to apply, although I think that the existing implementation ofcharacters
should keep its non-shrinking behaviour; there is no obvious default to shrink to (this would also be the case for the overload that doesn't take an explicit shrinkage target too).This also implies that the existing
*OfSize
methods would acquire overloads with lower and upper bounds of a range of sizes.I'm not completely sure that the second part of this is necessary - it isn't hard to flat-map sizes, and that permits flexibility as to how the sizes are provided - could be a range or could be some other set: