diff --git a/docs/02-execution-context.md b/docs/02-execution-context.md index 203cad68c..da6148811 100644 --- a/docs/02-execution-context.md +++ b/docs/02-execution-context.md @@ -33,3 +33,7 @@ End the test. Only works with `test.cb()`. ## `t.log(...values)` Log values contextually alongside the test result instead of immediately printing them to `stdout`. Behaves somewhat like `console.log`, but without support for placeholder tokens. + +## `t.timeout(ms)` + +Set a timeout for the test, in milliseconds. The test will fail if this timeout is exceeded. The timeout is reset each time an assertion is made. diff --git a/docs/07-test-timeouts.md b/docs/07-test-timeouts.md index c0cc1af67..f5cd4106c 100644 --- a/docs/07-test-timeouts.md +++ b/docs/07-test-timeouts.md @@ -13,3 +13,12 @@ npx ava --timeout=10s # 10 seconds npx ava --timeout=2m # 2 minutes npx ava --timeout=100 # 100 milliseconds ``` + +Timeouts can also be set individually for each test. These timeouts are reset each time an assertion is made. + +```js +test('foo', t => { + t.timeout(100); // 100 milliseconds + // Write your assertions here +}); +``` diff --git a/index.d.ts b/index.d.ts index 0417d2354..fca580a4a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -348,6 +348,7 @@ export interface ExecutionContext extends Assertions { log: LogFn; plan: PlanFn; + timeout: TimeoutFn; } export interface LogFn { @@ -369,6 +370,14 @@ export interface PlanFn { skip(count: number): void; } +export interface TimeoutFn { + /** + * Set a timeout for the test, in milliseconds. The test will fail if the timeout is exceeded. + * The timeout is reset each time an assertion is made. + */ + (ms: number): void; +} + /** The `t` value passed to implementations for tests & hooks declared with the `.cb` modifier. */ export interface CbExecutionContext extends ExecutionContext { /** diff --git a/index.js.flow b/index.js.flow index b9b9df983..2277fffba 100644 --- a/index.js.flow +++ b/index.js.flow @@ -361,6 +361,7 @@ export interface ExecutionContext extends Assertions { log: LogFn; plan: PlanFn; + timeout: TimeoutFn; } export interface LogFn { @@ -382,6 +383,14 @@ export interface PlanFn { skip(count: number): void; } +export interface TimeoutFn { + /** + * Set a timeout for the test, in milliseconds. The test will fail if the timeout is exceeded. + * The timeout is reset each time an assertion is made. + */ + (ms: number): void; +} + /** The `t` value passed to implementations for tests & hooks declared with the `.cb` modifier. */ export interface CbExecutionContext extends ExecutionContext { /** diff --git a/lib/test.js b/lib/test.js index 729c17f3d..f417034de 100644 --- a/lib/test.js +++ b/lib/test.js @@ -53,6 +53,10 @@ function plan(count) { this.plan(count, captureStack(this.plan)); } +function timeout(ms) { + this.timeout(ms); +} + const testMap = new WeakMap(); class ExecutionContext { constructor(test) { @@ -71,7 +75,8 @@ class ExecutionContext { return props; }, { log: {value: log.bind(test)}, - plan: {value: boundPlan} + plan: {value: boundPlan}, + timeout: {value: timeout.bind(test)} })); this.snapshot.skip = () => { @@ -140,11 +145,14 @@ class Test { this.endCallbackFinisher = null; this.finishDueToAttributedError = null; this.finishDueToInactivity = null; + this.finishDueToTimeout = null; this.finishing = false; this.pendingAssertionCount = 0; this.pendingThrowsAssertion = null; this.planCount = null; this.startedAt = 0; + this.timeoutTimer = null; + this.timeoutMs = 0; } bindEndCallback() { @@ -189,6 +197,7 @@ class Test { } this.assertCount++; + this.refreshTimeout(); } addLog(text) { @@ -202,9 +211,14 @@ class Test { this.assertCount++; this.pendingAssertionCount++; + this.refreshTimeout(); + promise .catch(error => this.saveFirstError(error)) - .then(() => this.pendingAssertionCount--); + .then(() => { + this.pendingAssertionCount--; + this.refreshTimeout(); + }); } addFailedAssertion(error) { @@ -213,6 +227,7 @@ class Test { } this.assertCount++; + this.refreshTimeout(); this.saveFirstError(error); } @@ -234,6 +249,39 @@ class Test { this.planStack = planStack; } + timeout(ms) { + if (this.finishing) { + return; + } + + this.clearTimeout(); + this.timeoutMs = ms; + this.timeoutTimer = nowAndTimers.setTimeout(() => { + this.saveFirstError(new Error('Test timeout exceeded')); + + if (this.finishDueToTimeout) { + this.finishDueToTimeout(); + } + }, ms); + } + + refreshTimeout() { + if (!this.timeoutTimer) { + return; + } + + if (this.timeoutTimer.refresh) { + this.timeoutTimer.refresh(); + } else { + this.timeout(this.timeoutMs); + } + } + + clearTimeout() { + clearTimeout(this.timeoutTimer); + this.timeoutTimer = null; + } + verifyPlan() { if (!this.assertError && this.planCount !== null && this.planCount !== this.assertCount) { this.saveFirstError(new assert.AssertionError({ @@ -370,6 +418,10 @@ class Test { resolve(this.finishPromised()); }; + this.finishDueToTimeout = () => { + resolve(this.finishPromised()); + }; + this.finishDueToInactivity = () => { this.saveFirstError(new Error('`t.end()` was never called')); resolve(this.finishPromised()); @@ -383,6 +435,10 @@ class Test { resolve(this.finishPromised()); }; + this.finishDueToTimeout = () => { + resolve(this.finishPromised()); + }; + this.finishDueToInactivity = () => { const error = returnedObservable ? new Error('Observable returned by test never completed') : @@ -415,6 +471,7 @@ class Test { return this.waitForPendingThrowsAssertion(); } + this.clearTimeout(); this.verifyPlan(); this.verifyAssertions(); diff --git a/test/test.js b/test/test.js index 53df88758..847836b97 100644 --- a/test/test.js +++ b/test/test.js @@ -764,3 +764,36 @@ test('implementation runs with null scope', t => { t.is(this, null); }).run(); }); + +test('timeout with promise', t => { + return ava(a => { + a.timeout(10); + return delay(200); + }).run().then(result => { + t.is(result.passed, false); + t.match(result.error.message, /timeout/); + }); +}); + +test('timeout with cb', t => { + return ava.cb(a => { + a.timeout(10); + setTimeout(() => a.end(), 200); + }).run().then(result => { + t.is(result.passed, false); + t.match(result.error.message, /timeout/); + }); +}); + +test('timeout is refreshed on assert', t => { + return ava.cb(a => { + a.timeout(10); + a.plan(3); + setTimeout(() => a.pass(), 5); + setTimeout(() => a.pass(), 10); + setTimeout(() => a.pass(), 15); + setTimeout(() => a.end(), 20); + }).run().then(result => { + t.is(result.passed, true); + }); +});