Skip to content

Commit 0841438

Browse files
committed
Improve failFast at API level
Don't run new test files after: * A timeout has occurred * A test has failed * Running the test file itself caused an error Refs #1158.
1 parent 1702967 commit 0841438

File tree

9 files changed

+183
-4
lines changed

9 files changed

+183
-4
lines changed

api.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,20 @@ class Api extends EventEmitter {
5656
};
5757

5858
// Track active forks and manage timeouts.
59+
const failFast = apiOptions.failFast === true;
60+
let bailed = false;
5961
const pendingForks = new Set();
6062
let restartTimer;
6163
if (apiOptions.timeout) {
6264
const timeout = ms(apiOptions.timeout);
65+
6366
restartTimer = debounce(() => {
67+
// If failFast is active, prevent new test files from running after
68+
// the current ones are exited.
69+
if (failFast) {
70+
bailed = true;
71+
}
72+
6473
for (const fork of pendingForks) {
6574
fork.exit();
6675
}
@@ -78,11 +87,20 @@ class Api extends EventEmitter {
7887
runOnlyExclusive: runtimeOptions.runOnlyExclusive,
7988
prefixTitles: apiOptions.explicitTitles || files.length > 1,
8089
base: path.relative(process.cwd(), commonPathPrefix(files)) + path.sep,
81-
failFast: apiOptions.failFast,
90+
failFast,
8291
updateSnapshots: runtimeOptions.updateSnapshots
8392
});
8493

8594
runStatus.on('test', restartTimer);
95+
if (failFast) {
96+
// Prevent new test files from running once a test has failed.
97+
runStatus.on('test', test => {
98+
if (test.error) {
99+
bailed = true;
100+
}
101+
});
102+
}
103+
86104
this.emit('test-run', runStatus, files);
87105

88106
// Bail out early if no files were found.
@@ -130,6 +148,12 @@ class Api extends EventEmitter {
130148

131149
// Try and run each file, limited by `concurrency`.
132150
return Bluebird.map(files, file => {
151+
// No new files should be run once a test has timed out or failed,
152+
// and failFast is enabled.
153+
if (bailed) {
154+
return null;
155+
}
156+
133157
let forked;
134158
return Bluebird.resolve(
135159
this._computeForkExecArgv().then(execArgv => {
@@ -157,6 +181,10 @@ class Api extends EventEmitter {
157181

158182
return forked;
159183
}).catch(err => {
184+
// Prevent new test files from running.
185+
if (failFast) {
186+
bailed = true;
187+
}
160188
handleError(Object.assign(err, {file}));
161189
return null;
162190
})

test/api.js

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ test('display filename prefixes for failed test stack traces in subdirs', t => {
202202
});
203203
});
204204

205-
test('fail-fast mode', t => {
205+
test('fail-fast mode - single file', t => {
206206
const api = apiCreator({
207207
failFast: true
208208
});
@@ -218,7 +218,7 @@ test('fail-fast mode', t => {
218218
});
219219
});
220220

221-
return api.run([path.join(__dirname, 'fixture/fail-fast.js')])
221+
return api.run([path.join(__dirname, 'fixture/fail-fast/single-file/test.js')])
222222
.then(result => {
223223
t.ok(api.options.failFast);
224224
t.strictDeepEqual(tests, [{
@@ -233,6 +233,121 @@ test('fail-fast mode', t => {
233233
});
234234
});
235235

236+
test('fail-fast mode - multiple files', t => {
237+
const api = apiCreator({
238+
failFast: true,
239+
serial: true
240+
});
241+
242+
const tests = [];
243+
244+
api.on('test-run', runStatus => {
245+
runStatus.on('test', test => {
246+
tests.push({
247+
ok: !test.error,
248+
title: test.title
249+
});
250+
});
251+
});
252+
253+
return api.run([
254+
path.join(__dirname, 'fixture/fail-fast/multiple-files/fails.js'),
255+
path.join(__dirname, 'fixture/fail-fast/multiple-files/passes.js')
256+
])
257+
.then(result => {
258+
t.ok(api.options.failFast);
259+
t.strictDeepEqual(tests, [{
260+
ok: true,
261+
title: 'fails › first pass'
262+
}, {
263+
ok: false,
264+
title: 'fails › second fail'
265+
}]);
266+
t.is(result.passCount, 1);
267+
t.is(result.failCount, 1);
268+
});
269+
});
270+
271+
test('fail-fast mode - crash', t => {
272+
const api = apiCreator({
273+
failFast: true,
274+
serial: true
275+
});
276+
277+
const tests = [];
278+
const errors = [];
279+
280+
api.on('test-run', runStatus => {
281+
runStatus.on('test', test => {
282+
tests.push({
283+
ok: !test.error,
284+
title: test.title
285+
});
286+
});
287+
runStatus.on('error', err => {
288+
errors.push(err);
289+
});
290+
});
291+
292+
return api.run([
293+
path.join(__dirname, 'fixture/fail-fast/crash/crashes.js'),
294+
path.join(__dirname, 'fixture/fail-fast/crash/passes.js')
295+
])
296+
.then(result => {
297+
t.ok(api.options.failFast);
298+
t.strictDeepEqual(tests, []);
299+
t.is(errors.length, 1);
300+
t.is(errors[0].name, 'AvaError');
301+
t.is(errors[0].message, 'test/fixture/fail-fast/crash/crashes.js exited with a non-zero exit code: 1');
302+
t.is(result.passCount, 0);
303+
t.is(result.failCount, 0);
304+
});
305+
});
306+
307+
test('fail-fast mode - timeout', t => {
308+
const api = apiCreator({
309+
failFast: true,
310+
serial: true,
311+
timeout: '100ms'
312+
});
313+
314+
const tests = [];
315+
const errors = [];
316+
317+
api.on('test-run', runStatus => {
318+
runStatus.on('test', test => {
319+
tests.push({
320+
ok: !test.error,
321+
title: test.title
322+
});
323+
});
324+
runStatus.on('error', err => {
325+
errors.push(err);
326+
});
327+
});
328+
329+
return api.run([
330+
path.join(__dirname, 'fixture/fail-fast/timeout/fails.js'),
331+
path.join(__dirname, 'fixture/fail-fast/timeout/passes.js')
332+
])
333+
.then(result => {
334+
t.ok(api.options.failFast);
335+
if (tests.length > 0) {
336+
t.is(tests.length, 1);
337+
// FIXME: The fails.js test file should have exited without the pending
338+
// test completing, but that's not always the case.
339+
t.is(tests[0].title, 'fails › slow pass');
340+
t.is(result.passCount, 1);
341+
} else {
342+
t.is(result.passCount, 0);
343+
}
344+
t.is(errors.length, 1);
345+
t.is(errors[0].name, 'AvaError');
346+
t.is(errors[0].message, 'Exited because no new tests completed within the last 100ms of inactivity');
347+
t.is(result.failCount, 0);
348+
});
349+
});
350+
236351
test('serial execution mode', t => {
237352
const api = apiCreator({
238353
serial: true
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import '../../../../'; // eslint-disable-line import/no-unassigned-import
2+
3+
process.exit(1); // eslint-disable-line unicorn/no-process-exit
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from '../../../../';
2+
3+
test('first pass', t => {
4+
t.pass();
5+
});

test/fixture/fail-fast.js renamed to test/fixture/fail-fast/multiple-files/fails.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import test from '../../';
1+
import test from '../../../../';
22

33
test('first pass', t => {
44
t.pass();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from '../../../../';
2+
3+
test('first pass', t => {
4+
t.pass();
5+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import test from '../../../../';
2+
3+
test('first pass', t => {
4+
t.pass();
5+
});
6+
7+
test('second fail', t => {
8+
t.fail();
9+
});
10+
11+
test('third pass', t => {
12+
t.pass();
13+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from '../../../../';
2+
3+
test.cb('slow pass', t => {
4+
setTimeout(t.end, 1000);
5+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import test from '../../../../';
2+
3+
test('first pass', t => {
4+
t.pass();
5+
});

0 commit comments

Comments
 (0)