-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Mega PR: unhandledRejection, uncaughtException, reliable IO capture, consistent tests. #206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a73374d
862edb8
c5d02f1
b3bc42a
15bd794
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,11 +45,13 @@ var cli = meow({ | |
|
||
var testCount = 0; | ||
var fileCount = 0; | ||
var unhandledRejectionCount = 0; | ||
var uncaughtExceptionCount = 0; | ||
var errors = []; | ||
|
||
function error(err) { | ||
console.error(err.stack); | ||
process.exit(1); | ||
flushIoAndExit(1); | ||
} | ||
|
||
function prefixTitle(file) { | ||
|
@@ -116,11 +118,24 @@ function run(file) { | |
return fork(args) | ||
.on('stats', stats) | ||
.on('test', test) | ||
.on('unhandledRejections', rejections) | ||
.on('uncaughtException', uncaughtException) | ||
.on('data', function (data) { | ||
process.stdout.write(data); | ||
}); | ||
} | ||
|
||
function rejections(data) { | ||
var unhandled = data.unhandledRejections; | ||
log.unhandledRejections(data.file, unhandled); | ||
unhandledRejectionCount += unhandled.length; | ||
} | ||
|
||
function uncaughtException(data) { | ||
uncaughtExceptionCount++; | ||
log.uncaughtException(data.file, data.uncaughtException); | ||
} | ||
|
||
function sum(arr, key) { | ||
var result = 0; | ||
|
||
|
@@ -145,21 +160,30 @@ function exit(results) { | |
var failed = sum(stats, 'failCount'); | ||
|
||
log.write(); | ||
log.report(passed, failed); | ||
log.report(passed, failed, unhandledRejectionCount, uncaughtExceptionCount); | ||
log.write(); | ||
|
||
if (failed > 0) { | ||
log.errors(flatten(tests)); | ||
} | ||
|
||
process.stdout.write(''); | ||
|
||
flushIoAndExit( | ||
failed > 0 || unhandledRejectionCount > 0 || uncaughtExceptionCount > 0 ? 1 : 0 | ||
); | ||
} | ||
|
||
function flushIoAndExit(code) { | ||
// TODO: figure out why this needs to be here to | ||
// correctly flush the output when multiple test files | ||
process.stdout.write(''); | ||
process.stderr.write(''); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wut, another one? :p There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, there's 3 of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oops - I meant to delete that other one. There should be one each |
||
|
||
// timeout required to correctly flush stderr on Node 0.10 Windows | ||
// timeout required to correctly flush io on Node 0.10 Windows | ||
setTimeout(function () { | ||
process.exit(failed > 0 ? 1 : 0); | ||
}, 0); | ||
process.exit(code); | ||
}, process.env.AVA_APPVEYOR ? 500 : 0); | ||
} | ||
|
||
function init(files) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
'use strict'; | ||
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 serializeValue = require('./serialize-value'); | ||
|
||
var hasGenerators = parseInt(process.version.slice(1), 10) > 0; | ||
var testPath = process.argv[2]; | ||
|
@@ -32,20 +34,46 @@ 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 | ||
}); | ||
|
||
if (!avaRequired) { | ||
console.error('No tests found in ' + testPath + ', make sure to import "ava" at the top of your test file'); | ||
setImmediate(function () { | ||
process.exit(1); | ||
}); | ||
throw new Error('No tests found in ' + testPath + ', make sure to import "ava" at the top of your test file'); | ||
} | ||
|
||
process.on('message', function (message) { | ||
if (message['ava-kill-command']) { | ||
var command = message['ava-child-process-command']; | ||
if (command) { | ||
process.emit('ava-' + command, message.data); | ||
} | ||
}); | ||
|
||
process.on('ava-kill', function () { | ||
setTimeout(function () { | ||
process.exit(0); | ||
}, process.env.AVA_APPVEYOR ? 100 : 0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AppVeyor doesn't set env variables like Travis to identify itself? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it does. http://www.appveyor.com/docs/environment-variables There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great, in that case we don't need our own There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @vdemedes I think the intention is to only do it when we run AVA's tests on AppVeyor, not when the user run their tests there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sindresorhus Hmm, don't those fixes also needed for user's tests? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sindresorhus got the intent right. The only tests this affects is ones that parse the stdout / stderr from our CLI. They will still always get the correct exit code - they might just be missing the last "1 test failed". |
||
}); | ||
|
||
process.on('ava-cleanup', function () { | ||
var unhandled = loudRejection.currentlyUnhandled(); | ||
if (unhandled.length) { | ||
unhandled = unhandled.map(function (entry) { | ||
return serializeValue(entry.reason); | ||
}); | ||
send('unhandledRejections', {unhandledRejections: unhandled}); | ||
} | ||
|
||
setTimeout(function () { | ||
send('cleaned-up', {}); | ||
}, 100); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,10 @@ module.exports = function (args) { | |
|
||
var ps = childProcess.fork(babel, args, options); | ||
|
||
function send(command, data) { | ||
ps.send({'ava-child-process-command': command, 'data': data}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can just be:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My only thought there was wether or not we wanted to monkeypatch I can imagine a scenario where you are writing some code that might be a child process (or might be top level), and so they would will listen to None of that is implemented. Just leaving the door open. |
||
} | ||
|
||
var promise = new Promise(function (resolve, reject) { | ||
var testResults; | ||
|
||
|
@@ -26,7 +30,15 @@ module.exports = function (args) { | |
|
||
// after all tests are finished and results received | ||
// kill the forked process, so AVA can exit safely | ||
ps.send({'ava-kill-command': true}); | ||
send('cleanup', true); | ||
}); | ||
|
||
ps.on('cleaned-up', function () { | ||
send('kill', true); | ||
}); | ||
|
||
ps.on('uncaughtException', function () { | ||
send('cleanup', true); | ||
}); | ||
|
||
ps.on('error', reject); | ||
|
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; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
const test = require('../../'); | ||
|
||
test('creates an unhandled rejection', t => { | ||
Promise.reject(new Error(`You can't handle this!`)); | ||
|
||
setTimeout(function () { | ||
t.end(); | ||
}); | ||
}); |
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 () {}; | ||
}); | ||
}); |
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 | ||
}); | ||
}); |
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!`) | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1058,14 +1058,50 @@ 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(); | ||
}); | ||
}); | ||
|
||
test('Unhandled promises will be reported to console', function (t) { | ||
execCli('fixture/loud-rejection.js', function (err, stdout, stderr) { | ||
t.ok(err); | ||
t.ok(/You can't handle this/.test(stderr)); | ||
t.ok(/1 unhandled rejection[^s]/.test(stderr)); | ||
t.end(); | ||
}); | ||
}); | ||
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For TODO's I prefer to include the username so we know who added it without having to git blame.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cool, I'm assuming you took care of it in the merge? Or do you want me to go handle it now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll do it. |
||
// 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); | ||
|
||
|
@@ -1091,9 +1127,9 @@ test('titles of both passing and failing tests and AssertionErrors are displayed | |
test('empty test files creates a failure with a helpful warning', function (t) { | ||
t.plan(2); | ||
|
||
execCli('fixture/empty.js', function (err, stdout) { | ||
execCli('fixture/empty.js', function (err, stdout, stderr) { | ||
t.ok(err); | ||
t.ok(/No tests found.*?import "ava"/.test(stdout)); | ||
t.ok(/No tests found.*?import "ava"/.test(stderr)); | ||
t.end(); | ||
}); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about just:
timeout 30 && npm install
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this tries to reinstall after 30 seconds if the first npm install fails (which it does about every 10 times or so on AppVeyor).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this timeout shorter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chose 30 seconds pretty arbitrarily, so I"m not opposed to shortening it and seeing what happens.
Why do you want it shorter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it matters to be honest. AppVeyor is sooooo slow regardless. But we can try with 10 seconds to see if it still works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jamestalmage Probably because it's annoying to always have to wait for AppVeyor before merging a PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So that tests are executed sooner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. That.
I got a lot of TV watched this weekend. Push a commit, watch 20 minutes... Still broken... Push a commit, watch 20 minutes...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha :p
Sounds like me this weekend. I was bumping dependencies on a gadzillion modules on a super slow connection.
$ david update
, wait 10 minutes, commit, push,npm publish
, wait 5 minutes, then next module...