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 WrappedIt<T> = (name: string, check: Check<T>) => void;
|
||||||
export type Suite<T> = (it: WrappedIt<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, () => {
|
describe(name, () => {
|
||||||
suite((name, test) => {
|
suite((name, test) => {
|
||||||
it(name, function (this: Mocha.Context): Promise<void> {
|
it(name, function (this: Mocha.Context): Promise<void> {
|
||||||
const ctx = this;
|
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) => {
|
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) {
|
if (result.failed) {
|
||||||
rej(new Error(formatDetails(result)));
|
rej(new Error(reporter(result)));
|
||||||
} else {
|
} else {
|
||||||
res();
|
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 prefix = formatPrefix(details);
|
||||||
const counts = `${prefix} after ${details.numRuns} runs and ${details.numShrinks} shrinks`;
|
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) {
|
if (details.counterexample !== null) {
|
||||||
const examples = details.counterexample.map((val) => {
|
const examples = details.counterexample.map((val) => {
|
||||||
if (isString(val)) {
|
if (isString(val)) {
|
||||||
|
@ -35,15 +61,16 @@ export function formatDetails<T>(details: RunDetails<[T]>): string {
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
}).join(',');
|
}).join(',');
|
||||||
return `${counts}, failing on: ${examples}`;
|
return `failing on: ${examples}`;
|
||||||
} else {
|
} else {
|
||||||
return `${counts}, without counterexamples`;
|
return `without counterexamples`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatPrefix<T>(details: RunDetails<[T]>): string {
|
export function formatPrefix<T>(details: RunDetails<[T]>): string {
|
||||||
if (isString(details.error)) {
|
if (isString(details.error)) {
|
||||||
if (details.error.startsWith('Error:')) {
|
if (isErrorRun(details)) {
|
||||||
return 'Property failed by throwing an error';
|
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 {
|
export function isString(val: unknown): val is string {
|
||||||
return typeof val === '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';
|
import { over } from '../src/index';
|
||||||
|
|
||||||
describe('some foo', () => {
|
const LARGE_VALUE = Math.floor(Math.random() * 1_000_000_000);
|
||||||
over('the bars', integer(), (it) => {
|
|
||||||
const large = Math.floor(Math.random() * 1_000_000);
|
describe('example properties', () => {
|
||||||
it('should be a small number', (bar: number) => {
|
over('some numbers', integer(), (it) => {
|
||||||
return bar < large;
|
it('should be a small number', (n: number) => {
|
||||||
|
return n < LARGE_VALUE;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be even', (bar: number) => {
|
it('should be even', (n: number) => {
|
||||||
return bar % 2 === 0;
|
return n % 2 === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw', (t: number) => {
|
it('should not throw', (n: number) => {
|
||||||
if (t.toString()[3] === '9') {
|
if (n.toString()[3] === '9') {
|
||||||
throw new Error('not a real number!');
|
throw new Error('not a real number!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +25,7 @@ describe('some foo', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
over('some IDs', uuid(), (it) => {
|
over('some IDs', uuid(), (it) => {
|
||||||
|
// beforeEach hooks work normally, since the wrapped it calls through to real it
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
console.log('before each ID test');
|
console.log('before each ID test');
|
||||||
});
|
});
|
||||||
|
@ -35,7 +38,8 @@ describe('some foo', () => {
|
||||||
return id.length > 2;
|
return id.length > 2;
|
||||||
});
|
});
|
||||||
}, {
|
}, {
|
||||||
examples: [['a']],
|
// fast-check parameters are supported, like examples
|
||||||
|
examples: ['a', 'b'],
|
||||||
numRuns: 1_000_000_000,
|
numRuns: 1_000_000_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,5 +53,29 @@ describe('some foo', () => {
|
||||||
it('should have content', (text: string) => {
|
it('should have content', (text: string) => {
|
||||||
return text.length > 0;
|
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