Skip to content

Commit

Permalink
✨ Interrupt predicates when interruptAfterTimeLimit (#3507)
Browse files Browse the repository at this point in the history
* ✨ Interrupt predicates when `interruptAfterTimeLimit`

In the context of #3084, we want to have a way built-in in fast-check to make sure that tests do not run for too long and can abide by certain timeouts constraints requested by the users.

While today we do have a timeout, it only scopes the interruption to one run of the predicate but does not consider "the runs" as a whole contrary to `interruptAfterTimeLimit`.

On the other hand, `interruptAfterTimeLimit` does have a timeout like behaviour but once started the predicate is never interrupted. Now it could be interrupted in-between, if running it until the end would cause a timeout given the time constraint passed to `interruptAfterTimeLimit`.

With such new feature in place, we will probably have to consider properties not having run at least once as failure, otherwise a buggy async algorithm would be marked as failure if it led to timeout at first run with `interruptAfterTimeLimit`.

* versions

* Add tests

* add tests

* Revert "Add tests"

This reverts commit 14fb071.
  • Loading branch information
dubzzz committed Dec 22, 2022
1 parent 36a7d2c commit 0c014fe
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .yarn/versions/cb9adede.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"
26 changes: 25 additions & 1 deletion packages/fast-check/src/check/property/SkipAfterProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ import { Value } from '../arbitrary/definition/Value';
import { PreconditionFailure } from '../precondition/PreconditionFailure';
import { IRawProperty } from './IRawProperty';

/** @internal */
function interruptAfter(timeMs: number) {
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
const promise = new Promise<PreconditionFailure>((resolve) => {
timeoutHandle = setTimeout(() => {
const preconditionFailure = new PreconditionFailure(true);
resolve(preconditionFailure);
}, timeMs);
});
return {
// `timeoutHandle` will always be initialised at this point: body of `new Promise` has already been executed
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
clear: () => clearTimeout(timeoutHandle!),
promise,
};
}

/** @internal */
export class SkipAfterProperty<Ts, IsAsync extends boolean> implements IRawProperty<Ts, IsAsync> {
runBeforeEach?: () => (IsAsync extends true ? Promise<void> : never) | (IsAsync extends false ? void : never);
Expand Down Expand Up @@ -38,14 +55,21 @@ export class SkipAfterProperty<Ts, IsAsync extends boolean> implements IRawPrope
}

run(v: Ts, dontRunHook: boolean): ReturnType<IRawProperty<Ts, IsAsync>['run']> {
if (this.getTime() >= this.skipAfterTime) {
const remainingTime = this.skipAfterTime - this.getTime();
if (remainingTime <= 0) {
const preconditionFailure = new PreconditionFailure(this.interruptExecution);
if (this.isAsync()) {
return Promise.resolve(preconditionFailure) as any; // IsAsync => Promise<PreconditionFailure | string | null>
} else {
return preconditionFailure as any; // !IsAsync => PreconditionFailure | string | null
}
}
if (this.interruptExecution && this.isAsync()) {
const t = interruptAfter(remainingTime);
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 as any; // IsAsync => Promise<PreconditionFailure | string | null>
}
return this.property.run(v, dontRunHook);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,81 @@ describe.each([[true], [false]])('SkipAfterProperty (dontRunHook: %p)', (dontRun
expect(PreconditionFailure.isFailure(out)).toBe(true);
expect(PreconditionFailure.isFailure(out) && out.interruptExecution).toBe(true);
});

describe('timeout', () => {
it('should clear all started timeouts on success', async () => {
// Arrange
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');
jest.spyOn(global, 'clearTimeout');
const { instance: decoratedProperty, run } = fakeProperty(true);
run.mockResolvedValueOnce(null);

// Act
const timeoutProp = new SkipAfterProperty(decoratedProperty, Date.now, 10, true);
if (dontRunHook) {
await timeoutProp.runBeforeEach!();
await timeoutProp.run({}, true);
await timeoutProp.runAfterEach!();
} else {
await timeoutProp.run({}, false);
}

// Assert
expect(setTimeout).toBeCalledTimes(1);
expect(clearTimeout).toBeCalledTimes(1);
});

it('should clear all started timeouts on failure', async () => {
// Arrange
const errorFromUnderlying = { error: undefined, errorMessage: 'plop' };
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');
jest.spyOn(global, 'clearTimeout');
const { instance: decoratedProperty, run } = fakeProperty(true);
run.mockResolvedValueOnce(errorFromUnderlying);

// Act
const timeoutProp = new SkipAfterProperty(decoratedProperty, Date.now, 10, true);
if (dontRunHook) {
await timeoutProp.runBeforeEach!();
await timeoutProp.run({}, true);
await timeoutProp.runAfterEach!();
} else {
await timeoutProp.run({}, false);
}

// Assert
expect(setTimeout).toBeCalledTimes(1);
expect(clearTimeout).toBeCalledTimes(1);
});

it('should timeout if it takes to long', async () => {
// Arrange
jest.useFakeTimers();
const { instance: decoratedProperty, run } = fakeProperty(true);
run.mockReturnValueOnce(
new Promise(function (resolve) {
setTimeout(() => resolve(null), 100);
})
);

// Act
const timeoutProp = new SkipAfterProperty(decoratedProperty, Date.now, 10, true);
let runPromise: ReturnType<typeof timeoutProp.run>;
if (dontRunHook) {
await timeoutProp.runBeforeEach!();
runPromise = timeoutProp.run({}, true);
} else {
runPromise = timeoutProp.run({}, false);
}
jest.advanceTimersByTime(10);

// Assert
const out = await runPromise;
expect(PreconditionFailure.isFailure(out)).toBe(true);
expect(PreconditionFailure.isFailure(out) && out.interruptExecution).toBe(true);
await timeoutProp.runAfterEach!();
});
});
});

0 comments on commit 0c014fe

Please sign in to comment.