Skip to content

Commit

Permalink
randomType can accepts any function able to build a RandomGenerator
Browse files Browse the repository at this point in the history
Fixes: #128
  • Loading branch information
dubzzz committed Sep 30, 2018
1 parent bd8fd65 commit 010a860
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 55 deletions.
19 changes: 2 additions & 17 deletions src/check/runner/Tosser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,22 @@ import * as prand from 'pure-rand';
import { Random } from '../../random/generator/Random';
import { Shrinkable } from '../arbitrary/definition/Shrinkable';
import { IProperty } from '../property/IProperty';
import { RandomType } from './configuration/RandomType';

/** @hidden */
function lazyGenerate<Ts>(generator: IProperty<Ts>, rng: prand.RandomGenerator, idx: number): () => Shrinkable<Ts> {
return () => generator.generate(new Random(rng), idx);
}

/** @hidden */
function buildRngFor(seed: number, random: RandomType): prand.RandomGenerator {
switch (random) {
case 'mersenne':
return prand.mersenne(seed);
case 'congruential':
return prand.congruential(seed);
case 'congruential32':
return prand.congruential32(seed);
default:
throw new Error(`Invalid random specified: '${random}'`);
}
}

/** @hidden */
export function* toss<Ts>(
generator: IProperty<Ts>,
seed: number,
random: RandomType,
random: (seed: number) => prand.RandomGenerator,
examples: Ts[]
): IterableIterator<() => Shrinkable<Ts>> {
yield* examples.map(e => () => new Shrinkable(e));
let idx = 0;
let rng = buildRngFor(seed, random);
let rng = random(seed);
for (;;) {
rng = prand.skipN(rng, 42);
yield lazyGenerate(generator, rng, idx++);
Expand Down
4 changes: 3 additions & 1 deletion src/check/runner/configuration/Parameters.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RandomGenerator } from 'pure-rand';
import { RandomType } from './RandomType';

/**
Expand All @@ -15,8 +16,9 @@ export interface Parameters<T = void> {
*
* Random generator is the core element behind the generation of random values - changing it might directly impact the quality and performances of the generation of random values.
* It can be one of: 'mersenne', 'congruential', 'congruential32'
* Or any function able to build a `RandomGenerator` based on a seed
*/
randomType?: RandomType;
randomType?: RandomType | ((seed: number) => RandomGenerator);
/**
* Number of runs before success: 100 by default
*/
Expand Down
22 changes: 18 additions & 4 deletions src/check/runner/configuration/QualifiedParameters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import prand, { RandomGenerator } from 'pure-rand';
import { Parameters } from './Parameters';
import { RandomType } from './RandomType';

/**
* @hidden
Expand All @@ -10,7 +10,7 @@ import { RandomType } from './RandomType';
*/
export class QualifiedParameters<T> {
seed: number;
randomType: RandomType;
randomType: (seed: number) => RandomGenerator;
numRuns: number;
maxSkipsPerRun: number;
timeout: number | null;
Expand All @@ -21,8 +21,22 @@ export class QualifiedParameters<T> {
examples: T[];

private static readSeed = <T>(p?: Parameters<T>): number => (p != null && p.seed != null ? p.seed : Date.now());
private static readRandomType = <T>(p?: Parameters<T>): RandomType =>
p != null && p.randomType != null ? p.randomType : 'mersenne';
private static readRandomType = <T>(p?: Parameters<T>): ((seed: number) => RandomGenerator) => {
if (p == null || p.randomType == null) return prand.mersenne;
if (typeof p.randomType === 'string') {
switch (p.randomType) {
case 'mersenne':
return prand.mersenne;
case 'congruential':
return prand.congruential;
case 'congruential32':
return prand.congruential32;
default:
throw new Error(`Invalid random specified: '${p.randomType}'`);
}
}
return p.randomType;
};
private static readNumRuns = <T>(p?: Parameters<T>): number => {
const defaultValue = 100;
if (p == null) return defaultValue;
Expand Down
50 changes: 18 additions & 32 deletions test/unit/check/runner/Tosser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { stream } from '../../../../src/stream/Stream';
import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
import { IProperty } from '../../../../src/check/property/IProperty';
import { Random } from '../../../../src/random/generator/Random';
import { RandomType } from '../../../../src/check/runner/configuration/RandomType';

import * as stubArb from '../../stubs/arbitraries';
import prand from 'pure-rand';

const wrap = <T>(arb: Arbitrary<T>): IProperty<T> =>
new class implements IProperty<T> {
Expand All @@ -18,14 +18,12 @@ const wrap = <T>(arb: Arbitrary<T>): IProperty<T> =>
run = () => '';
}(arb);

const randomTypeArb = fc.constantFrom('mersenne', 'congruential', 'congruential32') as fc.Arbitrary<RandomType>;

describe('Tosser', () => {
describe('toss', () => {
it('Should offset the random number generator between calls', () =>
fc.assert(
fc.property(fc.integer(), randomTypeArb, fc.nat(100), (seed, randomType, start) => {
const s = stream(toss(wrap(stubArb.forwardArray(4)), seed, randomType, []));
fc.property(fc.integer(), fc.nat(100), (seed, start) => {
const s = stream(toss(wrap(stubArb.forwardArray(4)), seed, prand.mersenne, []));
const [g1, g2] = [
...s
.drop(start)
Expand All @@ -38,15 +36,15 @@ describe('Tosser', () => {
));
it('Should produce the same sequence for the same seed', () =>
fc.assert(
fc.property(fc.integer(), randomTypeArb, fc.nat(20), (seed, randomType, num) => {
fc.property(fc.integer(), fc.nat(20), (seed, num) => {
assert.deepStrictEqual(
[
...stream(toss(wrap(stubArb.forward()), seed, randomType, []))
...stream(toss(wrap(stubArb.forward()), seed, prand.mersenne, []))
.take(num)
.map(f => f().value)
],
[
...stream(toss(wrap(stubArb.forward()), seed, randomType, []))
...stream(toss(wrap(stubArb.forward()), seed, prand.mersenne, []))
.take(num)
.map(f => f().value)
]
Expand All @@ -55,9 +53,9 @@ describe('Tosser', () => {
));
it('Should not depend on the order of iteration', () =>
fc.assert(
fc.property(fc.integer(), randomTypeArb, fc.nat(20), (seed, randomType, num) => {
const onGoingItems1 = [...stream(toss(wrap(stubArb.forward()), seed, randomType, [])).take(num)];
const onGoingItems2 = [...stream(toss(wrap(stubArb.forward()), seed, randomType, [])).take(num)];
fc.property(fc.integer(), fc.nat(20), (seed, num) => {
const onGoingItems1 = [...stream(toss(wrap(stubArb.forward()), seed, prand.mersenne, [])).take(num)];
const onGoingItems2 = [...stream(toss(wrap(stubArb.forward()), seed, prand.mersenne, [])).take(num)];
assert.deepStrictEqual(
onGoingItems2
.reverse()
Expand All @@ -69,27 +67,15 @@ describe('Tosser', () => {
));
it('Should offset toss with the provided examples', () =>
fc.assert(
fc.property(
fc.integer(),
randomTypeArb,
fc.nat(20),
fc.array(fc.integer()),
(seed, randomType, num, examples) => {
const noExamplesProvided = [
...stream(toss(wrap(stubArb.forward()), seed, randomType, [])).take(num - examples.length)
].map(f => f().value);
const examplesProvided = [
...stream(toss(wrap(stubArb.forward()), seed, randomType, examples)).take(num)
].map(f => f().value);
assert.deepStrictEqual([...examples, ...noExamplesProvided].slice(0, num), examplesProvided);
}
)
fc.property(fc.integer(), fc.nat(20), fc.array(fc.integer()), (seed, num, examples) => {
const noExamplesProvided = [
...stream(toss(wrap(stubArb.forward()), seed, prand.mersenne, [])).take(num - examples.length)
].map(f => f().value);
const examplesProvided = [
...stream(toss(wrap(stubArb.forward()), seed, prand.mersenne, examples)).take(num)
].map(f => f().value);
assert.deepStrictEqual([...examples, ...noExamplesProvided].slice(0, num), examplesProvided);
})
));
it('Should throw on invalid random', () => {
assert.throws(() => {
const data = toss(wrap(stubArb.forward()), 0, 'invalid' as any, []);
[...stream(data).take(1)];
});
});
});
});
18 changes: 17 additions & 1 deletion test/unit/check/runner/configuration/QualifiedParameters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as assert from 'assert';
import * as fc from '../../../../../lib/fast-check';
import * as prand from 'pure-rand';

import { QualifiedParameters } from '../../../../../src/check/runner/configuration/QualifiedParameters';
import { RandomType } from '../../../../../src/check/runner/configuration/RandomType';
Expand All @@ -16,7 +17,7 @@ const extractExceptSeed = <T>(conf: QualifiedParameters<T>) => {
const parametersArbitrary = fc.record(
{
seed: fc.integer(),
randomType: fc.constantFrom('mersenne', 'congruential', 'congruential32') as fc.Arbitrary<RandomType>,
randomType: fc.constantFrom(prand.mersenne, prand.congruential, prand.congruential32),
numRuns: fc.nat(),
timeout: fc.nat(),
path: fc.array(fc.nat()).map(arr => arr.join(':')),
Expand All @@ -27,6 +28,8 @@ const parametersArbitrary = fc.record(
{ withDeletedKeys: true }
);

const hardCodedRandomType = fc.constantFrom('mersenne', 'congruential', 'congruential32') as fc.Arbitrary<RandomType>;

describe('QualifiedParameters', () => {
describe('read', () => {
it('Should forward as-is values already set in Parameters', () =>
Expand All @@ -44,6 +47,19 @@ describe('QualifiedParameters', () => {
}
})
));
it('Should transform correctly hardcoded randomType', () =>
fc.assert(
fc.property(parametersArbitrary, hardCodedRandomType, (params, randomType) => {
const qparams = QualifiedParameters.read({ ...params, randomType });
return qparams.randomType === prand[randomType];
})
));
it('Should throw on invalid randomType', () =>
fc.assert(
fc.property(parametersArbitrary, params => {
assert.throws(() => QualifiedParameters.read({ ...params, randomType: 'invalid' as RandomType }));
})
));
});
describe('readOrNumRuns', () => {
it('Should be equivalent to read with only numRuns when specifying a number', () =>
Expand Down

0 comments on commit 010a860

Please sign in to comment.