1
0
Fork 0

forward runner parameters, custom error reporting

This commit is contained in:
ssube 2021-07-22 12:33:33 -05:00
parent 141999b881
commit e2bd5b1d26
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
2 changed files with 79 additions and 20 deletions

View File

@ -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;
}

View File

@ -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,
});
});