How do you shrink recursive records?
See original GitHub issue💬 Question and Help
I am trying to port my property based test from jsverify
to fast-check
, which was easy to do, except for the shrinking of my record values. In jsverify
I have to supply my own shrinking functions, which although a pain to write, are straightforward to write and understand. You just create smaller values in a lazy sequence. Unfortunately it seems the shrinking part of fast-check
is undocumented, I had to look at the source code of some arbitraries. However they seem a bit daunting to translate to my use case.
I have simplified my use case to the following code:
import * as fc from "fast-check"
type Record = { contents: Content[] }
type Content = number | Record
const valueArb = fc.nat(999)
const recordArb = fc.memo(n => fc.record({ contents: n > 1 ? contentsArb() : fc.array(valueArb) }))
const contentsArb: fc.Memo<Content[]> = fc.memo(n => fc.array(fc.oneof(valueArb, recordArb(n))))
const includesBadValue = (content: Content) =>
typeof content === "number" ? content === 666 : content.contents.some(includesBadValue)
fc.assert(fc.property(recordArb(5), record => !includesBadValue(record)))
And this produces errors like:
Error: Property failed after 1 tests
{ seed: -80021587, path: "0:3:4:6:5:7:9:8:19:18:16:14:12", endOnFailure: true }
Counterexample: [{"contents":[{"contents":[{"contents":[{"contents":[{"contents":[666]}]}]}]}]}]
Shrunk 12 time(s)
Got error: Property failed by returning false
The counter example could however be simplified to {"contents":[666]}
, which it doesn’t do. Must I write my own fc.Arbitrary<Record>
to make this happen? I did try this, but am unsure how to supply a list of potential shrinks:
class RecordArbitrary extends fc.Arbitrary<Record> {
constructor(readonly n: number) {
super()
}
generate(mrng: fc.Random): fc.Shrinkable<Record> {
const recordShrinkable = recordArb(this.n).generate(mrng)
return new fc.Shrinkable(recordShrinkable.value_, () =>
recordShrinkable.shrink().join(
// This obviously does not work, but conceptually is what I want:
// record.contents.filter(content => typeof content === "object") as Record[]
)
)
}
}
Is there any documentation I might have missed that can help me out, or some easier and more fitting examples than say ArrayArbitrary
? Or am I going at it all wrong?
Issue Analytics
- State:
- Created 3 years ago
- Comments:6 (3 by maintainers)
Top GitHub Comments
Huray! The last idea was indeed the problem with my previous
boolean().chain(...)
approach, after chaining the contents, making sure the generated value is the same as in the nesting (it is what produced the failure after all), it is no longer left to chance, but will now consistently shrink in the same way, meaning[666]
. This is the code that finally worked for me:I will close the issue, as your suggestion ended up giving me a working solution, but I would love to know if there is a better way or if there is some more documentation on
ArbitraryWithShrink
on how to actually use it in practice, rather than the theory behind it (that is well enough documented inAdvancedArbitraries.md
andHowItWorks.md
). Or more to the point, how to add additional shrinkables to an existing arbitrary.I tried to implement
ArbitraryWithShrink<Record>
, but theshrink
method is never called, so I am not quite sure how to use it. Thus I tried to implementArbitrary<Record>
and got it to shrink the recursion, except that other further shrinking is no longer done, which makes sense, as those shrink streams are lost without access to theShrinkable
that produced the values. Here is what I tried:My guess would be that if I wanted it to work with a custom
Arbitrary
, I will have to do something troublesome like keeping track of all contents arbitraries and creating and reimplementing most of array shrinking myself, like it seems to be done forCommandsArbitrary.ts
. I hope that is not the case, as that is at the level that should remain implementation details of the tester, not something you have to write for adding some custom shrinkage.In a last attempt I also tried the
boolean().chain(...)
trick, after thinking of a way to get it to flatten the recursion onfalse
, but it does not do so consistently. While it does result in consistenly smaller values, it does not actually get as minimal as it could (that being[666]
). This is how I went around doing so:One last idea I have is to chain
contents
, as I think it does not quite yet do as I intended (the same exact contents should be either become a record, i.e. a nesting, or become part of the contents, i.e. remain flat).