Skip to content

Commit e3e1752

Browse files
committed
Merge pull request #654 from sindresorhus/timeout
Add timeout functionality
2 parents 8d6490a + ae45752 commit e3e1752

File tree

11 files changed

+164
-36
lines changed

11 files changed

+164
-36
lines changed

api.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ var commonPathPrefix = require('common-path-prefix');
1313
var resolveCwd = require('resolve-cwd');
1414
var uniqueTempDir = require('unique-temp-dir');
1515
var findCacheDir = require('find-cache-dir');
16+
var debounce = require('lodash.debounce');
1617
var slash = require('slash');
1718
var isObj = require('is-obj');
19+
var ms = require('ms');
1820
var AvaError = require('./lib/ava-error');
1921
var fork = require('./lib/fork');
2022
var formatter = require('./lib/enhance-assert').formatter();
@@ -49,6 +51,11 @@ function Api(options) {
4951
}, this);
5052

5153
this._reset();
54+
55+
if (this.options.timeout) {
56+
var timeout = ms(this.options.timeout);
57+
this._restartTimer = debounce(this._onTimeout, timeout);
58+
}
5259
}
5360

5461
util.inherits(Api, EventEmitter);
@@ -187,6 +194,18 @@ Api.prototype._prefixTitle = function (file) {
187194
return prefix;
188195
};
189196

197+
Api.prototype._onTimeout = function () {
198+
var timeout = ms(this.options.timeout);
199+
var message = 'Exited because no new tests completed within the last ' + timeout + 'ms of inactivity';
200+
201+
this._handleExceptions({
202+
exception: new AvaError(message),
203+
file: null
204+
});
205+
206+
this.emit('timeout');
207+
};
208+
190209
Api.prototype.run = function (files, options) {
191210
var self = this;
192211

@@ -196,6 +215,11 @@ Api.prototype.run = function (files, options) {
196215
this.hasExclusive = true;
197216
}
198217

218+
if (this.options.timeout) {
219+
this._restartTimer();
220+
this.on('test', this._restartTimer);
221+
}
222+
199223
return handlePaths(files, this.excludePatterns)
200224
.map(function (file) {
201225
return path.resolve(file);
@@ -221,6 +245,12 @@ Api.prototype.run = function (files, options) {
221245

222246
var tests = new Array(self.fileCount);
223247

248+
self.on('timeout', function () {
249+
tests.forEach(function (fork) {
250+
fork.exit();
251+
});
252+
});
253+
224254
return new Promise(function (resolve) {
225255
function run() {
226256
if (self.options.match.length > 0 && !self.hasExclusive) {
@@ -313,6 +343,11 @@ Api.prototype.run = function (files, options) {
313343
});
314344
})
315345
.then(function (results) {
346+
// cancel debounced _onTimeout() from firing
347+
if (self.options.timeout) {
348+
self._restartTimer.cancel();
349+
}
350+
316351
// assemble stats from all tests
317352
self.stats = results.map(function (result) {
318353
return result.stats;

cli.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ var cli = meow([
6969
' --match, -m Only run tests with matching title (Can be repeated)',
7070
' --watch, -w Re-run tests when tests and source files change',
7171
' --source, -S Pattern to match source files so tests can be re-run (Can be repeated)',
72+
' --timeout, -T Set global timeout',
7273
'',
7374
'Examples',
7475
' ava',
@@ -84,6 +85,7 @@ var cli = meow([
8485
string: [
8586
'_',
8687
'require',
88+
'timeout',
8789
'source',
8890
'match'
8991
],
@@ -102,7 +104,8 @@ var cli = meow([
102104
s: 'serial',
103105
m: 'match',
104106
w: 'watch',
105-
S: 'source'
107+
S: 'source',
108+
T: 'timeout'
106109
}
107110
});
108111

@@ -120,7 +123,8 @@ var api = new Api({
120123
cacheEnabled: cli.flags.cache !== false,
121124
explicitTitles: cli.flags.watch,
122125
match: arrify(cli.flags.match),
123-
babelConfig: conf.babel
126+
babelConfig: conf.babel,
127+
timeout: cli.flags.timeout
124128
});
125129

126130
var reporter;

index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ function test(props) {
6161
}
6262

6363
function exit() {
64+
var stats = runner._buildStats();
65+
6466
send('results', {
65-
stats: runner.stats
67+
stats: stats
6668
});
6769
}
6870

@@ -85,6 +87,10 @@ globals.setImmediate(function () {
8587
process.on('ava-run', function (options) {
8688
runner.run(options).then(exit);
8789
});
90+
91+
process.on('ava-init-exit', function () {
92+
exit();
93+
});
8894
});
8995

9096
module.exports = runner.test;

lib/fork.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ module.exports = function (file, opts) {
4848
}
4949
};
5050

51+
var testResults = [];
52+
var results;
53+
5154
var promise = new Promise(function (resolve, reject) {
5255
ps.on('error', reject);
5356

@@ -65,9 +68,6 @@ module.exports = function (file, opts) {
6568
ps.emit(event.name, event.data);
6669
});
6770

68-
var testResults = [];
69-
var results;
70-
7171
ps.on('test', function (props) {
7272
testResults.push(props);
7373
});
@@ -142,6 +142,12 @@ module.exports = function (file, opts) {
142142
return promise;
143143
};
144144

145+
promise.exit = function () {
146+
send(ps, 'init-exit');
147+
148+
return promise;
149+
};
150+
145151
// send 'run' event only when fork is listening for it
146152
var isReady = false;
147153

lib/runner.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function Runner(options) {
4141

4242
options = options || {};
4343

44+
this.results = [];
4445
this.tests = new TestCollection();
4546
this._bail = options.bail;
4647
this._serial = options.serial;
@@ -112,11 +113,51 @@ Runner.prototype._addTestResult = function (result) {
112113
todo: test.metadata.todo
113114
};
114115

116+
this.results.push(result);
115117
this.emit('test', props);
116118
};
117119

120+
Runner.prototype._buildStats = function () {
121+
var stats = {
122+
testCount: 0,
123+
skipCount: 0,
124+
todoCount: 0
125+
};
126+
127+
this.results
128+
.map(function (result) {
129+
return result.result;
130+
})
131+
.filter(function (test) {
132+
return test.metadata.type === 'test';
133+
})
134+
.forEach(function (test) {
135+
stats.testCount++;
136+
137+
if (test.metadata.skipped) {
138+
stats.skipCount++;
139+
}
140+
141+
if (test.metadata.todo) {
142+
stats.todoCount++;
143+
}
144+
});
145+
146+
stats.failCount = this.results
147+
.filter(function (result) {
148+
return result.passed === false;
149+
})
150+
.length;
151+
152+
stats.passCount = stats.testCount - stats.failCount - stats.skipCount - stats.todoCount;
153+
154+
return stats;
155+
};
156+
118157
Runner.prototype.run = function (options) {
119-
var stats = this.stats = {
158+
var self = this;
159+
160+
this.stats = {
120161
failCount: 0,
121162
passCount: 0,
122163
skipCount: 0,
@@ -131,6 +172,8 @@ Runner.prototype.run = function (options) {
131172
this.tests.on('test', this._addTestResult);
132173

133174
return Promise.resolve(this.tests.build(this._bail).run()).then(function () {
134-
stats.passCount = stats.testCount - stats.failCount - stats.skipCount - stats.todoCount;
175+
self.stats = self._buildStats();
176+
177+
return self.stats;
135178
});
136179
};

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,13 @@
109109
"is-observable": "^0.1.0",
110110
"is-promise": "^2.1.0",
111111
"last-line-stream": "^1.0.0",
112+
"lodash.debounce": "^4.0.3",
112113
"loud-rejection": "^1.2.0",
113114
"matcher": "^0.1.1",
114115
"max-timeout": "^1.0.0",
115116
"md5-hex": "^1.2.0",
116117
"meow": "^3.7.0",
118+
"ms": "^0.7.1",
117119
"multimatch": "^2.1.0",
118120
"object-assign": "^4.0.1",
119121
"observable-to-promise": "^0.3.0",

readme.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ $ ava --help
146146
--match, -m Only run tests with matching title (Can be repeated)',
147147
--watch, -w Re-run tests when tests and source files change
148148
--source, -S Pattern to match source files so tests can be re-run (Can be repeated)
149+
--timeout, -T Set global timeout
149150

150151
Examples
151152
ava
@@ -679,6 +680,20 @@ AVA automatically removes unrelated lines in stack traces, allowing you to find
679680

680681
<img src="media/stack-traces.png" width="300">
681682

683+
### Global timeout
684+
685+
A global timeout can be set via the `--timeout` option.
686+
Timeout in AVA behaves differently than in other test frameworks.
687+
AVA resets a timer after each test, forcing tests to quit if no new test results were received within the specified timeout.
688+
689+
You can set timeouts in a human-readable way:
690+
691+
```
692+
$ ava --timeout=10s # 10 seconds
693+
$ ava --timeout=2m # 2 minutes
694+
$ ava --timeout=100 # 100 milliseconds
695+
```
696+
682697
## API
683698

684699
### `test([title], callback)`

test/cli.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,15 @@ test('disallow invalid babel config shortcuts', function (t) {
7878
});
7979
});
8080

81-
test('throwing a named function will report the function to the console', function (t) {
81+
test('timeout', function (t) {
82+
execCli(['fixture/long-running.js', '-T', '1s'], function (err, stdout, stderr) {
83+
t.ok(err);
84+
t.match(stderr, /Exited because no new tests completed within the last 1000ms of inactivity/);
85+
t.end();
86+
});
87+
});
88+
89+
test('throwing a named function will report the to the console', function (t) {
8290
execCli('fixture/throw-named-function.js', function (err, stdout, stderr) {
8391
t.ok(err);
8492
t.match(stderr, /function fooFn\(\) \{\}/);

test/fixture/long-running.js

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,9 @@
11
import test from '../../';
2-
import signalExit from 'signal-exit';
32

4-
test.cb('long running', t => {
5-
t.plan(1);
6-
7-
signalExit(() => {
8-
// simulate an exit hook that lasts a short while
9-
const start = Date.now();
10-
11-
while (Date.now() - start < 2000) {
12-
// synchronously wait for 2 seconds
13-
}
14-
15-
process.send({
16-
name: 'cleanup-completed',
17-
data: {completed: true},
18-
ava: true
19-
});
20-
}, {alwaysLast: true});
3+
test.cb('slow', t => {
4+
setTimeout(t.end, 5000);
5+
});
216

22-
setTimeout(() => {
23-
t.ok(true);
24-
t.end();
25-
});
7+
test('fast', t => {
268

27-
setTimeout(() => {
28-
// this would keep the process running for a long time
29-
console.log('I\'m gonna live forever!!');
30-
}, 15000);
319
});

test/fixture/slow-exit.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import test from '../../';
2+
import signalExit from 'signal-exit';
3+
4+
test.cb('long running', t => {
5+
t.plan(1);
6+
7+
signalExit(() => {
8+
// simulate an exit hook that lasts a short while
9+
const start = Date.now();
10+
11+
while (Date.now() - start < 2000) {
12+
// synchronously wait for 2 seconds
13+
}
14+
15+
process.send({
16+
name: 'cleanup-completed',
17+
data: {completed: true},
18+
ava: true
19+
});
20+
}, {alwaysLast: true});
21+
22+
setTimeout(() => {
23+
t.ok(true);
24+
t.end();
25+
});
26+
27+
setTimeout(() => {
28+
// this would keep the process running for a long time
29+
console.log('I\'m gonna live forever!!');
30+
}, 15000);
31+
});

test/fork.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ test('exit after tests are finished', function (t) {
4949
var start = Date.now();
5050
var cleanupCompleted = false;
5151

52-
fork(fixture('long-running.js'))
52+
fork(fixture('slow-exit.js'))
5353
.run({})
5454
.on('exit', function () {
5555
t.true(Date.now() - start < 10000, 'test waited for a pending setTimeout');

0 commit comments

Comments
 (0)