From 7b842cfb17387e19d6912edafbe08b7d0af08f2d Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Tue, 30 Jan 2018 03:11:38 +0100 Subject: [PATCH] Add support for async/await and Promises --- src/check/property/AsyncProperty.ts | 207 ++++++++++++++++++++++++++ src/check/property/IProperty.ts | 3 +- src/check/property/Property.ts | 2 +- src/check/runner/Runner.ts | 45 +++++- src/fast-check.ts | 3 +- test/unit/check/runner/Runner.spec.ts | 17 ++- 6 files changed, 266 insertions(+), 11 deletions(-) create mode 100644 src/check/property/AsyncProperty.ts diff --git a/src/check/property/AsyncProperty.ts b/src/check/property/AsyncProperty.ts new file mode 100644 index 00000000000..eb5acb702a5 --- /dev/null +++ b/src/check/property/AsyncProperty.ts @@ -0,0 +1,207 @@ +import Arbitrary from '../arbitrary/definition/Arbitrary' +import Shrinkable from '../arbitrary/definition/Shrinkable' +import { tuple } from '../arbitrary/TupleArbitrary' +import MutableRandomGenerator from '../../random/generator/MutableRandomGenerator' +import IProperty from './IProperty' + +export class AsyncProperty implements IProperty { + constructor(readonly arb: Arbitrary, readonly predicate: (t: Ts) => Promise) { } + isAsync = () => true; + generate(mrng: MutableRandomGenerator): Shrinkable { + return this.arb.generate(mrng); + } + async run(v: Ts): Promise { + try { + const output = await this.predicate(v); + return output == null || output == true ? null : "Property failed by returning false"; + } + catch (err) { + if (err instanceof Error && err.stack) + return `${err}\n\nStack trace: ${err.stack}` + return `${err}`; + } + } +} + +function asyncProperty( + arb1: Arbitrary, + predicate: (t1:T1) => Promise +): AsyncProperty<[T1]>; + +function asyncProperty( + arb1: Arbitrary, + arb2: Arbitrary, + predicate: (t1:T1,t2:T2) => Promise +): AsyncProperty<[T1,T2]>; + +function asyncProperty( + arb1: Arbitrary, + arb2: Arbitrary, + arb3: Arbitrary, + predicate: (t1:T1,t2:T2,t3:T3) => Promise +): AsyncProperty<[T1,T2,T3]>; + +function asyncProperty( + arb1: Arbitrary, + arb2: Arbitrary, + arb3: Arbitrary, + arb4: Arbitrary, + predicate: (t1:T1,t2:T2,t3:T3,t4:T4) => Promise +): AsyncProperty<[T1,T2,T3,T4]>; + +function asyncProperty( + arb1: Arbitrary, + arb2: Arbitrary, + arb3: Arbitrary, + arb4: Arbitrary, + arb5: Arbitrary, + predicate: (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5) => Promise +): AsyncProperty<[T1,T2,T3,T4,T5]>; + +function asyncProperty( + arb1: Arbitrary, + arb2: Arbitrary, + arb3: Arbitrary, + arb4: Arbitrary, + arb5: Arbitrary, + arb6: Arbitrary, + predicate: (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6) => Promise +): AsyncProperty<[T1,T2,T3,T4,T5,T6]>; + +function asyncProperty( + arb1: Arbitrary, + arb2: Arbitrary, + arb3: Arbitrary, + arb4: Arbitrary, + arb5: Arbitrary, + arb6: Arbitrary, + arb7: Arbitrary, + predicate: (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6,t7:T7) => Promise +): AsyncProperty<[T1,T2,T3,T4,T5,T6,T7]>; + +function asyncProperty( + arb1: Arbitrary, + arb2: Arbitrary, + arb3: Arbitrary, + arb4: Arbitrary, + arb5: Arbitrary, + arb6: Arbitrary, + arb7: Arbitrary, + arb8: Arbitrary, + predicate: (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6,t7:T7,t8:T8) => Promise +): AsyncProperty<[T1,T2,T3,T4,T5,T6,T7,T8]>; + +function asyncProperty( + arb1: Arbitrary, + arb2: Arbitrary, + arb3: Arbitrary, + arb4: Arbitrary, + arb5: Arbitrary, + arb6: Arbitrary, + arb7: Arbitrary, + arb8: Arbitrary, + arb9: Arbitrary, + predicate: (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6,t7:T7,t8:T8,t9:T9) => Promise +): AsyncProperty<[T1,T2,T3,T4,T5,T6,T7,T8,T9]>; + +function asyncProperty( + arb1: Arbitrary, + arb2: Arbitrary | ((t1:T1) => Promise), + arb3?: Arbitrary | ((t1:T1,t2:T2) => Promise), + arb4?: Arbitrary | ((t1:T1,t2:T2,t3:T3) => Promise), + arb5?: Arbitrary | ((t1:T1,t2:T2,t3:T3,t4:T4) => Promise), + arb6?: Arbitrary | ((t1:T1,t2:T2,t3:T3,t4:T4,t5:T5) => Promise), + arb7?: Arbitrary | ((t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6) => Promise), + arb8?: Arbitrary | ((t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6,t7:T7) => Promise), + arb9?: Arbitrary | ((t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6,t7:T7,t8:T8) => Promise), + predicate?: (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6,t7:T7,t8:T8,t9:T9) => Promise +) { + if (predicate) { + const p = predicate as (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6,t7:T7,t8:T8,t9:T9) => Promise; + return new AsyncProperty(tuple( + arb1 as Arbitrary, + arb2 as Arbitrary, + arb3 as Arbitrary, + arb4 as Arbitrary, + arb5 as Arbitrary, + arb6 as Arbitrary, + arb7 as Arbitrary, + arb8 as Arbitrary, + arb9 as Arbitrary + ), t => p(t[0],t[1],t[2],t[3],t[4],t[5],t[6],t[7],t[8])); + } + if (arb9) { + const p = arb9 as (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6,t7:T7,t8:T8) => Promise; + return new AsyncProperty(tuple( + arb1 as Arbitrary, + arb2 as Arbitrary, + arb3 as Arbitrary, + arb4 as Arbitrary, + arb5 as Arbitrary, + arb6 as Arbitrary, + arb7 as Arbitrary, + arb8 as Arbitrary + ), t => p(t[0],t[1],t[2],t[3],t[4],t[5],t[6],t[7])); + } + if (arb8) { + const p = arb8 as (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6,t7:T7) => Promise; + return new AsyncProperty(tuple( + arb1 as Arbitrary, + arb2 as Arbitrary, + arb3 as Arbitrary, + arb4 as Arbitrary, + arb5 as Arbitrary, + arb6 as Arbitrary, + arb7 as Arbitrary + ), t => p(t[0],t[1],t[2],t[3],t[4],t[5],t[6])); + } + if (arb7) { + const p = arb7 as (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5,t6:T6) => Promise; + return new AsyncProperty(tuple( + arb1 as Arbitrary, + arb2 as Arbitrary, + arb3 as Arbitrary, + arb4 as Arbitrary, + arb5 as Arbitrary, + arb6 as Arbitrary + ), t => p(t[0],t[1],t[2],t[3],t[4],t[5])); + } + if (arb6) { + const p = arb6 as (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5) => Promise; + return new AsyncProperty(tuple( + arb1 as Arbitrary, + arb2 as Arbitrary, + arb3 as Arbitrary, + arb4 as Arbitrary, + arb5 as Arbitrary + ), t => p(t[0],t[1],t[2],t[3],t[4])); + } + if (arb5) { + const p = arb5 as (t1:T1,t2:T2,t3:T3,t4:T4) => Promise; + return new AsyncProperty(tuple( + arb1 as Arbitrary, + arb2 as Arbitrary, + arb3 as Arbitrary, + arb4 as Arbitrary + ), t => p(t[0],t[1],t[2],t[3])); + } + if (arb4) { + const p = arb4 as (t1:T1,t2:T2,t3:T3) => Promise; + return new AsyncProperty(tuple( + arb1 as Arbitrary, + arb2 as Arbitrary, + arb3 as Arbitrary + ), t => p(t[0],t[1],t[2])); + } + if (arb3) { + const p = arb3 as (t1:T1,t2:T2) => Promise; + return new AsyncProperty(tuple( + arb1 as Arbitrary, + arb2 as Arbitrary + ), t => p(t[0],t[1])); + } + const p = arb2 as (t1:T1) => Promise; + return new AsyncProperty(tuple(arb1), t => p(t[0])); +} + +export { asyncProperty }; diff --git a/src/check/property/IProperty.ts b/src/check/property/IProperty.ts index 4855dd08c4d..e2120241375 100644 --- a/src/check/property/IProperty.ts +++ b/src/check/property/IProperty.ts @@ -3,6 +3,7 @@ import MutableRandomGenerator from '../../random/generator/MutableRandomGenerato import Stream from '../../stream/Stream' export default interface IProperty { + isAsync(): boolean; generate(mrng: MutableRandomGenerator): Shrinkable; - run(v: Ts): (string|null); + run(v: Ts): Promise | (string|null); } diff --git a/src/check/property/Property.ts b/src/check/property/Property.ts index c30ca2a848e..48d9e2b98b4 100644 --- a/src/check/property/Property.ts +++ b/src/check/property/Property.ts @@ -2,11 +2,11 @@ import Arbitrary from '../arbitrary/definition/Arbitrary' import Shrinkable from '../arbitrary/definition/Shrinkable' import { tuple } from '../arbitrary/TupleArbitrary' import MutableRandomGenerator from '../../random/generator/MutableRandomGenerator' -import Stream from '../../stream/Stream' import IProperty from './IProperty' export class Property implements IProperty { constructor(readonly arb: Arbitrary, readonly predicate: (t: Ts) => (boolean | void)) { } + isAsync = () => false; generate(mrng: MutableRandomGenerator): Shrinkable { return this.arb.generate(mrng); } diff --git a/src/check/runner/Runner.ts b/src/check/runner/Runner.ts index 93db0a26184..bf3d64ce193 100644 --- a/src/check/runner/Runner.ts +++ b/src/check/runner/Runner.ts @@ -1,23 +1,33 @@ import Shrinkable from '../arbitrary/definition/Shrinkable' import { RandomGenerator, skip_n } from '../../random/generator/RandomGenerator' import IProperty from '../property/IProperty' +import { Property } from '../property/Property' +import { AsyncProperty } from '../property/AsyncProperty' import toss from './Tosser' import { Parameters, QualifiedParameters, RunDetails, successFor, failureFor, throwIfFailed } from './utils/utils' function shrinkIt(property: IProperty, value: Shrinkable, error: string, num_shrinks: number = 0): [Ts, number, string] { for (const v of value.shrink()) { - const out = property.run(v.value); + const out = property.run(v.value) as (string|null); if (out != null) { return shrinkIt(property, v, out, num_shrinks+1); } } return [value.value, num_shrinks, error]; } +async function asyncShrinkIt(property: IProperty, value: Shrinkable, error: string, num_shrinks: number = 0): Promise<[Ts, number, string]> { + for (const v of value.shrink()) { + const out = await property.run(v.value); + if (out != null) { + return await asyncShrinkIt(property, v, out, num_shrinks+1); + } + } + return [value.value, num_shrinks, error]; +} -function check(property: IProperty, params?: Parameters): RunDetails { +function internalCheck(property: IProperty, params?: Parameters): RunDetails { const qParams = QualifiedParameters.read(params); const generator = toss(property, qParams.seed); - for (let idx = 0 ; idx < qParams.num_runs ; ++idx) { const g = generator.next().value; const out = property.run(g.value); @@ -28,10 +38,37 @@ function check(property: IProperty, params?: Parameters): RunDetails } return successFor(qParams); } +async function asyncInternalCheck(property: IProperty, params?: Parameters): Promise> { + const qParams = QualifiedParameters.read(params); + const generator = toss(property, qParams.seed); + for (let idx = 0 ; idx < qParams.num_runs ; ++idx) { + const g = generator.next().value; + const out = await property.run(g.value); + if (out != null) { + const [shrinkedValue, numShrinks, error] = await shrinkIt(property, g, out as string); + return failureFor(qParams, idx+1, numShrinks, shrinkedValue, error); + } + } + return successFor(qParams); +} + +function check(property: AsyncProperty, params?: Parameters) : Promise>; +function check(property: Property, params?: Parameters) : RunDetails; +function check(property: IProperty, params?: Parameters) : Promise> | RunDetails; +function check(property: IProperty, params?: Parameters) { + return property.isAsync() + ? asyncInternalCheck(property, params) + : internalCheck(property, params); +} +function assert(property: AsyncProperty, params?: Parameters) : Promise; +function assert(property: Property, params?: Parameters) : void; +function assert(property: IProperty, params?: Parameters) : Promise | void; function assert(property: IProperty, params?: Parameters) { const out = check(property, params); - throwIfFailed(out); + return property.isAsync() + ? (out as Promise>).then(throwIfFailed) + : throwIfFailed(out as RunDetails); } export { check, assert }; \ No newline at end of file diff --git a/src/fast-check.ts b/src/fast-check.ts index 3c58d3fbcd8..5689f767915 100644 --- a/src/fast-check.ts +++ b/src/fast-check.ts @@ -1,6 +1,7 @@ import { check, assert } from './check/runner/Runner' import { sample, statistics } from './check/runner/Sampler' import { Parameters, RunDetails } from './check/runner/utils/utils' +import { asyncProperty } from './check/property/AsyncProperty' import { property } from './check/property/Property' import Arbitrary from './check/arbitrary/definition/Arbitrary' @@ -34,7 +35,7 @@ export { // check the property check, assert, // property definition - property, + property, asyncProperty, // pre-built arbitraries boolean, // boolean float, double, // floating point types diff --git a/test/unit/check/runner/Runner.spec.ts b/test/unit/check/runner/Runner.spec.ts index 165561de792..e7ae770f9aa 100644 --- a/test/unit/check/runner/Runner.spec.ts +++ b/test/unit/check/runner/Runner.spec.ts @@ -5,6 +5,7 @@ import Shrinkable from '../../../../src/check/arbitrary/definition/Shrinkable'; import IProperty from '../../../../src/check/property/IProperty'; import { check, assert as rAssert } from '../../../../src/check/runner/Runner'; import MutableRandomGenerator from '../../../../src/random/generator/MutableRandomGenerator'; +import { RunDetails } from '../../../../src/check/runner/utils/utils'; const MAX_NUM_RUNS = 1000; describe('Runner', () => { @@ -13,6 +14,7 @@ describe('Runner', () => { let num_calls_generate = 0; let num_calls_run = 0; const p: IProperty<[number]> = { + isAsync: () => false, generate: () => { assert.equal(num_calls_run, num_calls_generate, 'Should have called run before calling back'); ++num_calls_generate; @@ -24,7 +26,7 @@ describe('Runner', () => { return null; } }; - const out = check(p); + const out = check(p) as RunDetails<[number]>;; 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'); @@ -33,6 +35,7 @@ describe('Runner', () => { let num_calls_generate = 0; let num_calls_run = 0; const p: IProperty<[number]> = { + isAsync: () => false, generate: () => { ++num_calls_generate; return new Shrinkable([0], () => { throw 'Not implemented'; }) as Shrinkable<[number]>; @@ -42,7 +45,7 @@ describe('Runner', () => { return null; }, }; - const out = check(p); + const out = check(p) as RunDetails<[number]>;; 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'); @@ -52,6 +55,7 @@ describe('Runner', () => { let num_calls_generate = 0; let num_calls_run = 0; const p: IProperty<[number]> = { + isAsync: () => false, generate: () => { ++num_calls_generate; return new Shrinkable([0]) as Shrinkable<[number]>; @@ -60,7 +64,7 @@ describe('Runner', () => { return ++num_calls_run < num ? null : "error"; } }; - const out = check(p, {seed: seed}); + const out = check(p, {seed: seed}) as RunDetails<[number]>;; 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'); @@ -74,6 +78,7 @@ describe('Runner', () => { let num_calls_generate = 0; let num_calls_run = 0; const p: IProperty<[number]> = { + isAsync: () => false, generate: () => { ++num_calls_generate; return new Shrinkable([0]) as Shrinkable<[number]>; @@ -83,7 +88,7 @@ describe('Runner', () => { return null; } }; - const out = check(p, {num_runs: num}); + const out = check(p, {num_runs: num}) as RunDetails<[number]>; 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'); @@ -94,6 +99,7 @@ describe('Runner', () => { fc.property(fc.integer(), (seed) => { const buildPropertyFor = function(runOn: number[]) { const p: IProperty<[number]> = { + isAsync: () => false, generate: (rng: MutableRandomGenerator) => { return new Shrinkable([rng.next()[0]]) as Shrinkable<[number]>; }, @@ -117,14 +123,17 @@ describe('Runner', () => { const v1 = { toString: () => "toString(value#1)" }; const v2 = { a: "Hello", b: 21 }; const failingProperty: IProperty<[any,any]> = { + isAsync: () => false, generate: () => new Shrinkable([v1,v2]) as Shrinkable<[any,any]>, run: (v: [any,any]) => "error in failingProperty" }; const failingComplexProperty: IProperty<[any,any,any]> = { + isAsync: () => false, 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]> = { + isAsync: () => false, generate: () => new Shrinkable([v1,v2]) as Shrinkable<[any,any]>, run: (v: [any,any]) => null };