Skip to content

Commit c5d02f1

Browse files
committed
report uncaught exceptions
1 parent 862edb8 commit c5d02f1

File tree

11 files changed

+113
-30
lines changed

11 files changed

+113
-30
lines changed

cli.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ var cli = meow({
4646
var testCount = 0;
4747
var fileCount = 0;
4848
var unhandledRejectionCount = 0;
49+
var uncaughtExceptionCount = 0;
4950
var errors = [];
5051

5152
function error(err) {
@@ -118,6 +119,7 @@ function run(file) {
118119
.on('stats', stats)
119120
.on('test', test)
120121
.on('unhandledRejections', rejections)
122+
.on('uncaughtException', uncaughtException)
121123
.on('data', function (data) {
122124
process.stdout.write(data);
123125
});
@@ -129,6 +131,11 @@ function rejections(data) {
129131
unhandledRejectionCount += unhandled.length;
130132
}
131133

134+
function uncaughtException(data) {
135+
uncaughtExceptionCount++;
136+
log.uncaughtException(data.file, data.uncaughtException);
137+
}
138+
132139
function sum(arr, key) {
133140
var result = 0;
134141

@@ -153,7 +160,7 @@ function exit(results) {
153160
var failed = sum(stats, 'failCount');
154161

155162
log.write();
156-
log.report(passed, failed, unhandledRejectionCount);
163+
log.report(passed, failed, unhandledRejectionCount, uncaughtExceptionCount);
157164
log.write();
158165

159166
if (failed > 0) {
@@ -166,7 +173,7 @@ function exit(results) {
166173

167174
// timeout required to correctly flush stderr on Node 0.10 Windows
168175
setTimeout(function () {
169-
process.exit(failed > 0 || unhandledRejectionCount > 0 ? 1 : 0);
176+
process.exit(failed > 0 || unhandledRejectionCount > 0 || uncaughtExceptionCount > 0 ? 1 : 0);
170177
}, 0);
171178
}
172179

index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ var setImmediate = require('set-immediate-shim');
44
var hasFlag = require('has-flag');
55
var chalk = require('chalk');
66
var relative = require('path').relative;
7-
var serializeError = require('destroy-circular');
7+
var serializeError = require('./lib/serialize-value');
88
var Runner = require('./lib/runner');
99
var log = require('./lib/logger');
1010
var runner = new Runner();

lib/babel.js

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ var loudRejection = require('loud-rejection/api')(process);
33
var resolveFrom = require('resolve-from');
44
var createEspowerPlugin = require('babel-plugin-espower/create');
55
var requireFromString = require('require-from-string');
6-
var destroyCircular = require('destroy-circular');
6+
var serializeValue = require('./serialize-value');
77

88
var hasGenerators = parseInt(process.version.slice(1), 10) > 0;
99
var testPath = process.argv[2];
@@ -34,6 +34,14 @@ module.exports = {
3434
}
3535
};
3636

37+
function send(name, data) {
38+
process.send({name: name, data: data});
39+
}
40+
41+
process.on('uncaughtException', function (exception) {
42+
send('uncaughtException', {uncaughtException: serializeValue(exception)});
43+
});
44+
3745
var transpiled = babel.transformFileSync(testPath, options);
3846
requireFromString(transpiled.code, testPath, {
3947
appendPaths: module.paths
@@ -61,27 +69,12 @@ process.on('ava-cleanup', function () {
6169
var unhandled = loudRejection.currentlyUnhandled();
6270
if (unhandled.length) {
6371
unhandled = unhandled.map(function (entry) {
64-
var err = entry.reason;
65-
if (typeof err === 'object') {
66-
return destroyCircular(err);
67-
}
68-
if (typeof err === 'function') {
69-
return '[Function ' + err.name + ']';
70-
}
71-
return err;
72-
});
73-
process.send({
74-
name: 'unhandledRejections',
75-
data: {
76-
unhandledRejections: unhandled
77-
}
72+
return serializeValue(entry.reason);
7873
});
74+
send('unhandledRejections', {unhandledRejections: unhandled});
7975
}
8076

8177
setTimeout(function () {
82-
process.send({
83-
name: 'cleaned-up',
84-
data: {}
85-
});
78+
send('cleaned-up', {});
8679
}, 100);
8780
});

lib/fork.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ module.exports = function (args) {
3737
send('kill', true);
3838
});
3939

40+
ps.on('uncaughtException', function () {
41+
send('cleanup', true);
42+
});
43+
4044
ps.on('error', reject);
4145

4246
ps.on('exit', function (code) {

lib/logger.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ x.errors = function (results) {
7070
});
7171
};
7272

73-
x.report = function (passed, failed, unhandled) {
73+
x.report = function (passed, failed, unhandled, uncaught) {
7474
if (failed > 0) {
7575
log.writelpad(chalk.red(failed, plur('test', failed), 'failed'));
7676
} else {
@@ -79,6 +79,9 @@ x.report = function (passed, failed, unhandled) {
7979
if (unhandled > 0) {
8080
log.writelpad(chalk.red(unhandled, 'unhandled', plur('rejection', unhandled)));
8181
}
82+
if (uncaught > 0) {
83+
log.writelpad(chalk.red(uncaught, 'uncaught', plur('exception', uncaught)));
84+
}
8285
};
8386

8487
x.unhandledRejections = function (file, rejections) {
@@ -95,3 +98,13 @@ x.unhandledRejections = function (file, rejections) {
9598
log.write();
9699
});
97100
};
101+
102+
x.uncaughtException = function (file, error) {
103+
log.write(chalk.red('Uncaught Exception: ', file));
104+
if (error.stack) {
105+
log.writelpad(chalk.red(beautifyStack(error.stack)));
106+
} else {
107+
log.writelpad(chalk.red(JSON.stringify(error)));
108+
}
109+
log.write();
110+
};

lib/serialize-value.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
var destroyCircular = require('destroy-circular');
3+
4+
// Make a value ready for JSON.stringify() / process.send()
5+
6+
module.exports = function serializeValue(value) {
7+
if (typeof value === 'object') {
8+
return destroyCircular(value);
9+
}
10+
if (typeof value === 'function') {
11+
// JSON.stringify discards functions, leaving no context information once we serialize and send across.
12+
// We replace thrown functions with a string to provide as much information to the user as possible.
13+
return '[Function: ' + (value.name || 'anonymous') + ']';
14+
}
15+
return value;
16+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const test = require('../../');
2+
3+
test('throw an uncaught exception', t => {
4+
setImmediate(() => {
5+
throw function () {};
6+
});
7+
});

test/fixture/throw-named-function.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const test = require('../../');
2+
3+
function fooFn() {}
4+
5+
test('throw an uncaught exception', t => {
6+
setImmediate(() => {
7+
throw fooFn
8+
});
9+
});

test/fixture/uncaught-exception.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const test = require('../../');
2+
3+
test('throw an uncaught exception', t => {
4+
setImmediate(() => {
5+
throw new Error(`Can't catch me!`)
6+
});
7+
});

test/fork.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ test('resolves promise with tests info', function (t) {
2828
});
2929

3030
test('rejects on error and streams output', function (t) {
31-
var buffer = '';
32-
31+
t.plan(2);
3332
fork(fixture('broken.js'))
34-
.on('data', function (data) {
35-
buffer += data;
33+
.on('uncaughtException', function (data) {
34+
var exception = data.uncaughtException;
35+
t.ok(/no such file or directory/.test(exception.message));
3636
})
3737
.catch(function () {
38-
t.ok(/no such file or directory/.test(buffer));
38+
t.pass();
3939
t.end();
4040
});
4141
});

0 commit comments

Comments
 (0)