Skip to content

Commit

Permalink
Merge ac338dd into e762996
Browse files Browse the repository at this point in the history
  • Loading branch information
dubzzz committed Jul 3, 2019
2 parents e762996 + ac338dd commit 4e81147
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 1 deletion.
16 changes: 15 additions & 1 deletion documentation/1-Guides/Arbitraries.md
Expand Up @@ -125,7 +125,7 @@ Default for `values` are: `fc.boolean()`, `fc.integer()`, `fc.double()`, `fc.str

## Recursive structures

- `fc.letrec(builder: (tie) => { [arbitraryName: string]: Arbitrary<T> })` produce arbitraries as specified by builder function. The `tie` function given to builder should be used as a placeholder to handle the recursion. It takes as input the name of the arbitrary to use in the recursion.
- `fc.letrec(builder: (tie) => { [arbitraryName: string]: Arbitrary<T> })` produce arbitraries as specified by builder function. The `tie` function given to builder should be used as a placeholder to handle the recursion. It takes as input the name of the arbitrary to use in the recursion

```typescript
const { tree } = fc.letrec(tie => ({
Expand All @@ -139,6 +139,20 @@ const { tree } = fc.letrec(tie => ({
tree() // Is a tree arbitrary (as fc.nat() is an integer arbitrary)
```

- `fc.memo<T>(builder: (n: number) => Arbitrary<T>): ((n?: number) => Arbitrary<T>)` produce arbitraries as specified by builder function. Contrary to `fc.letrec`, `fc.memo` can control the maximal depth of your recursive structure by relying on the `n` parameter given as input of the `builder` function

```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;
tree() // Is a tree arbitrary (as fc.nat() is an integer arbitrary)
// with maximal depth of 10 (equivalent to tree(10))
```

## Functions

- `compareBooleanFunc()` generate a comparison function taking two parameters `a` and `b` and producing a boolean value. `true` means that `a < b`, `false` that `a = b` or `a > b`
Expand Down
60 changes: 60 additions & 0 deletions src/check/arbitrary/MemoArbitrary.ts
@@ -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>;
};
3 changes: 3 additions & 0 deletions src/fast-check-default.ts
Expand Up @@ -26,6 +26,7 @@ import { ipV4, ipV6 } from './check/arbitrary/IpArbitrary';
import { letrec } from './check/arbitrary/LetRecArbitrary';
import { lorem } from './check/arbitrary/LoremArbitrary';
import { mapToConstant } from './check/arbitrary/MapToConstantArbitrary';
import { memo, Memo } from './check/arbitrary/MemoArbitrary';
import {
anything,
json,
Expand Down Expand Up @@ -148,6 +149,7 @@ export {
unicodeJson,
unicodeJsonObject,
letrec,
memo,
compareBooleanFunc,
compareFunc,
func,
Expand Down Expand Up @@ -179,6 +181,7 @@ export {
Context,
ExecutionStatus,
ExecutionTree,
Memo,
ObjectConstraints,
Parameters,
RecordConstraints,
Expand Down
72 changes: 72 additions & 0 deletions test/e2e/arbitraries/MemoArbitrary.spec.ts
@@ -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 });
});
});
});
145 changes: 145 additions & 0 deletions test/unit/check/arbitrary/MemoArbitrary.spec.ts
@@ -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);
})();
};

0 comments on commit 4e81147

Please sign in to comment.