-
Hello All, I have a small typescript module called fun in which I implement a "Schemable" type that represents a minimal abstract json-like builder interface. This schemable type can be implemented for parsers, refinement functions, json schema, and now Arbitrary. I've gone ahead and implemented this type over the various Arbitraries exported by fast-check but found myself in need of an Intersection (aka allof) Arbitrary. Generally, this Arbitrary is only used to intersect required and partial record arbitraries, so this is the only use case that I really need. I'm not familiar with the jargon inside of fast-check but I've got a working implementation here that I'm sure breaks some of the assumptions that fast-check makes. I'm hoping someone more knowledgeable with this library can review this implementation and provide some advice for improvements. I'll paste the relevant code below as well: export class IntersectArbitrary<U, V> extends fc.Arbitrary<U & V> {
constructor(private first: Arbitrary<U>, private second: Arbitrary<V>) {
super();
}
generate(mrng: Random, biasFactor: number | undefined): Value<U & V> {
const fst = this.first.generate(mrng, biasFactor);
const snd = this.second.generate(mrng, biasFactor);
return new fc.Value(
Object.assign({}, fst.value, snd.value),
mrng.nextInt(),
);
}
canShrinkWithoutContext(value: unknown): value is U & V {
return this.first.canShrinkWithoutContext(value) &&
this.second.canShrinkWithoutContext(value);
}
shrink(value: U & V, context: unknown): Stream<Value<U & V>> {
return fc.Stream.of(new fc.Value(value, context));
}
} Thanks in advance! |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
At first glance, your
You probably lost too much context in export class IntersectArbitrary<U, V> extends fc.Arbitrary<U & V> {
generate(mrng: Random, biasFactor: number | undefined): Value<U & V> {
const fst = this.first.generate(mrng, biasFactor);
const snd = this.second.generate(mrng, biasFactor);
return new fc.Value(
Object.assign({}, fst.value, snd.value),
{first: fst, second: snd},
);
}
// code...
shrink(value: U & V, context: unknown): Stream<Value<U & V>> {
const incompleteContext = context.first === undefined || context.second === undefined; // see comment "cannot produce..."
if (incompleteContext && !this.canShrinkWithoutContext(value)) {
return Stream.nil(); // unable to shrink with this partial context
}
if (context === undefined || incompleteContext) {
return this.first.shrink(value, undefined) // first told us it can shrink value without any context in canShrinkWithoutContext
.map(smallerFirst => new fc.Value(Object.assign({}, smallerFirst.value, value), {first: smallerFirst})) // cannot produce a context for second
.join(
this.second.shrink(value, undefined)
.map(smallerSecond => new fc.Value(Object.assign({}, value, smallerSecond.value), {second: smallerSecond}))
);
}
return this.first.shrink(context.first.value, context.first.context)
.map(smallerFirst => new fc.Value(Object.assign({}, smallerFirst.value, value), {first: smallerFirst, second: context.second}))
.join(
this.second.shrink(context.second.value, context.second.context)
.map(smallerSecond => new fc.Value(Object.assign({}, value, smallerSecond.value), {first: context.first, second: smallerSecond}))
);
}
fc.tuple(first, second)
.map((fst, snd) => Object.assign({}, fst.value, snd.value)) Regarding invariants, you are not breaking the key ones: no need to generate values before in the pipe to be able to replay. They are more or less defined in https://github.com/dubzzz/fast-check/blob/1f0a30ee837c297a534f71e5c6763a92f80a69c3/packages/fast-check/test/unit/arbitrary/__test-helpers__/ArbitraryAssertions.ts |
Beta Was this translation helpful? Give feedback.
At first glance, your
Arbitrary
looks great. There are some refinement that could be made:The second argument passed to
fc.Value
is never used in your case: It is supposed to be leveraged at shrinking time to get back how the values have been built (if and only if you want to provide a shrinker).The
canShrinkWithoutContext
is not symmetric withshrink
, or more precisely yourshrink
seems buggy: The methodshrink
will either be called with the context passed as a second argument offc.Value
or withundefined
in casecanShrinkWithoutContext
returned true.You probably lost too much context in
generate
if you want to shrink anything: