Permalink
Browse files

Add `t.timeout()`

Fixes #1565
  • Loading branch information...
dflupu authored and novemberborn committed Jan 26, 2019
1 parent ed7807e commit b65c6d7da8ba3c7274f36dbcbcff26485f27d36f
Showing with 123 additions and 2 deletions.
  1. +4 βˆ’0 docs/02-execution-context.md
  2. +9 βˆ’0 docs/07-test-timeouts.md
  3. +9 βˆ’0 index.d.ts
  4. +9 βˆ’0 index.js.flow
  5. +59 βˆ’2 lib/test.js
  6. +33 βˆ’0 test/test.js
@@ -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.
@@ -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
});
```
@@ -348,6 +348,7 @@ export interface ExecutionContext<Context = {}> 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<Context = {}> extends ExecutionContext<Context> {
/**
@@ -361,6 +361,7 @@ export interface ExecutionContext<Context = {}> 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<Context = {}> extends ExecutionContext<Context> {
/**
@@ -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();

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

0 comments on commit b65c6d7

Please sign in to comment.