Skip to content

Commit 9cea60d

Browse files
novemberbornsindresorhus
authored andcommitted
Ensure test run failures crash worker (#1265)
* Ensure Test#run() returns exit promise This allows errors from the exit logic to propagate to the runner. * Treat runner errors as uncaught exceptions Errors that occur inside the runner are treated as uncaught exceptions. This prevents them from being swallowed in the promise chain, causing the test to hang. * fixup! Treat runner errors as uncaught exceptions * fixup! Treat runner errors as uncaught exceptions
1 parent f3b60f4 commit 9cea60d

File tree

8 files changed

+80
-10
lines changed

8 files changed

+80
-10
lines changed

lib/main.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ globals.setImmediate(() => {
7070
runner.on('test', test);
7171

7272
process.on('ava-run', options => {
73-
runner.run(options).then(exit);
73+
runner.run(options)
74+
.then(exit)
75+
.catch(err => {
76+
process.emit('uncaughtException', err);
77+
});
7478
});
7579

7680
process.on('ava-init-exit', () => {

lib/test-worker.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,20 @@ process.on('unhandledRejection', throwsHelper);
3636

3737
process.on('uncaughtException', exception => {
3838
throwsHelper(exception);
39-
send('uncaughtException', {exception: serializeError(exception)});
39+
40+
let serialized;
41+
try {
42+
serialized = serializeError(exception);
43+
} catch (ignore) { // eslint-disable-line unicorn/catch-error-name
44+
// Avoid using serializeError
45+
const err = new Error('Failed to serialize uncaught exception');
46+
serialized = {
47+
name: err.name,
48+
message: err.message,
49+
stack: err.stack
50+
};
51+
}
52+
send('uncaughtException', {exception: serialized});
4053
});
4154

4255
// If AVA was not required, show an error

lib/test.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,9 @@ class Test {
240240
this._setAssertError(new Error(`Do not return ${asyncType} from tests declared via \`test.cb(...)\`, if you want to return a promise simply declare the test via \`test(...)\``));
241241
}
242242

243-
ret.then(
244-
() => {
245-
this.exit();
246-
},
243+
// Convert to a Bluebird promise
244+
return Promise.resolve(ret).then(
245+
() => this.exit(),
247246
err => {
248247
if (!(err instanceof Error)) {
249248
err = new assert.AssertionError({
@@ -254,10 +253,9 @@ class Test {
254253
}
255254

256255
this._setAssertError(err);
257-
this.exit();
258-
});
259-
260-
return this.promise().promise;
256+
return this.exit();
257+
}
258+
);
261259
}
262260

263261
if (this.metadata.callback && !this.threwSync) {

test/cli.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,3 +412,17 @@ test('workers ensure test files load the same version of ava', t => {
412412
t.end();
413413
});
414414
});
415+
416+
test('worker errors are treated as uncaught exceptions', t => {
417+
execCli(['--no-color', '--verbose', 'test.js'], {dirname: 'fixture/trigger-worker-exception'}, (_, __, stderr) => {
418+
t.match(stderr, /Forced error/);
419+
t.end();
420+
});
421+
});
422+
423+
test('uncaught exceptions are raised for worker errors even if the error cannot be serialized', t => {
424+
execCli(['--no-color', '--verbose', 'test-fallback.js'], {dirname: 'fixture/trigger-worker-exception'}, (_, __, stderr) => {
425+
t.match(stderr, /Failed to serialize uncaught exception/);
426+
t.end();
427+
});
428+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
require('../../../lib/serialize-error');
4+
5+
const serializeModule = require.cache[require.resolve('../../../lib/serialize-error')];
6+
7+
const original = serializeModule.exports;
8+
let restored = false;
9+
let restoreAfterFirstCall = false;
10+
serializeModule.exports = error => {
11+
if (restored) {
12+
return original(error);
13+
}
14+
if (restoreAfterFirstCall) {
15+
restored = true;
16+
}
17+
18+
throw new Error('Forced error');
19+
};
20+
21+
exports.restoreAfterFirstCall = () => {
22+
restoreAfterFirstCall = true;
23+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"ava": {
3+
"require": "./hack.js"
4+
}
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(async () => {
4+
throw new Error('Hi :)');
5+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import test from '../../../';
2+
3+
import { restoreAfterFirstCall } from './hack';
4+
restoreAfterFirstCall();
5+
6+
test(async () => {
7+
throw new Error('Hi :)');
8+
});

0 commit comments

Comments
 (0)