Skip to content

Commit b24b2ea

Browse files
committed
Merge pull request #206 from jamestalmage/use-loud-rejection
Mega PR: unhandledRejection, uncaughtException, reliable IO capture, consistent tests.
2 parents 78a6c56 + 15bd794 commit b24b2ea

File tree

14 files changed

+206
-25
lines changed

14 files changed

+206
-25
lines changed

appveyor.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ environment:
77
install:
88
- ps: Install-Product node $env:nodejs_version
99
- set CI=true
10-
- npm -g install npm@latest
10+
- set AVA_APPVEYOR=true
11+
- npm -g install npm@latest || (timeout 30 && npm -g install npm@latest)
1112
- set PATH=%APPDATA%\npm;%PATH%
12-
- npm install
13+
- npm install || (timeout 30 && npm install)
1314
matrix:
1415
fast_finish: true
1516
build: off
@@ -19,4 +20,4 @@ clone_depth: 1
1920
test_script:
2021
- node --version
2122
- npm --version
22-
- npm run test-win
23+
- npm run test-win || (timeout 30 && npm run test-win)

cli.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ var cli = meow({
4545

4646
var testCount = 0;
4747
var fileCount = 0;
48+
var unhandledRejectionCount = 0;
49+
var uncaughtExceptionCount = 0;
4850
var errors = [];
4951

5052
function error(err) {
5153
console.error(err.stack);
52-
process.exit(1);
54+
flushIoAndExit(1);
5355
}
5456

5557
function prefixTitle(file) {
@@ -116,11 +118,24 @@ function run(file) {
116118
return fork(args)
117119
.on('stats', stats)
118120
.on('test', test)
121+
.on('unhandledRejections', rejections)
122+
.on('uncaughtException', uncaughtException)
119123
.on('data', function (data) {
120124
process.stdout.write(data);
121125
});
122126
}
123127

128+
function rejections(data) {
129+
var unhandled = data.unhandledRejections;
130+
log.unhandledRejections(data.file, unhandled);
131+
unhandledRejectionCount += unhandled.length;
132+
}
133+
134+
function uncaughtException(data) {
135+
uncaughtExceptionCount++;
136+
log.uncaughtException(data.file, data.uncaughtException);
137+
}
138+
124139
function sum(arr, key) {
125140
var result = 0;
126141

@@ -145,21 +160,30 @@ function exit(results) {
145160
var failed = sum(stats, 'failCount');
146161

147162
log.write();
148-
log.report(passed, failed);
163+
log.report(passed, failed, unhandledRejectionCount, uncaughtExceptionCount);
149164
log.write();
150165

151166
if (failed > 0) {
152167
log.errors(flatten(tests));
153168
}
154169

170+
process.stdout.write('');
171+
172+
flushIoAndExit(
173+
failed > 0 || unhandledRejectionCount > 0 || uncaughtExceptionCount > 0 ? 1 : 0
174+
);
175+
}
176+
177+
function flushIoAndExit(code) {
155178
// TODO: figure out why this needs to be here to
156179
// correctly flush the output when multiple test files
157180
process.stdout.write('');
181+
process.stderr.write('');
158182

159-
// timeout required to correctly flush stderr on Node 0.10 Windows
183+
// timeout required to correctly flush io on Node 0.10 Windows
160184
setTimeout(function () {
161-
process.exit(failed > 0 ? 1 : 0);
162-
}, 0);
185+
process.exit(code);
186+
}, process.env.AVA_APPVEYOR ? 500 : 0);
163187
}
164188

165189
function init(files) {

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: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
'use strict';
2+
var loudRejection = require('loud-rejection/api')(process);
23
var resolveFrom = require('resolve-from');
34
var createEspowerPlugin = require('babel-plugin-espower/create');
45
var requireFromString = require('require-from-string');
6+
var serializeValue = require('./serialize-value');
57

68
var hasGenerators = parseInt(process.version.slice(1), 10) > 0;
79
var testPath = process.argv[2];
@@ -32,20 +34,46 @@ module.exports = {
3234
}
3335
};
3436

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+
3545
var transpiled = babel.transformFileSync(testPath, options);
3646
requireFromString(transpiled.code, testPath, {
3747
appendPaths: module.paths
3848
});
3949

4050
if (!avaRequired) {
41-
console.error('No tests found in ' + testPath + ', make sure to import "ava" at the top of your test file');
42-
setImmediate(function () {
43-
process.exit(1);
44-
});
51+
throw new Error('No tests found in ' + testPath + ', make sure to import "ava" at the top of your test file');
4552
}
4653

4754
process.on('message', function (message) {
48-
if (message['ava-kill-command']) {
55+
var command = message['ava-child-process-command'];
56+
if (command) {
57+
process.emit('ava-' + command, message.data);
58+
}
59+
});
60+
61+
process.on('ava-kill', function () {
62+
setTimeout(function () {
4963
process.exit(0);
64+
}, process.env.AVA_APPVEYOR ? 100 : 0);
65+
});
66+
67+
process.on('ava-cleanup', function () {
68+
var unhandled = loudRejection.currentlyUnhandled();
69+
if (unhandled.length) {
70+
unhandled = unhandled.map(function (entry) {
71+
return serializeValue(entry.reason);
72+
});
73+
send('unhandledRejections', {unhandledRejections: unhandled});
5074
}
75+
76+
setTimeout(function () {
77+
send('cleaned-up', {});
78+
}, 100);
5179
});

lib/fork.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ module.exports = function (args) {
1818

1919
var ps = childProcess.fork(babel, args, options);
2020

21+
function send(command, data) {
22+
ps.send({'ava-child-process-command': command, 'data': data});
23+
}
24+
2125
var promise = new Promise(function (resolve, reject) {
2226
var testResults;
2327

@@ -26,7 +30,15 @@ module.exports = function (args) {
2630

2731
// after all tests are finished and results received
2832
// kill the forked process, so AVA can exit safely
29-
ps.send({'ava-kill-command': true});
33+
send('cleanup', true);
34+
});
35+
36+
ps.on('cleaned-up', function () {
37+
send('kill', true);
38+
});
39+
40+
ps.on('uncaughtException', function () {
41+
send('cleanup', true);
3042
});
3143

3244
ps.on('error', reject);

lib/logger.js

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

73-
x.report = function (passed, failed) {
73+
x.report = function (passed, failed, unhandled, uncaught) {
7474
if (failed > 0) {
7575
log.writelpad(chalk.red(failed, plur('test', failed), 'failed'));
7676
} else {
7777
log.writelpad(chalk.green(passed, plur('test', passed), 'passed'));
7878
}
79+
if (unhandled > 0) {
80+
log.writelpad(chalk.red(unhandled, 'unhandled', plur('rejection', unhandled)));
81+
}
82+
if (uncaught > 0) {
83+
log.writelpad(chalk.red(uncaught, 'uncaught', plur('exception', uncaught)));
84+
}
85+
};
86+
87+
x.unhandledRejections = function (file, rejections) {
88+
if (!(rejections && rejections.length)) {
89+
return;
90+
}
91+
rejections.forEach(function (rejection) {
92+
log.write(chalk.red('Unhandled Rejection: ', file));
93+
if (rejection.stack) {
94+
log.writelpad(chalk.red(beautifyStack(rejection.stack)));
95+
} else {
96+
log.writelpad(chalk.red(JSON.stringify(rejection)));
97+
}
98+
log.write();
99+
});
100+
};
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();
79110
};

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+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"globby": "^3.0.1",
7878
"has-flag": "^1.0.0",
7979
"is-generator-fn": "^1.0.0",
80+
"loud-rejection": "^1.2.0",
8081
"max-timeout": "^1.0.0",
8182
"meow": "^3.3.0",
8283
"plur": "^2.0.0",

test/fixture/loud-rejection.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+
test('creates an unhandled rejection', t => {
4+
Promise.reject(new Error(`You can't handle this!`));
5+
6+
setTimeout(function () {
7+
t.end();
8+
});
9+
});
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+
});

0 commit comments

Comments
 (0)