Skip to content

Commit

Permalink
report uncaught exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
jamestalmage committed Nov 16, 2015
1 parent 862edb8 commit c5d02f1
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 30 deletions.
11 changes: 9 additions & 2 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ var cli = meow({
var testCount = 0;
var fileCount = 0;
var unhandledRejectionCount = 0;
var uncaughtExceptionCount = 0;
var errors = [];

function error(err) {
Expand Down Expand Up @@ -118,6 +119,7 @@ function run(file) {
.on('stats', stats)
.on('test', test)
.on('unhandledRejections', rejections)
.on('uncaughtException', uncaughtException)
.on('data', function (data) {
process.stdout.write(data);
});
Expand All @@ -129,6 +131,11 @@ function rejections(data) {
unhandledRejectionCount += unhandled.length;
}

function uncaughtException(data) {
uncaughtExceptionCount++;
log.uncaughtException(data.file, data.uncaughtException);
}

function sum(arr, key) {
var result = 0;

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

log.write();
log.report(passed, failed, unhandledRejectionCount);
log.report(passed, failed, unhandledRejectionCount, uncaughtExceptionCount);
log.write();

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

// timeout required to correctly flush stderr on Node 0.10 Windows
setTimeout(function () {
process.exit(failed > 0 || unhandledRejectionCount > 0 ? 1 : 0);
process.exit(failed > 0 || unhandledRejectionCount > 0 || uncaughtExceptionCount > 0 ? 1 : 0);
}, 0);
}

Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ var setImmediate = require('set-immediate-shim');
var hasFlag = require('has-flag');
var chalk = require('chalk');
var relative = require('path').relative;
var serializeError = require('destroy-circular');
var serializeError = require('./lib/serialize-value');
var Runner = require('./lib/runner');
var log = require('./lib/logger');
var runner = new Runner();
Expand Down
31 changes: 12 additions & 19 deletions lib/babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ var loudRejection = require('loud-rejection/api')(process);
var resolveFrom = require('resolve-from');
var createEspowerPlugin = require('babel-plugin-espower/create');
var requireFromString = require('require-from-string');
var destroyCircular = require('destroy-circular');
var serializeValue = require('./serialize-value');

var hasGenerators = parseInt(process.version.slice(1), 10) > 0;
var testPath = process.argv[2];
Expand Down Expand Up @@ -34,6 +34,14 @@ module.exports = {
}
};

function send(name, data) {
process.send({name: name, data: data});
}

process.on('uncaughtException', function (exception) {
send('uncaughtException', {uncaughtException: serializeValue(exception)});
});

var transpiled = babel.transformFileSync(testPath, options);
requireFromString(transpiled.code, testPath, {
appendPaths: module.paths
Expand Down Expand Up @@ -61,27 +69,12 @@ process.on('ava-cleanup', function () {
var unhandled = loudRejection.currentlyUnhandled();
if (unhandled.length) {
unhandled = unhandled.map(function (entry) {
var err = entry.reason;
if (typeof err === 'object') {
return destroyCircular(err);
}
if (typeof err === 'function') {
return '[Function ' + err.name + ']';
}
return err;
});
process.send({
name: 'unhandledRejections',
data: {
unhandledRejections: unhandled
}
return serializeValue(entry.reason);
});
send('unhandledRejections', {unhandledRejections: unhandled});
}

setTimeout(function () {
process.send({
name: 'cleaned-up',
data: {}
});
send('cleaned-up', {});
}, 100);
});
4 changes: 4 additions & 0 deletions lib/fork.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ module.exports = function (args) {
send('kill', true);
});

ps.on('uncaughtException', function () {
send('cleanup', true);
});

ps.on('error', reject);

ps.on('exit', function (code) {
Expand Down
15 changes: 14 additions & 1 deletion lib/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ x.errors = function (results) {
});
};

x.report = function (passed, failed, unhandled) {
x.report = function (passed, failed, unhandled, uncaught) {
if (failed > 0) {
log.writelpad(chalk.red(failed, plur('test', failed), 'failed'));
} else {
Expand All @@ -79,6 +79,9 @@ x.report = function (passed, failed, unhandled) {
if (unhandled > 0) {
log.writelpad(chalk.red(unhandled, 'unhandled', plur('rejection', unhandled)));
}
if (uncaught > 0) {
log.writelpad(chalk.red(uncaught, 'uncaught', plur('exception', uncaught)));
}
};

x.unhandledRejections = function (file, rejections) {
Expand All @@ -95,3 +98,13 @@ x.unhandledRejections = function (file, rejections) {
log.write();
});
};

x.uncaughtException = function (file, error) {
log.write(chalk.red('Uncaught Exception: ', file));
if (error.stack) {
log.writelpad(chalk.red(beautifyStack(error.stack)));
} else {
log.writelpad(chalk.red(JSON.stringify(error)));
}
log.write();
};
16 changes: 16 additions & 0 deletions lib/serialize-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';
var destroyCircular = require('destroy-circular');

// Make a value ready for JSON.stringify() / process.send()

module.exports = function serializeValue(value) {
if (typeof value === 'object') {
return destroyCircular(value);
}
if (typeof value === 'function') {
// JSON.stringify discards functions, leaving no context information once we serialize and send across.
// We replace thrown functions with a string to provide as much information to the user as possible.
return '[Function: ' + (value.name || 'anonymous') + ']';
}
return value;
};
7 changes: 7 additions & 0 deletions test/fixture/throw-anonymous-function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const test = require('../../');

test('throw an uncaught exception', t => {
setImmediate(() => {
throw function () {};
});
});
9 changes: 9 additions & 0 deletions test/fixture/throw-named-function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const test = require('../../');

function fooFn() {}

test('throw an uncaught exception', t => {
setImmediate(() => {
throw fooFn
});
});
7 changes: 7 additions & 0 deletions test/fixture/uncaught-exception.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const test = require('../../');

test('throw an uncaught exception', t => {
setImmediate(() => {
throw new Error(`Can't catch me!`)
});
});
10 changes: 5 additions & 5 deletions test/fork.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ test('resolves promise with tests info', function (t) {
});

test('rejects on error and streams output', function (t) {
var buffer = '';

t.plan(2);
fork(fixture('broken.js'))
.on('data', function (data) {
buffer += data;
.on('uncaughtException', function (data) {
var exception = data.uncaughtException;
t.ok(/no such file or directory/.test(exception.message));
})
.catch(function () {
t.ok(/no such file or directory/.test(buffer));
t.pass();
t.end();
});
});
Expand Down
31 changes: 29 additions & 2 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1058,8 +1058,7 @@ test('change process.cwd() to a test\'s directory', function (t) {

test('Babel require hook only applies to the test file', function (t) {
execCli('fixture/babel-hook.js', function (err, stdout, stderr) {
t.ok(/exited with a non-zero exit code/.test(stderr));
t.ok(/Unexpected token/.test(stdout));
t.ok(/Unexpected token/.test(stderr));
t.ok(err);
t.is(err.code, 1);
t.end();
Expand All @@ -1075,6 +1074,34 @@ test('Unhandled promises will be reported to console', function (t) {
});
});

test('uncaught exception will be reported to console', function (t) {
execCli('fixture/uncaught-exception.js', function (err, stdout, stderr) {
t.ok(err);
t.ok(/Can't catch me!/.test(stderr));
// TODO: This should get printed, but we reject the promise (ending all tests) instead of just ending that one test and reporting.
// t.ok(/1 uncaught exception[^s]/.test(stdout));
t.end();
});
});

test('throwing a named function will report the to the console', function (t) {
execCli('fixture/throw-named-function.js', function (err, stdout, stderr) {
t.ok(err);
t.ok(/\[Function: fooFn]/.test(stderr));
// t.ok(/1 uncaught exception[^s]/.test(stdout));
t.end();
});
});

test('throwing a anonymous function will report the function to the console', function (t) {
execCli('fixture/throw-anonymous-function.js', function (err, stdout, stderr) {
t.ok(err);
t.ok(/\[Function: anonymous]/.test(stderr));
// t.ok(/1 uncaught exception[^s]/.test(stdout));
t.end();
});
});

test('absolute paths in CLI', function (t) {
t.plan(2);

Expand Down

0 comments on commit c5d02f1

Please sign in to comment.