forward runner parameters, custom error reporting
This commit is contained in:
parent
141999b881
commit
e2bd5b1d26
49
src/index.ts
49
src/index.ts
|
@ -1,19 +1,36 @@
|
|||
import { Arbitrary, check, property, RunDetails, Parameters } from 'fast-check';
|
||||
import { Arbitrary, check, Parameters, property, RunDetails } from 'fast-check';
|
||||
|
||||
export type Check<T> = (this: Mocha.Context, val: T) => boolean;
|
||||
export type Check<T> = (this: Mocha.Context, val: T) => boolean | never | void;
|
||||
export type WrappedIt<T> = (name: string, check: Check<T>) => void;
|
||||
export type Suite<T> = (it: WrappedIt<T>) => void;
|
||||
|
||||
export function over<T>(name: string, strategy: Arbitrary<T>, suite: Suite<T>, parameters?: Parameters<[T]>): void {
|
||||
export type ErrorReporter<T> = (details: RunDetails<T>) => string | undefined;
|
||||
export interface ErrorParameters<T> extends Parameters<T> {
|
||||
errorReporter?: ErrorReporter<T>;
|
||||
}
|
||||
|
||||
export function over<T>(name: string, strategy: Arbitrary<T>, suite: Suite<T>, parameters?: ErrorParameters<T>): void {
|
||||
describe(name, () => {
|
||||
suite((name, test) => {
|
||||
it(name, function (this: Mocha.Context): Promise<void> {
|
||||
const ctx = this;
|
||||
// something about check's type signature requires examples to be tuples,
|
||||
// which leads to triple-wrapping examples for tuple properties. help remove one layer
|
||||
const examples: Array<[T]> = parameters?.examples?.map((it) => [it]) || [];
|
||||
const checkParameters: Parameters<[T]> = {
|
||||
...parameters,
|
||||
// handle result formatting here
|
||||
asyncReporter: undefined,
|
||||
reporter: undefined,
|
||||
examples,
|
||||
};
|
||||
const reporter = (parameters?.errorReporter || briefReporter) as ErrorReporter<[T]>;
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
const result = check(property(strategy, (val) => test.call(ctx, val)), parameters);
|
||||
// wrap the strategy arb in a one-shot property checking the test fn
|
||||
const result = check(property(strategy, (val) => test.call(ctx, val)), checkParameters);
|
||||
if (result.failed) {
|
||||
rej(new Error(formatDetails(result)));
|
||||
rej(new Error(reporter(result)));
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
|
@ -23,10 +40,19 @@ export function over<T>(name: string, strategy: Arbitrary<T>, suite: Suite<T>, p
|
|||
});
|
||||
}
|
||||
|
||||
export function formatDetails<T>(details: RunDetails<[T]>): string {
|
||||
export function briefReporter<T>(details: RunDetails<[T]>): string {
|
||||
const prefix = formatPrefix(details);
|
||||
const counts = `${prefix} after ${details.numRuns} runs and ${details.numShrinks} shrinks`;
|
||||
const examples = formatExamples(details);
|
||||
|
||||
if (isErrorRun(details)) {
|
||||
return `${counts}, ${examples}\n${details.error}`;
|
||||
} else {
|
||||
return `${counts}, ${examples}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatExamples<T>(details: RunDetails<[T]>): string {
|
||||
if (details.counterexample !== null) {
|
||||
const examples = details.counterexample.map((val) => {
|
||||
if (isString(val)) {
|
||||
|
@ -35,15 +61,16 @@ export function formatDetails<T>(details: RunDetails<[T]>): string {
|
|||
return val;
|
||||
}
|
||||
}).join(',');
|
||||
return `${counts}, failing on: ${examples}`;
|
||||
return `failing on: ${examples}`;
|
||||
} else {
|
||||
return `${counts}, without counterexamples`;
|
||||
return `without counterexamples`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function formatPrefix<T>(details: RunDetails<[T]>): string {
|
||||
if (isString(details.error)) {
|
||||
if (details.error.startsWith('Error:')) {
|
||||
if (isErrorRun(details)) {
|
||||
return 'Property failed by throwing an error';
|
||||
}
|
||||
|
||||
|
@ -56,3 +83,7 @@ export function formatPrefix<T>(details: RunDetails<[T]>): string {
|
|||
export function isString(val: unknown): val is string {
|
||||
return typeof val === 'string';
|
||||
}
|
||||
|
||||
export function isErrorRun<T>(details: RunDetails<T>): boolean {
|
||||
return details.error?.startsWith('Error:') || false;
|
||||
}
|
|
@ -1,20 +1,22 @@
|
|||
import { integer, lorem, tuple, uuid } from 'fast-check';
|
||||
import { expect } from 'chai';
|
||||
import { array, defaultReportMessage, integer, lorem, oneof, tuple, uuid } from 'fast-check';
|
||||
|
||||
import { over } from '../src/index';
|
||||
|
||||
describe('some foo', () => {
|
||||
over('the bars', integer(), (it) => {
|
||||
const large = Math.floor(Math.random() * 1_000_000);
|
||||
it('should be a small number', (bar: number) => {
|
||||
return bar < large;
|
||||
const LARGE_VALUE = Math.floor(Math.random() * 1_000_000_000);
|
||||
|
||||
describe('example properties', () => {
|
||||
over('some numbers', integer(), (it) => {
|
||||
it('should be a small number', (n: number) => {
|
||||
return n < LARGE_VALUE;
|
||||
});
|
||||
|
||||
it('should be even', (bar: number) => {
|
||||
return bar % 2 === 0;
|
||||
it('should be even', (n: number) => {
|
||||
return n % 2 === 0;
|
||||
});
|
||||
|
||||
it('should not throw', (t: number) => {
|
||||
if (t.toString()[3] === '9') {
|
||||
it('should not throw', (n: number) => {
|
||||
if (n.toString()[3] === '9') {
|
||||
throw new Error('not a real number!');
|
||||
}
|
||||
|
||||
|
@ -23,6 +25,7 @@ describe('some foo', () => {
|
|||
});
|
||||
|
||||
over('some IDs', uuid(), (it) => {
|
||||
// beforeEach hooks work normally, since the wrapped it calls through to real it
|
||||
beforeEach(() => {
|
||||
console.log('before each ID test');
|
||||
});
|
||||
|
@ -35,7 +38,8 @@ describe('some foo', () => {
|
|||
return id.length > 2;
|
||||
});
|
||||
}, {
|
||||
examples: [['a']],
|
||||
// fast-check parameters are supported, like examples
|
||||
examples: ['a', 'b'],
|
||||
numRuns: 1_000_000_000,
|
||||
});
|
||||
|
||||
|
@ -49,5 +53,29 @@ describe('some foo', () => {
|
|||
it('should have content', (text: string) => {
|
||||
return text.length > 0;
|
||||
});
|
||||
}, {
|
||||
// error formatting can be overridden with a custom handler, or fast-check's default
|
||||
errorReporter: defaultReportMessage,
|
||||
});
|
||||
|
||||
over('tuples', tuple(integer(), integer()), (it) => {
|
||||
// tuple properties are passed as a single parameter
|
||||
it('should not be equal', ([a, b]) => {
|
||||
return a === b;
|
||||
});
|
||||
|
||||
it('should be uneven', ([a, b]) => {
|
||||
return a < b;
|
||||
});
|
||||
}, {
|
||||
examples: [[1, 2]]
|
||||
});
|
||||
|
||||
over('arrays', array(integer()), (it) => {
|
||||
it('should have items', (t: Array<number>) => {
|
||||
expect(t).to.have.length.lessThan(5_000);
|
||||
});
|
||||
}, {
|
||||
numRuns: 1_000,
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue