-
-
Notifications
You must be signed in to change notification settings - Fork 176
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Mutually recursive arbitrary builder (#377)
* First draft implementation of letrec for fast-check * Add support for bias for letrec * Better typings for the tie function * Add unit tests for letrec * Add e2e tests for letrec * Do not prettify dist/ * Add test to check recursive shrinker * Add jsdoc * Delayed calls to tie should be considered valid * Check tie throws on generate when receiving invalid inputs * Remove unnecessary check in withBias * Check bias applies at most up to depth 5 * Add no regression tests for letrec * Add letrec documentation
- Loading branch information
Showing
9 changed files
with
349 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
lib/ | ||
lib-*/ | ||
coverage/ | ||
dist/ | ||
docs/ | ||
*.generated.ts | ||
*.generated.spec.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { Random } from '../../random/generator/Random'; | ||
import { Arbitrary } from './definition/Arbitrary'; | ||
import { Shrinkable } from './definition/Shrinkable'; | ||
|
||
/** @hidden */ | ||
export class LazyArbitrary extends Arbitrary<any> { | ||
private static readonly MaxBiasLevels = 5; | ||
private numBiasLevels = 0; | ||
underlying: Arbitrary<any> | null = null; | ||
|
||
constructor(readonly name: string) { | ||
super(); | ||
} | ||
generate(mrng: Random): Shrinkable<any> { | ||
if (!this.underlying) { | ||
throw new Error(`Lazy arbitrary ${JSON.stringify(this.name)} not correctly initialized`); | ||
} | ||
return this.underlying.generate(mrng); | ||
} | ||
withBias(freq: number): Arbitrary<any> { | ||
if (!this.underlying) { | ||
throw new Error(`Lazy arbitrary ${JSON.stringify(this.name)} not correctly initialized`); | ||
} | ||
if (this.numBiasLevels >= LazyArbitrary.MaxBiasLevels) { | ||
return this; | ||
} | ||
++this.numBiasLevels; | ||
const biasedArb = this.underlying.withBias(freq); | ||
--this.numBiasLevels; | ||
return biasedArb; | ||
} | ||
} | ||
|
||
/** @hidden */ | ||
function isLazyArbitrary(arb: Arbitrary<any> | undefined): arb is LazyArbitrary { | ||
return arb !== undefined && arb.hasOwnProperty('underlying'); | ||
} | ||
|
||
/** | ||
* For mutually recursive types | ||
* | ||
* @example | ||
* ```typescript | ||
* const { tree } = fc.letrec(tie => ({ | ||
* tree: fc.oneof(tie('node'), tie('leaf'), tie('leaf')), | ||
* node: fc.tuple(tie('tree'), tie('tree')), | ||
* leaf: fc.nat() | ||
* })); // tree is 1 / 3 of node, 2 / 3 of leaf | ||
* ``` | ||
* | ||
* @param builder Arbitraries builder based on themselves (through `tie`) | ||
*/ | ||
export function letrec<T>( | ||
builder: (tie: (key: string) => Arbitrary<unknown>) => { [K in keyof T]: Arbitrary<T[K]> } | ||
): { [K in keyof T]: Arbitrary<T[K]> } { | ||
const lazyArbs: { [K in keyof T]?: Arbitrary<T[K]> } = {}; | ||
const tie = (key: keyof T): Arbitrary<any> => { | ||
if (!lazyArbs[key]) lazyArbs[key] = new LazyArbitrary(key as any); | ||
return lazyArbs[key]!; | ||
}; | ||
const strictArbs = builder(tie as any); | ||
for (const key in strictArbs) { | ||
if (!strictArbs.hasOwnProperty(key)) { | ||
// Prevents accidental iteration over properties inherited from an object’s prototype | ||
continue; | ||
} | ||
|
||
const lazyAtKey = lazyArbs[key]; | ||
const lazyArb = isLazyArbitrary(lazyAtKey) ? lazyAtKey : new LazyArbitrary(key); | ||
lazyArb.underlying = strictArbs[key]; | ||
lazyArbs[key] = lazyArb; | ||
} | ||
return strictArbs; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import * as fc from '../../../src/fast-check'; | ||
|
||
const seed = Date.now(); | ||
describe(`LetRecArbitrary (seed: ${seed})`, () => { | ||
describe('letrec', () => { | ||
it('Should be able to rebuild simple arbitraries', () => { | ||
const ref = fc.array(fc.tuple(fc.string(), fc.integer())); | ||
const { b } = fc.letrec(tie => ({ | ||
a: fc.integer(), | ||
b: fc.array(tie('c')), | ||
c: fc.tuple(tie('d'), tie('a')), | ||
d: fc.string() | ||
})); | ||
expect(fc.sample(b, { seed })).toEqual(fc.sample(ref, { seed })); | ||
}); | ||
it('Should be usable to build deep tree instances', () => { | ||
const { tree } = fc.letrec(tie => ({ | ||
// tree is 1 / 3 of node, 2 / 3 of leaf | ||
tree: fc.oneof(tie('node'), tie('leaf'), tie('leaf')), | ||
node: fc.tuple(tie('tree'), tie('tree')), | ||
leaf: fc.nat() | ||
})); | ||
const out = fc.check( | ||
fc.property(tree, t => { | ||
const depth = (n: any): number => { | ||
if (typeof n === 'number') return 0; | ||
return 1 + Math.max(depth(n[0]), depth(n[1])); | ||
}; | ||
return depth(t) < 5; | ||
}), | ||
{ seed } | ||
); | ||
expect(out.failed).toBe(true); // depth can be greater or equal to 5 | ||
}); | ||
it('Should be able to shrink to smaller cases recursively', () => { | ||
const { tree } = fc.letrec(tie => { | ||
return { | ||
tree: fc.nat(1).chain(id => (id === 0 ? tie('leaf') : tie('node'))), | ||
node: fc.tuple(tie('tree'), tie('tree')), | ||
leaf: fc.nat() | ||
}; | ||
}); | ||
const out = fc.check(fc.property(tree, t => typeof t !== 'object'), { seed }); | ||
expect(out.failed).toBe(true); | ||
expect(out.counterexample![0]).toEqual([0, 0]); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import { LazyArbitrary, letrec } from '../../../../src/check/arbitrary/LetRecArbitrary'; | ||
import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary'; | ||
import { Shrinkable } from '../../../../src/check/arbitrary/definition/Shrinkable'; | ||
import { Random } from '../../../../src/random/generator/Random'; | ||
|
||
import * as stubRng from '../../stubs/generators'; | ||
|
||
describe('LetRecArbitrary', () => { | ||
describe('letrec', () => { | ||
it('Should be able to construct independant arbitraries', () => { | ||
const expectedArb1 = buildArbitrary(jest.fn()); | ||
const expectedArb2 = buildArbitrary(jest.fn()); | ||
|
||
const { arb1, arb2 } = letrec(tie => ({ | ||
arb1: expectedArb1, | ||
arb2: expectedArb2 | ||
})); | ||
|
||
expect(arb1).toBe(expectedArb1); | ||
expect(arb2).toBe(expectedArb2); | ||
}); | ||
it('Should not produce LazyArbitrary for no-tie constructs', () => { | ||
const { arb } = letrec(tie => ({ | ||
arb: buildArbitrary(jest.fn()) | ||
})); | ||
expect(arb).not.toBeInstanceOf(LazyArbitrary); | ||
}); | ||
it('Should not produce LazyArbitrary for indirect tie constructs', () => { | ||
const { arb } = letrec(tie => ({ | ||
// arb is an arbitrary wrapping the tie value (as fc.array) | ||
arb: buildArbitrary(mrng => tie('arb').generate(mrng)) | ||
})); | ||
expect(arb).not.toBeInstanceOf(LazyArbitrary); | ||
}); | ||
it('Should produce LazyArbitrary for direct tie constructs', () => { | ||
const { arb } = letrec(tie => ({ | ||
arb: tie('arb') | ||
})); | ||
expect(arb).toBeInstanceOf(LazyArbitrary); | ||
}); | ||
it('Should be able to construct mutually recursive arbitraries', () => { | ||
const { arb1, arb2 } = letrec(tie => ({ | ||
arb1: tie('arb2'), | ||
arb2: tie('arb1') | ||
})); | ||
expect(arb1).toBeDefined(); | ||
expect(arb2).toBeDefined(); | ||
}); | ||
it('Should apply tie correctly', () => { | ||
const expectedArb = buildArbitrary(jest.fn()); | ||
const { arb1, arb2, arb3 } = letrec(tie => ({ | ||
arb1: tie('arb2'), | ||
arb2: tie('arb3'), | ||
arb3: expectedArb | ||
})); | ||
|
||
expect(arb1).toBeInstanceOf(LazyArbitrary); | ||
expect(arb2).toBeInstanceOf(LazyArbitrary); | ||
expect(arb3).not.toBeInstanceOf(LazyArbitrary); | ||
|
||
expect((arb1 as any).underlying).toBe(arb2); | ||
expect((arb2 as any).underlying).toBe(arb3); | ||
expect(arb3).toBe(expectedArb); | ||
}); | ||
it('Should be able to delay calls to tie', () => { | ||
const mrng = stubRng.mutable.nocall(); | ||
const generateMock = jest.fn(); | ||
const simpleArb = buildArbitrary(generateMock); | ||
const { arb1 } = letrec(tie => ({ | ||
arb1: buildArbitrary(mrng => tie('arb2').generate(mrng)), | ||
arb2: simpleArb | ||
})); | ||
|
||
expect(generateMock).not.toHaveBeenCalled(); | ||
arb1.generate(mrng); | ||
|
||
expect(generateMock).toHaveBeenCalled(); | ||
}); | ||
it('Should throw on generate if tie receives an invalid parameter', () => { | ||
const mrng = stubRng.mutable.nocall(); | ||
const { arb1 } = letrec(tie => ({ | ||
arb1: tie('missing') | ||
})); | ||
expect(() => arb1.generate(mrng)).toThrowErrorMatchingSnapshot(); | ||
}); | ||
it('Should throw on generate if tie receives an invalid parameter after creation', () => { | ||
const mrng = stubRng.mutable.nocall(); | ||
const { arb1 } = letrec(tie => ({ | ||
arb1: buildArbitrary(mrng => tie('missing').generate(mrng)) | ||
})); | ||
expect(() => arb1.generate(mrng)).toThrowErrorMatchingSnapshot(); | ||
}); | ||
}); | ||
describe('LazyArbitrary', () => { | ||
it('Should fail to generate when no underlying arbitrary', () => { | ||
const mrng = stubRng.mutable.nocall(); | ||
const lazy = new LazyArbitrary('id007'); | ||
expect(() => lazy.generate(mrng)).toThrowErrorMatchingSnapshot(); | ||
}); | ||
it('Should fail to bias when no underlying arbitrary', () => { | ||
const lazy = new LazyArbitrary('id008'); | ||
expect(() => lazy.withBias(2)).toThrowErrorMatchingSnapshot(); | ||
}); | ||
it('Should call generate method of underlying on generate', () => { | ||
const mrng = stubRng.mutable.nocall(); | ||
const lazy = new LazyArbitrary('id008'); | ||
const expectedGen = Symbol(); | ||
const generateMock = jest.fn(); | ||
generateMock.mockReturnValue(expectedGen); | ||
lazy.underlying = buildArbitrary(generateMock); | ||
|
||
const g = lazy.generate(mrng); | ||
expect(g).toBe(expectedGen); | ||
expect(generateMock).toHaveBeenCalledTimes(1); | ||
}); | ||
it('Should be able to bias to the biased value of underlying', () => { | ||
const lazy = new LazyArbitrary('id008'); | ||
const noCallMock = jest.fn(); | ||
const biasedArb = buildArbitrary(noCallMock); | ||
const arb = buildArbitrary(noCallMock, () => biasedArb); | ||
lazy.underlying = arb; | ||
|
||
const biasedLazy = lazy.withBias(2); | ||
expect(biasedLazy).toBe(biasedArb); | ||
expect(noCallMock).not.toHaveBeenCalled(); | ||
}); | ||
it('Should be able to bias recursive arbitraries', () => { | ||
const lazy = new LazyArbitrary('id008'); | ||
const noCallMock = jest.fn(); | ||
lazy.underlying = buildArbitrary(noCallMock, n => lazy.withBias(n)); | ||
|
||
const biasedLazy = lazy.withBias(2); | ||
expect(biasedLazy).toBe(lazy); | ||
expect(noCallMock).not.toHaveBeenCalled(); | ||
}); | ||
it('Should bias recursive arbitraries at a max depth of 5', () => { | ||
const lazyA = new LazyArbitrary('id007'); | ||
const lazyB = new LazyArbitrary('id008'); | ||
|
||
const noCallMock = jest.fn(); | ||
const biasAMock = jest.fn(); | ||
const biasBMock = jest.fn(); | ||
biasAMock.mockImplementation(n => lazyB.withBias(n)); | ||
biasBMock.mockImplementation(n => lazyB.withBias(n)); | ||
lazyA.underlying = buildArbitrary(noCallMock, biasAMock); | ||
lazyB.underlying = buildArbitrary(noCallMock, biasBMock); | ||
|
||
const biasedLazyA = lazyA.withBias(2); | ||
expect(biasedLazyA).toBe(lazyB); | ||
expect(biasAMock).toHaveBeenCalledTimes(1); | ||
expect(biasBMock).toHaveBeenCalledTimes(5); | ||
}); | ||
}); | ||
}); | ||
|
||
const buildArbitrary = (generate: (mrng: Random) => Shrinkable<any>, withBias?: (n: number) => Arbitrary<any>) => { | ||
return new (class extends Arbitrary<any> { | ||
generate = generate; | ||
withBias = (n: number): Arbitrary<any> => (withBias ? withBias(n) : this); | ||
})(); | ||
}; |
9 changes: 9 additions & 0 deletions
9
test/unit/check/arbitrary/__snapshots__/LetRecArbitrary.spec.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`LetRecArbitrary LazyArbitrary Should fail to bias when no underlying arbitrary 1`] = `"Lazy arbitrary \\"id008\\" not correctly initialized"`; | ||
|
||
exports[`LetRecArbitrary LazyArbitrary Should fail to generate when no underlying arbitrary 1`] = `"Lazy arbitrary \\"id007\\" not correctly initialized"`; | ||
|
||
exports[`LetRecArbitrary letrec Should throw on generate if tie receives an invalid parameter 1`] = `"Lazy arbitrary \\"missing\\" not correctly initialized"`; | ||
|
||
exports[`LetRecArbitrary letrec Should throw on generate if tie receives an invalid parameter after creation 1`] = `"Lazy arbitrary \\"missing\\" not correctly initialized"`; |