Skip to content

Commit

Permalink
✨ No timeout for beforeEach or afterEach (#3464)
Browse files Browse the repository at this point in the history
Timeout must only focus on the code under tests, in other words the predicate. It should not be triggered because of a beforeEach being too long, or a afterEach being too long.
  • Loading branch information
dubzzz committed Dec 10, 2022
1 parent 4c153e6 commit b330dd3
Show file tree
Hide file tree
Showing 16 changed files with 633 additions and 101 deletions.
7 changes: 7 additions & 0 deletions .yarn/versions/d036f217.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
releases:
fast-check: minor

declined:
- "@fast-check/ava"
- "@fast-check/jest"
- "@fast-check/worker"
16 changes: 14 additions & 2 deletions packages/fast-check/src/check/property/AsyncProperty.generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,18 @@ export class AsyncProperty<Ts> implements IAsyncPropertyWithHooks<Ts> {
return this.arb.shrink(value.value_, safeContext).map(noUndefinedAsContext);
}

async run(v: Ts): Promise<PreconditionFailure | PropertyFailure | null> {
async runBeforeEach(): Promise<void> {
await this.beforeEachHook();
}

async runAfterEach(): Promise<void> {
await this.afterEachHook();
}

async run(v: Ts, dontRunHook?: boolean): Promise<PreconditionFailure | PropertyFailure | null> {
if (!dontRunHook) {
await this.beforeEachHook();
}
try {
const output = await this.predicate(v);
return output == null || output === true
Expand All @@ -120,7 +130,9 @@ export class AsyncProperty<Ts> implements IAsyncPropertyWithHooks<Ts> {
}
return { error: err, errorMessage: String(err) };
} finally {
await this.afterEachHook();
if (!dontRunHook) {
await this.afterEachHook();
}
}
}

Expand Down
16 changes: 15 additions & 1 deletion packages/fast-check/src/check/property/IRawProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,27 @@ export interface IRawProperty<Ts, IsAsync extends boolean = boolean> {
/**
* Check the predicate for v
* @param v - Value of which we want to check the predicate
* @param dontRunHook - Do not run beforeEach and afterEach hooks within run
* @remarks Since 0.0.7
*/
run(
v: Ts
v: Ts,
dontRunHook?: boolean
):
| (IsAsync extends true ? Promise<PreconditionFailure | PropertyFailure | null> : never)
| (IsAsync extends false ? PreconditionFailure | PropertyFailure | null : never);

/**
* Run before each hook
* @remarks Since 3.4.0
*/
runBeforeEach?: () => (IsAsync extends true ? Promise<void> : never) | (IsAsync extends false ? void : never);

/**
* Run after each hook
* @remarks Since 3.4.0
*/
runAfterEach?: () => (IsAsync extends true ? Promise<void> : never) | (IsAsync extends false ? void : never);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,18 @@ function fromCachedUnsafe<Ts, IsAsync extends boolean>(

/** @internal */
export class IgnoreEqualValuesProperty<Ts, IsAsync extends boolean> implements IRawProperty<Ts, IsAsync> {
runBeforeEach?: () => (IsAsync extends true ? Promise<void> : never) | (IsAsync extends false ? void : never);
runAfterEach?: () => (IsAsync extends true ? Promise<void> : never) | (IsAsync extends false ? void : never);
private coveredCases: Map<string, ReturnType<IRawProperty<Ts, IsAsync>['run']>> = new Map();

constructor(readonly property: IRawProperty<Ts, IsAsync>, readonly skipRuns: boolean) {}
constructor(readonly property: IRawProperty<Ts, IsAsync>, readonly skipRuns: boolean) {
if (this.property.runBeforeEach !== undefined && this.property.runAfterEach !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.runBeforeEach = () => this.property.runBeforeEach!();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.runAfterEach = () => this.property.runAfterEach!();
}
}

isAsync(): IsAsync {
return this.property.isAsync();
Expand All @@ -55,7 +64,7 @@ export class IgnoreEqualValuesProperty<Ts, IsAsync extends boolean> implements I
return this.property.shrink(value);
}

run(v: Ts): ReturnType<IRawProperty<Ts, IsAsync>['run']> {
run(v: Ts, dontRunHook: boolean): ReturnType<IRawProperty<Ts, IsAsync>['run']> {
const stringifiedValue = stringify(v);
if (this.coveredCases.has(stringifiedValue)) {
const lastOutput = this.coveredCases.get(stringifiedValue) as ReturnType<IRawProperty<Ts, IsAsync>['run']>;
Expand All @@ -64,7 +73,7 @@ export class IgnoreEqualValuesProperty<Ts, IsAsync extends boolean> implements I
}
return fromCachedUnsafe(lastOutput, this.property.isAsync());
}
const out = this.property.run(v);
const out = this.property.run(v, dontRunHook);
this.coveredCases.set(stringifiedValue, out);
return out;
}
Expand Down
16 changes: 14 additions & 2 deletions packages/fast-check/src/check/property/Property.generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,18 @@ export class Property<Ts> implements IProperty<Ts>, IPropertyWithHooks<Ts> {
return this.arb.shrink(value.value_, safeContext).map(noUndefinedAsContext);
}

run(v: Ts): PreconditionFailure | PropertyFailure | null {
runBeforeEach(): void {
this.beforeEachHook();
}

runAfterEach(): void {
this.afterEachHook();
}

run(v: Ts, dontRunHook?: boolean): PreconditionFailure | PropertyFailure | null {
if (!dontRunHook) {
this.beforeEachHook();
}
try {
const output = this.predicate(v);
return output == null || output === true
Expand All @@ -136,7 +146,9 @@ export class Property<Ts> implements IProperty<Ts>, IPropertyWithHooks<Ts> {
}
return { error: err, errorMessage: String(err) };
} finally {
this.afterEachHook();
if (!dontRunHook) {
this.afterEachHook();
}
}
}

Expand Down
13 changes: 11 additions & 2 deletions packages/fast-check/src/check/property/SkipAfterProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ import { IRawProperty } from './IRawProperty';

/** @internal */
export class SkipAfterProperty<Ts, IsAsync extends boolean> implements IRawProperty<Ts, IsAsync> {
runBeforeEach?: () => (IsAsync extends true ? Promise<void> : never) | (IsAsync extends false ? void : never);
runAfterEach?: () => (IsAsync extends true ? Promise<void> : never) | (IsAsync extends false ? void : never);
private skipAfterTime: number;

constructor(
readonly property: IRawProperty<Ts, IsAsync>,
readonly getTime: () => number,
timeLimit: number,
readonly interruptExecution: boolean
) {
this.skipAfterTime = this.getTime() + timeLimit;
if (this.property.runBeforeEach !== undefined && this.property.runAfterEach !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.runBeforeEach = () => this.property.runBeforeEach!();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.runAfterEach = () => this.property.runAfterEach!();
}
}

isAsync(): IsAsync {
Expand All @@ -28,7 +37,7 @@ export class SkipAfterProperty<Ts, IsAsync extends boolean> implements IRawPrope
return this.property.shrink(value);
}

run(v: Ts): ReturnType<IRawProperty<Ts, IsAsync>['run']> {
run(v: Ts, dontRunHook: boolean): ReturnType<IRawProperty<Ts, IsAsync>['run']> {
if (this.getTime() >= this.skipAfterTime) {
const preconditionFailure = new PreconditionFailure(this.interruptExecution);
if (this.isAsync()) {
Expand All @@ -37,6 +46,6 @@ export class SkipAfterProperty<Ts, IsAsync extends boolean> implements IRawPrope
return preconditionFailure as any; // !IsAsync => PreconditionFailure | string | null
}
}
return this.property.run(v);
return this.property.run(v, dontRunHook);
}
}
16 changes: 13 additions & 3 deletions packages/fast-check/src/check/property/TimeoutProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@ const timeoutAfter = (timeMs: number) => {

/** @internal */
export class TimeoutProperty<Ts> implements IRawProperty<Ts, true> {
constructor(readonly property: IRawProperty<Ts>, readonly timeMs: number) {}
runBeforeEach?: () => Promise<void>;
runAfterEach?: () => Promise<void>;

constructor(readonly property: IRawProperty<Ts>, readonly timeMs: number) {
if (this.property.runBeforeEach !== undefined && this.property.runAfterEach !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.runBeforeEach = () => Promise.resolve(this.property.runBeforeEach!());
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.runAfterEach = () => Promise.resolve(this.property.runAfterEach!());
}
}

isAsync(): true {
return true;
Expand All @@ -40,9 +50,9 @@ export class TimeoutProperty<Ts> implements IRawProperty<Ts, true> {
return this.property.shrink(value);
}

async run(v: Ts): Promise<PreconditionFailure | PropertyFailure | null> {
async run(v: Ts, dontRunHook: boolean): Promise<PreconditionFailure | PropertyFailure | null> {
const t = timeoutAfter(this.timeMs);
const propRun = Promise.race([this.property.run(v), t.promise]);
const propRun = Promise.race([this.property.run(v, dontRunHook), t.promise]);
propRun.then(t.clear, t.clear); // always clear timeout handle - catch should never occur
return propRun;
}
Expand Down
16 changes: 13 additions & 3 deletions packages/fast-check/src/check/property/UnbiasedProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ import { IRawProperty } from './IRawProperty';

/** @internal */
export class UnbiasedProperty<Ts, IsAsync extends boolean> implements IRawProperty<Ts, IsAsync> {
constructor(readonly property: IRawProperty<Ts, IsAsync>) {}
runBeforeEach?: () => (IsAsync extends true ? Promise<void> : never) | (IsAsync extends false ? void : never);
runAfterEach?: () => (IsAsync extends true ? Promise<void> : never) | (IsAsync extends false ? void : never);

constructor(readonly property: IRawProperty<Ts, IsAsync>) {
if (this.property.runBeforeEach !== undefined && this.property.runAfterEach !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.runBeforeEach = () => this.property.runBeforeEach!();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.runAfterEach = () => this.property.runAfterEach!();
}
}

isAsync(): IsAsync {
return this.property.isAsync();
Expand All @@ -19,7 +29,7 @@ export class UnbiasedProperty<Ts, IsAsync extends boolean> implements IRawProper
return this.property.shrink(value);
}

run(v: Ts): ReturnType<IRawProperty<Ts, IsAsync>['run']> {
return this.property.run(v);
run(v: Ts, dontRunHook: boolean): ReturnType<IRawProperty<Ts, IsAsync>['run']> {
return this.property.run(v, dontRunHook);
}
}
22 changes: 20 additions & 2 deletions packages/fast-check/src/check/runner/Runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,18 @@ function runIt<Ts>(
verbose: VerbosityLevel,
interruptedAsFailure: boolean
): RunExecution<Ts> {
const isModernProperty = property.runBeforeEach !== undefined && property.runAfterEach !== undefined;
const runner = new RunnerIterator(sourceValues, shrink, verbose, interruptedAsFailure);
for (const v of runner) {
const out = property.run(v) as PreconditionFailure | PropertyFailure | null;
if (isModernProperty) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
property.runBeforeEach!();
}
const out = property.run(v, isModernProperty) as PreconditionFailure | PropertyFailure | null;
if (isModernProperty) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
property.runAfterEach!();
}
runner.handleResult(out);
}
return runner.runExecution;
Expand All @@ -43,9 +52,18 @@ async function asyncRunIt<Ts>(
verbose: VerbosityLevel,
interruptedAsFailure: boolean
): Promise<RunExecution<Ts>> {
const isModernProperty = property.runBeforeEach !== undefined && property.runAfterEach !== undefined;
const runner = new RunnerIterator(sourceValues, shrink, verbose, interruptedAsFailure);
for (const v of runner) {
const out = await property.run(v);
if (isModernProperty) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await property.runBeforeEach!();
}
const out = await property.run(v, isModernProperty);
if (isModernProperty) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await property.runAfterEach!();
}
runner.handleResult(out);
}
return runner.runExecution;
Expand Down
28 changes: 28 additions & 0 deletions packages/fast-check/test/e2e/Timeout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as fc from '../../src/fast-check';
import { seed } from './seed';

describe(`Timeout (seed: ${seed})`, () => {
it('should always run beforeEach and afterEach even in case of timeout', async () => {
let numRuns = 0;
const beforeEach = jest.fn().mockResolvedValue(undefined);
const afterEach = jest.fn().mockResolvedValue(undefined);
const out = await fc.check(
fc
.asyncProperty(fc.integer().noShrink(), async (_x) => {
++numRuns;
await new Promise(() => {}); // never ending promise
})
.beforeEach(beforeEach)
.afterEach(afterEach),
{ timeout: 0 }
);
expect(out.failed).toBe(true);
expect(out.interrupted).toBe(false);
expect(out.numRuns).toBe(1); // only once, it timeouts on first run and then shrink (no-shrink here)
expect(out.numShrinks).toBe(0);
expect(out.numSkips).toBe(0);
expect(numRuns).toBe(1);
expect(beforeEach).toHaveBeenCalledTimes(1);
expect(afterEach).toHaveBeenCalledTimes(1);
});
});

0 comments on commit b330dd3

Please sign in to comment.