Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
295 additions
and
1 deletion.
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
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,60 @@ | ||
import { Random } from '../../random/generator/Random'; | ||
import { Arbitrary } from './definition/Arbitrary'; | ||
import { Shrinkable } from './definition/Shrinkable'; | ||
|
||
/** @hidden */ | ||
export class MemoArbitrary<T> extends Arbitrary<T> { | ||
private lastFreq = -1; | ||
private lastBiased: Arbitrary<T> = this; | ||
constructor(readonly underlying: Arbitrary<T>) { | ||
super(); | ||
} | ||
generate(mrng: Random): Shrinkable<T> { | ||
return this.underlying.generate(mrng); | ||
} | ||
withBias(freq: number): Arbitrary<T> { | ||
if (freq !== this.lastFreq) { | ||
this.lastFreq = freq; | ||
this.lastBiased = this.underlying.withBias(freq); | ||
} | ||
return this.lastBiased; | ||
} | ||
} | ||
|
||
/** | ||
* Output type for {@link memo} | ||
*/ | ||
export type Memo<T> = (maxDepth?: number) => Arbitrary<T>; | ||
|
||
/** @hidden */ | ||
let contextRemainingDepth = 10; | ||
|
||
/** | ||
* For mutually recursive types | ||
* | ||
* @example | ||
* ```typescript | ||
* // tree is 1 / 3 of node, 2 / 3 of leaf | ||
* const tree: fc.Memo<Tree> = fc.memo(n => fc.oneof(node(n), leaf(), leaf())); | ||
* const node: fc.Memo<Tree> = fc.memo(n => { | ||
* if (n <= 1) return fc.record({ left: leaf(), right: leaf() }); | ||
* return fc.record({ left: tree(), right: tree() }); // tree() is equivalent to tree(n-1) | ||
* }); | ||
* const leaf = fc.nat; | ||
* ``` | ||
* | ||
* @param builder Arbitrary builder taken the maximal depth allowed as input (parameter `n`) | ||
*/ | ||
export const memo = <T>(builder: (maxDepth: number) => Arbitrary<T>): Memo<T> => { | ||
const previous: { [depth: number]: Arbitrary<T> } = {}; | ||
return ((maxDepth?: number): Arbitrary<T> => { | ||
const n = maxDepth !== undefined ? maxDepth : contextRemainingDepth; | ||
if (!previous.hasOwnProperty(n)) { | ||
const prev = contextRemainingDepth; | ||
contextRemainingDepth = n - 1; | ||
previous[n] = new MemoArbitrary(builder(n)); | ||
contextRemainingDepth = prev; | ||
} | ||
return previous[n]; | ||
}) as Memo<T>; | ||
}; |
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,72 @@ | ||
import * as fc from '../../../src/fast-check'; | ||
|
||
type Tree = Node | Leaf; | ||
type Node = { | ||
left: Tree; | ||
right: Tree; | ||
}; | ||
type Leaf = number; | ||
|
||
const seed = Date.now(); | ||
describe(`MemoArbitrary (seed: ${seed})`, () => { | ||
describe('memo', () => { | ||
it('Should be able to build deep tree instances (manual depth)', () => { | ||
const leaf = fc.nat; | ||
|
||
// tree is 1 / 3 of node, 2 / 3 of leaf | ||
const tree: fc.Memo<Tree> = fc.memo(n => fc.oneof(node(n), leaf(), leaf())); | ||
const node: fc.Memo<Tree> = fc.memo(n => { | ||
if (n <= 1) return fc.record({ left: leaf(), right: leaf() }); | ||
return fc.record({ left: tree(), right: tree() }); // tree() is equivalent to tree(n-1) | ||
}); | ||
|
||
const maxDepth = 3; | ||
const out = fc.check( | ||
fc.property(tree(maxDepth), t => { | ||
const depth = (n: Tree): number => { | ||
if (typeof n === 'number') return 0; | ||
return 1 + Math.max(depth(n.left), depth(n.right)); | ||
}; | ||
return depth(t) < maxDepth; | ||
}), | ||
{ seed } | ||
); | ||
expect(out.failed).toBe(true); | ||
}); | ||
it('Should be able to build tree instances with limited depth (manual depth)', () => { | ||
const leaf = fc.nat; | ||
|
||
// tree is 1 / 3 of node, 2 / 3 of leaf | ||
const tree: fc.Memo<Tree> = fc.memo(n => fc.oneof(node(n), leaf(), leaf())); | ||
const node: fc.Memo<Tree> = fc.memo(n => { | ||
if (n <= 1) return fc.record({ left: leaf(), right: leaf() }); | ||
return fc.record({ left: tree(), right: tree() }); // tree() is equivalent to tree(n-1) | ||
}); | ||
|
||
const maxDepth = 3; | ||
const out = fc.check( | ||
fc.property(tree(maxDepth), t => { | ||
const depth = (n: Tree): number => { | ||
if (typeof n === 'number') return 0; | ||
return 1 + Math.max(depth(n.left), depth(n.right)); | ||
}; | ||
return depth(t) <= maxDepth; | ||
}), | ||
{ seed } | ||
); | ||
expect(out.failed).toBe(false); | ||
}); | ||
it('Should be able to shrink to smaller cases recursively', () => { | ||
const leaf = fc.nat; | ||
const tree: fc.Memo<Tree> = fc.memo(n => fc.nat(1).chain(id => (id === 0 ? leaf() : node(n)))); | ||
const node: fc.Memo<Tree> = fc.memo(n => { | ||
if (n <= 1) return fc.record({ left: leaf(), right: leaf() }); | ||
return fc.record({ left: tree(), right: tree() }); // tree() is equivalent to tree(n-1) | ||
}); | ||
|
||
const out = fc.check(fc.property(tree(), t => typeof t !== 'object'), { seed }); | ||
expect(out.failed).toBe(true); | ||
expect(out.counterexample![0]).toEqual({ left: 0, right: 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,145 @@ | ||
import { MemoArbitrary, memo } from '../../../../src/check/arbitrary/MemoArbitrary'; | ||
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('MemoArbitrary', () => { | ||
describe('memo', () => { | ||
it('Should wrap the arbitrary into a MemoArbitrary', () => { | ||
const expectedArb = buildArbitrary(jest.fn()); | ||
const builder = memo(() => expectedArb); | ||
const arb = builder(); | ||
|
||
expect(arb).toBeInstanceOf(MemoArbitrary); | ||
expect(((arb as any) as MemoArbitrary<any>).underlying).toBe(expectedArb); | ||
}); | ||
it('Should cache arbitraries associated to each depth', () => { | ||
const expectedArb = buildArbitrary(jest.fn()); | ||
const builder = memo(() => expectedArb); | ||
const arb = builder(); | ||
|
||
expect(builder(10)).toBe(builder(10)); | ||
expect(builder(42)).toBe(builder(42)); | ||
expect(builder(65500)).toBe(builder(65500)); | ||
expect(builder()).toBe(arb); | ||
}); | ||
it('Should instantiate new arbitraries for each depth', () => { | ||
const expectedArb = buildArbitrary(jest.fn()); | ||
const builder = memo(() => expectedArb); | ||
|
||
expect(builder(10)).not.toBe(builder(42)); | ||
}); | ||
it('Should consider no depth as depth 10', () => { | ||
const expectedArb = buildArbitrary(jest.fn()); | ||
const builder = memo(() => expectedArb); | ||
|
||
expect(builder()).toBe(builder(10)); | ||
}); | ||
it('Should automatically decrease depth for self recursive', () => { | ||
const expectedArb = buildArbitrary(jest.fn()); | ||
const memoFun = jest.fn(); | ||
memoFun.mockImplementation(n => (n <= 6 ? expectedArb : builder())); | ||
const builder = memo(memoFun); | ||
|
||
builder(); | ||
|
||
expect(memoFun.mock.calls).toEqual([[10], [9], [8], [7], [6]]); | ||
}); | ||
it('Should automatically interleave decrease depth for mutually recursive', () => { | ||
const expectedArb = buildArbitrary(jest.fn()); | ||
const memoFunA = jest.fn(); | ||
memoFunA.mockImplementation(n => (n <= 6 ? expectedArb : builderB())); | ||
const memoFunB = jest.fn(); | ||
memoFunB.mockImplementation(n => (n <= 6 ? expectedArb : builderA())); | ||
const builderA = memo(memoFunA); | ||
const builderB = memo(memoFunB); | ||
|
||
builderA(); | ||
|
||
expect(memoFunA.mock.calls).toEqual([[10], [8], [6]]); | ||
expect(memoFunB.mock.calls).toEqual([[9], [7]]); | ||
}); | ||
it('Should be able to override decrease depth', () => { | ||
const expectedArb = buildArbitrary(jest.fn()); | ||
const memoFun = jest.fn(); | ||
memoFun.mockImplementation(n => (n <= 0 ? expectedArb : builder(n - 3))); | ||
const builder = memo(memoFun); | ||
|
||
builder(); | ||
|
||
expect(memoFun.mock.calls).toEqual([[10], [7], [4], [1], [-2]]); | ||
}); | ||
it('Should be able to delay calls to sub-builders', () => { | ||
const mrng = stubRng.mutable.nocall(); | ||
const generateMock = jest.fn(); | ||
const simpleArb = buildArbitrary(generateMock); | ||
const builderA = memo(() => buildArbitrary(mrng => builderB().generate(mrng))); | ||
const builderB = memo(() => simpleArb); | ||
|
||
expect(generateMock).not.toHaveBeenCalled(); | ||
builderA().generate(mrng); | ||
|
||
expect(generateMock).toHaveBeenCalled(); | ||
}); | ||
}); | ||
describe('MemoArbitrary', () => { | ||
it('Should call generate method of underlying on generate', () => { | ||
const mrng = stubRng.mutable.nocall(); | ||
const expectedGen = Symbol(); | ||
const generateMock = jest.fn(); | ||
generateMock.mockReturnValue(expectedGen); | ||
const arb = buildArbitrary(generateMock); | ||
const memoArb = new MemoArbitrary(arb); | ||
|
||
const g = memoArb.generate(mrng); | ||
expect(g).toBe(expectedGen); | ||
expect(generateMock).toHaveBeenCalledTimes(1); | ||
}); | ||
it('Should be able to bias to the biased value of underlying', () => { | ||
const noCallMock = jest.fn(); | ||
const biasedArb = buildArbitrary(noCallMock); | ||
const arb = buildArbitrary(noCallMock, () => biasedArb); | ||
const memoArb = new MemoArbitrary(arb); | ||
|
||
const biasedLazy = memoArb.withBias(2); | ||
expect(biasedLazy).toBe(biasedArb); | ||
expect(noCallMock).not.toHaveBeenCalled(); | ||
}); | ||
it('Should cache biased arbitrary for same freq', () => { | ||
const biasArb48 = buildArbitrary(jest.fn()); | ||
const biasMock = jest.fn(); | ||
biasMock.mockImplementationOnce(() => biasArb48); | ||
const arb = buildArbitrary(jest.fn(), biasMock); | ||
const memoArb = new MemoArbitrary(arb); | ||
|
||
memoArb.withBias(48); | ||
biasMock.mockClear(); | ||
|
||
expect(memoArb.withBias(48)).toBe(biasArb48); | ||
expect(biasMock).not.toBeCalled(); | ||
}); | ||
it('Should not take from cache if freq changed', () => { | ||
const biasArb48 = buildArbitrary(jest.fn()); | ||
const biasArb69 = buildArbitrary(jest.fn()); | ||
const biasMock = jest.fn(); | ||
biasMock.mockImplementationOnce(() => biasArb48).mockImplementationOnce(() => biasArb69); | ||
const arb = buildArbitrary(jest.fn(), biasMock); | ||
const memoArb = new MemoArbitrary(arb); | ||
|
||
memoArb.withBias(48); | ||
biasMock.mockClear(); | ||
|
||
expect(memoArb.withBias(69)).toBe(biasArb69); | ||
expect(biasMock).toBeCalled(); | ||
}); | ||
}); | ||
}); | ||
|
||
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); | ||
})(); | ||
}; |