Skip to content

Commit

Permalink
Rework IProperty definition
Browse files Browse the repository at this point in the history
  • Loading branch information
dubzzz committed Jan 22, 2018
1 parent 1c1a0d0 commit b884478
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 53 deletions.
4 changes: 2 additions & 2 deletions src/check/property/IProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import MutableRandomGenerator from '../../random/generator/MutableRandomGenerato
import Stream from '../../stream/Stream'

export default interface IProperty<Ts> {
run(mrng: MutableRandomGenerator): [(string|null), Shrinkable<Ts>];
runOne(v: Ts): (string|null);
generate(mrng: MutableRandomGenerator): Shrinkable<Ts>;
run(v: Ts): (string|null);
}
7 changes: 3 additions & 4 deletions src/check/property/Property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import IProperty from './IProperty'

export class Property<Ts> implements IProperty<Ts> {
constructor(readonly arb: Arbitrary<Ts>, readonly predicate: (t: Ts) => (boolean | void)) { }
run(mrng: MutableRandomGenerator): [(string|null), Shrinkable<Ts>] {
const value = this.arb.generate(mrng);
return [this.runOne(value.value), value];
generate(mrng: MutableRandomGenerator): Shrinkable<Ts> {
return this.arb.generate(mrng);
}
runOne(v: Ts): (string|null) {
run(v: Ts): (string|null) {
try {
const output = this.predicate(v);
return output == null || output == true ? null : "Property failed by returning false";
Expand Down
17 changes: 8 additions & 9 deletions src/check/property/Runner.ts → src/check/runner/Runner.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import Shrinkable from '../arbitrary/definition/Shrinkable'
import { RandomGenerator, skip_n } from '../../random/generator/RandomGenerator'
import MersenneTwister from '../../random/generator/MersenneTwister'
import MutableRandomGenerator from '../../random/generator/MutableRandomGenerator'
import IProperty from './IProperty'
import IProperty from '../property/IProperty'
import toss from './Tosser'

export interface Parameters {
seed?: number;
Expand All @@ -11,7 +10,7 @@ export interface Parameters {

function shrinkIt<Ts>(property: IProperty<Ts>, value: Shrinkable<Ts>, error: string, num_shrinks: number = 0): [Ts, number, string] {
for (const v of value.shrink()) {
const out = property.runOne(v.value);
const out = property.run(v.value);
if (out != null) {
return shrinkIt(property, v, out, num_shrinks+1);
}
Expand All @@ -22,13 +21,13 @@ function shrinkIt<Ts>(property: IProperty<Ts>, value: Shrinkable<Ts>, error: str
function check<Ts>(property: IProperty<Ts>, params?: Parameters) {
const seed = (params && params.seed != null) ? params.seed as number : Date.now();
const num_runs = (params && params.num_runs != null) ? params.num_runs as number : 100;
const generator = toss(property, seed);

let rng: RandomGenerator = MersenneTwister.from(seed);
for (let idx = 0 ; idx < num_runs ; ++idx) {
rng = skip_n(rng, 42);
const out = property.run(new MutableRandomGenerator(rng));
if (out[0] != null) {
const [shrinkedValue, numShrinks, error] = shrinkIt(property, out[1], out[0] as string);
const g = generator.next().value;
const out = property.run(g.value);
if (out != null) {
const [shrinkedValue, numShrinks, error] = shrinkIt(property, g, out as string);
return {failed: true, num_runs: idx+1, num_shrinks: numShrinks, seed: seed, counterexample: shrinkedValue, error: error};
}
}
Expand Down
17 changes: 17 additions & 0 deletions src/check/runner/Tosser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Arbitrary from '../arbitrary/definition/Arbitrary'
import Shrinkable from '../arbitrary/definition/Shrinkable'
import { RandomGenerator, generate_n, skip_n } from '../../random/generator/RandomGenerator'
import MersenneTwister from '../../random/generator/MersenneTwister'
import MutableRandomGenerator from '../../random/generator/MutableRandomGenerator'
import IProperty from '../property/IProperty'

export default function* toss<Ts>(generator: (IProperty<Ts> | Arbitrary<Ts>), seed?: number): IterableIterator<Shrinkable<Ts>> {
const s: number = seed != null ? seed as number : Date.now();
let rng: RandomGenerator = MersenneTwister.from(s);
for (;;) {
rng = skip_n(rng, 42);
yield generator.generate(new MutableRandomGenerator(rng));
}
}

export { toss };
2 changes: 1 addition & 1 deletion src/fast-check.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { check, assert } from './check/property/Runner'
import { check, assert } from './check/runner/Runner'
import { property } from './check/property/Property'

import Arbitrary from './check/arbitrary/definition/Arbitrary'
Expand Down
14 changes: 7 additions & 7 deletions test/check/property/Property.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ describe('Property', () => {
const p = property(single(8), (arg: number) => {
return false;
});
assert.notEqual(p.run(generator())[0], null, 'Property should fail');
assert.notEqual(p.run(p.generate(generator()).value), null, 'Property should fail');
});
it('Should fail if predicate throws', () => {
const p = property(single(8), (arg: number) => {
throw 'predicate throws';
});
assert.equal(p.run(generator())[0], 'predicate throws', 'Property should fail and attach the exception as string');
assert.equal(p.run(p.generate(generator()).value), 'predicate throws', 'Property should fail and attach the exception as string');
});
it('Should fail if predicate fails on asserts', () => {
const p = property(single(8), (arg: number) => {
Expand All @@ -57,19 +57,19 @@ describe('Property', () => {
let expected = "";
try { assert.ok(false); } catch (err) { expected = `${err}`; }

const out = p.run(generator())[0]
const out = p.run(p.generate(generator()).value);
assert.ok(out!.startsWith(expected), 'Property should fail and attach the exception as string');
assert.ok(out!.indexOf('\n\nStack trace:') !== -1, 'Property should include the stack trace when available');
});
it('Should succeed if predicate is true', () => {
const p = property(single(8), (arg: number) => {
return true;
});
assert.equal(p.run(generator())[0], null, 'Property should succeed');
assert.equal(p.run(p.generate(generator()).value), null, 'Property should succeed');
});
it('Should succeeds if predicate does not return anything', () => {
it('Should succeed if predicate does not return anything', () => {
const p = property(single(8), (arg: number) => {});
assert.equal(p.run(generator())[0], null, 'Property should succeed');
assert.equal(p.run(p.generate(generator()).value), null, 'Property should succeed');
});
it('Should call and forward arbitraries one time', () => {
let one_call_to_predicate = false;
Expand All @@ -85,7 +85,7 @@ describe('Property', () => {
for (let idx = 0 ; idx !== arbs.length ; ++idx) {
assert.equal(arbs[idx].called_once, false, `The creation of a property should not trigger call to generator #${idx+1}`);
}
assert.equal(p.run(generator())[0], null, 'Predicate should receive the right arguments');
assert.equal(p.run(p.generate(generator()).value), null, 'Predicate should receive the right arguments');
assert.ok(one_call_to_predicate, 'Predicate should have been called by run');
for (let idx = 0 ; idx !== arbs.length ; ++idx) {
assert.ok(arbs[idx].called_once, `Generator #${idx+1} should have been called by run`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,66 @@
import * as assert from 'power-assert';
import IProperty from '../../../src/check/property/IProperty';
import { check, assert as rAssert } from '../../../src/check/property/Runner';
import { check, assert as rAssert } from '../../../src/check/runner/Runner';
import Shrinkable from '../../../src/check/arbitrary/definition/Shrinkable';
import * as fc from '../../../src/fast-check';

const MAX_NUM_RUNS = 1000;
describe('Runner', () => {
describe('check', () => {
it('Should call the property 100 times by default (on success)', () => {
let num_calls = 0;
let num_calls_generate = 0;
let num_calls_run = 0;
const p: IProperty<[number]> = {
run: () => {
++num_calls;
return [null, new Shrinkable([0]) as Shrinkable<[number]>];
generate: () => {
assert.equal(num_calls_run, num_calls_generate, 'Should have called run before calling back');
++num_calls_generate;
return new Shrinkable([num_calls_generate]) as Shrinkable<[number]>;
},
runOne: () => { throw 'Not implemented'; }
run: (value: [number]) => {
assert.equal(value[0], num_calls_generate, 'Should be called with previously generated value');
++num_calls_run;
return null;
}
};
const out = check(p);
assert.equal(num_calls, 100, 'Should have been called 100 times');
assert.equal(num_calls_generate, 100, 'Should have called generate 100 times');
assert.equal(num_calls_run, 100, 'Should have called run 100 times');
assert.equal(out.failed, false, 'Should not have failed');
});
it('Should never call shrink on success', () => {
let num_calls = 0;
let num_calls_generate = 0;
let num_calls_run = 0;
const p: IProperty<[number]> = {
run: () => {
++num_calls;
return [null, new Shrinkable([0], () => { throw 'Not implemented'; }) as Shrinkable<[number]>];
generate: () => {
++num_calls_generate;
return new Shrinkable([0], () => { throw 'Not implemented'; }) as Shrinkable<[number]>;
},
run: (value: [number]) => {
++num_calls_run;
return null;
},
runOne: () => { throw 'Not implemented'; },
};
const out = check(p);
assert.equal(num_calls, 100, 'Should have been called 100 times');
assert.equal(num_calls_generate, 100, 'Should have called generate 100 times');
assert.equal(num_calls_run, 100, 'Should have called run 100 times');
assert.equal(out.failed, false, 'Should not have failed');
});
it('Should call the property 100 times by default (except on error)', () => fc.assert(
fc.property(fc.integer(1, 100), fc.integer(), (num, seed) => {
let num_calls = 0;
let num_calls_generate = 0;
let num_calls_run = 0;
const p: IProperty<[number]> = {
run: () => {
return [++num_calls < num ? null : "error", new Shrinkable([0]) as Shrinkable<[number]>];
generate: () => {
++num_calls_generate;
return new Shrinkable([0]) as Shrinkable<[number]>;
},
runOne: () => { throw 'Not implemented'; }
run: (value: [number]) => {
return ++num_calls_run < num ? null : "error";
}
};
const out = check(p, {seed: seed});
assert.equal(num_calls, num, `Should have stopped at first failing run (run number ${num})`);
assert.equal(num_calls_generate, num, `Should have stopped generate at first failing run (run number ${num})`);
assert.equal(num_calls_run, num, `Should have stopped run (because no shrink) at first failing run (run number ${num})`);
assert.ok(out.failed, 'Should have failed');
assert.equal(out.num_runs, num, `Should have failed after ${num} tests`);
assert.equal(out.seed, seed, `Should attach the failing seed`);
Expand All @@ -52,16 +69,21 @@ describe('Runner', () => {
));
it('Should alter the number of runs when asked to', () => fc.assert(
fc.property(fc.nat(MAX_NUM_RUNS), (num) => {
let num_calls = 0;
let num_calls_generate = 0;
let num_calls_run = 0;
const p: IProperty<[number]> = {
run: () => {
++num_calls;
return [null, new Shrinkable([0]) as Shrinkable<[number]>];
generate: () => {
++num_calls_generate;
return new Shrinkable([0]) as Shrinkable<[number]>;
},
runOne: () => { throw 'Not implemented'; }
run: (value: [number]) => {
++num_calls_run;
return null;
}
};
const out = check(p, {num_runs: num});
assert.equal(num_calls, num, `Should have been called ${num} times`);
assert.equal(num_calls_generate, num, `Should have called generate ${num} times`);
assert.equal(num_calls_run, num, `Should have called run ${num} times`);
assert.equal(out.failed, false, 'Should not have failed');
return true;
})
Expand All @@ -71,16 +93,16 @@ describe('Runner', () => {
const v1 = { toString: () => "toString(value#1)" };
const v2 = { a: "Hello", b: 21 };
const failingProperty: IProperty<[any,any]> = {
run: () => ["error in failingProperty", new Shrinkable([v1,v2]) as Shrinkable<[any,any]>],
runOne: () => { throw 'Not implemented'; }
generate: () => new Shrinkable([v1,v2]) as Shrinkable<[any,any]>,
run: (v: [any,any]) => "error in failingProperty"
};
const failingComplexProperty: IProperty<[any,any,any]> = {
run: () => ["error in failingComplexProperty", new Shrinkable([[v1,v2],v2,v1]) as Shrinkable<[any,any,any]>],
runOne: () => { throw 'Not implemented'; }
generate: () => new Shrinkable([[v1,v2],v2,v1]) as Shrinkable<[any,any,any]>,
run: (v: [any,any,any]) => "error in failingComplexProperty"
};
const successProperty: IProperty<[any,any]> = {
run: () => [null, new Shrinkable([v1,v2]) as Shrinkable<[any,any]>],
runOne: () => { throw 'Not implemented'; }
generate: () => new Shrinkable([v1,v2]) as Shrinkable<[any,any]>,
run: (v: [any,any]) => null
};

it('Should never throw if no failure occured', () => {
Expand Down

0 comments on commit b884478

Please sign in to comment.