diff --git a/cli.js b/cli.js index 5da18e31d..e0e79e611 100755 --- a/cli.js +++ b/cli.js @@ -43,10 +43,10 @@ var cli = meow({ ] }); +var rejectionCount = 0; +var exceptionCount = 0; var testCount = 0; var fileCount = 0; -var unhandledRejectionCount = 0; -var uncaughtExceptionCount = 0; var errors = []; function error(err) { @@ -120,22 +120,23 @@ function run(file) { return fork(args) .on('stats', stats) .on('test', test) - .on('unhandledRejections', rejections) - .on('uncaughtException', uncaughtException) + .on('unhandledRejections', handleRejections) + .on('uncaughtException', handleExceptions) .on('data', function (data) { process.stdout.write(data); }); } -function rejections(data) { - var unhandled = data.unhandledRejections; - log.unhandledRejections(data.file, unhandled); - unhandledRejectionCount += unhandled.length; +function handleRejections(data) { + log.unhandledRejections(data.file, data.rejections); + + rejectionCount += data.rejections.length; } -function uncaughtException(data) { - uncaughtExceptionCount++; - log.uncaughtException(data.file, data.uncaughtException); +function handleExceptions(data) { + log.uncaughtException(data.file, data.exception); + + exceptionCount++; } function sum(arr, key) { @@ -162,7 +163,7 @@ function exit(results) { var failed = sum(stats, 'failCount'); log.write(); - log.report(passed, failed, unhandledRejectionCount, uncaughtExceptionCount); + log.report(passed, failed, rejectionCount, exceptionCount); log.write(); if (failed > 0) { @@ -172,7 +173,7 @@ function exit(results) { process.stdout.write(''); flushIoAndExit( - failed > 0 || unhandledRejectionCount > 0 || uncaughtExceptionCount > 0 ? 1 : 0 + failed > 0 || rejectionCount > 0 || exceptionCount > 0 ? 1 : 0 ); } diff --git a/index.js b/index.js index 8484e43e4..13139a723 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,18 @@ 'use strict'; -require('./lib/babel').avaRequired(); var setImmediate = require('set-immediate-shim'); +var relative = require('path').relative; var hasFlag = require('has-flag'); var chalk = require('chalk'); -var relative = require('path').relative; var serializeError = require('./lib/serialize-value'); var Runner = require('./lib/runner'); +var send = require('./lib/send'); var log = require('./lib/logger'); + var runner = new Runner(); +// note that test files have require('ava') +require('./lib/babel').avaRequired = true; + // check if the test is being run without AVA cli var isForked = typeof process.send === 'function'; @@ -39,10 +43,7 @@ function test(props) { props.error = props.error ? serializeError(props.error) : {}; - process.send({ - name: 'test', - data: props - }); + send('test', props); if (props.error && hasFlag('fail-fast')) { isFailed = true; @@ -58,12 +59,9 @@ function exit() { } }); - process.send({ - name: 'results', - data: { - stats: runner.stats, - tests: runner.results - } + send('results', { + stats: runner.stats, + tests: runner.results }); } diff --git a/lib/babel.js b/lib/babel.js index 0f6eba818..dd68e7922 100644 --- a/lib/babel.js +++ b/lib/babel.js @@ -1,12 +1,16 @@ '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 loudRejection = require('loud-rejection/api')(process); +var resolveFrom = require('resolve-from'); var serializeValue = require('./serialize-value'); +var send = require('./send'); +// if node's major version part is >= 1, generators are supported var hasGenerators = parseInt(process.version.slice(1), 10) > 0; var testPath = process.argv[2]; + +// include local babel and fallback to ava's babel var babel; try { @@ -16,64 +20,69 @@ try { babel = require('babel-core'); } +// initialize power-assert +var powerAssert = createEspowerPlugin(babel, { + patterns: require('./enhance-assert').PATTERNS +}); + +// if generators are not supported, use regenerator var options = { blacklist: hasGenerators ? ['regenerator'] : [], optional: hasGenerators ? ['asyncToGenerator', 'runtime'] : ['runtime'], - plugins: [ - createEspowerPlugin(babel, { - patterns: require('./enhance-assert').PATTERNS - }) - ] + plugins: [powerAssert] }; -var avaRequired; - -module.exports = { - avaRequired: function () { - avaRequired = true; - } -}; - -function send(name, data) { - process.send({name: name, data: data}); -} +// check if test files required ava and show error, when they didn't +exports.avaRequired = false; process.on('uncaughtException', function (exception) { - send('uncaughtException', {uncaughtException: serializeValue(exception)}); + send('uncaughtException', {exception: serializeValue(exception)}); }); +// include test file var transpiled = babel.transformFileSync(testPath, options); requireFromString(transpiled.code, testPath, { appendPaths: module.paths }); -if (!avaRequired) { +// if ava was not required, show an error +if (!exports.avaRequired) { throw new Error('No tests found in ' + testPath + ', make sure to import "ava" at the top of your test file'); } +// parse and re-emit ava messages process.on('message', function (message) { - var command = message['ava-child-process-command']; - if (command) { - process.emit('ava-' + command, message.data); + if (!message.ava) { + return; } + + process.emit(message.name, message.data); }); -process.on('ava-kill', function () { +process.on('ava-exit', function () { + // use a little delay when running on AppVeyor (because it's shit) + var delay = process.env.AVA_APPVEYOR ? 100 : 0; + setTimeout(function () { process.exit(0); - }, process.env.AVA_APPVEYOR ? 100 : 0); + }, delay); }); -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}); +process.on('ava-teardown', function () { + var rejections = loudRejection.currentlyUnhandled(); + + if (rejections.length === 0) { + return exit(); } - setTimeout(function () { - send('cleaned-up', {}); - }, 100); + rejections = rejections.map(function (rejection) { + return serializeValue(rejection.reason); + }); + + send('unhandledRejections', {rejections: rejections}); + setTimeout(exit, 100); }); + +function exit() { + send('teardown'); +} diff --git a/lib/fork.js b/lib/fork.js index 45530061d..6f09ca354 100644 --- a/lib/fork.js +++ b/lib/fork.js @@ -2,13 +2,14 @@ var childProcess = require('child_process'); var Promise = require('bluebird'); var path = require('path'); +var send = require('./send'); module.exports = function (args) { if (!Array.isArray(args)) { args = [args]; } - var babel = path.join(__dirname, 'babel.js'); + var filepath = path.join(__dirname, 'babel.js'); var file = args[0]; var options = { @@ -16,39 +17,26 @@ module.exports = function (args) { cwd: path.dirname(file) }; - var ps = childProcess.fork(babel, args, options); - - function send(command, data) { - ps.send({'ava-child-process-command': command, 'data': data}); - } + var ps = childProcess.fork(filepath, args, options); var promise = new Promise(function (resolve, reject) { var testResults; + ps.on('error', reject); ps.on('results', function (results) { testResults = results; - // after all tests are finished and results received - // kill the forked process, so AVA can exit safely - send('cleanup', true); - }); - - ps.on('cleaned-up', function () { - send('kill', true); + send(ps, 'teardown'); }); - ps.on('uncaughtException', function () { - send('cleanup', true); - }); - - ps.on('error', reject); - ps.on('exit', function (code) { if (code > 0 && code !== 143) { - reject(new Error(file + ' exited with a non-zero exit code: ' + code)); - } else if (testResults) { - if (!testResults.tests.length) { - testResults.stats.failCount++; + return reject(new Error(file + ' exited with a non-zero exit code: ' + code)); + } + + if (testResults) { + if (testResults.tests.length === 0) { + testResults.stats.failCount = 1; testResults.tests.push({ duration: 0, title: file, @@ -56,20 +44,36 @@ module.exports = function (args) { type: 'test' }); } + resolve(testResults); } else { - reject(new Error('Never got test results from: ' + file)); + reject(new Error('Test results were not received from: ' + file)); } }); }); // emit `test` and `stats` events ps.on('message', function (event) { + if (!event.ava) { + return; + } + + event.name = event.name.replace(/^ava\-/, ''); event.data.file = file; ps.emit(event.name, event.data); }); + // teardown finished, now exit + ps.on('teardown', function () { + send(ps, 'exit'); + }); + + // uncaught exception in fork, need to exit + ps.on('uncaughtException', function () { + send(ps, 'teardown'); + }); + // emit data events on forked process' output ps.stdout.on('data', function (data) { ps.emit('data', data); diff --git a/lib/runner.js b/lib/runner.js index 4443e39b0..a1f70b7de 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -1,9 +1,10 @@ 'use strict'; -var util = require('util'); var EventEmitter = require('events').EventEmitter; +var util = require('util'); var Promise = require('bluebird'); var hasFlag = require('has-flag'); var Test = require('./test'); +var send = require('./send'); function noop() {} @@ -192,10 +193,7 @@ Runner.prototype.run = function () { // Runner is executed directly in tests, in that case process.send() == undefined if (process.send) { - process.send({ - name: 'stats', - data: stats - }); + send('stats', stats); } return eachSeries(tests.before, this._runTest.bind(this)) diff --git a/lib/send.js b/lib/send.js new file mode 100644 index 000000000..bedc8b82e --- /dev/null +++ b/lib/send.js @@ -0,0 +1,18 @@ +'use strict'; + +// utility to send messages to processes +function send(ps, name, data) { + if (typeof ps === 'string') { + data = name || {}; + name = ps; + ps = process; + } + + ps.send({ + name: 'ava-' + name, + data: data, + ava: true + }); +} + +module.exports = send; diff --git a/test/fixture/long-running.js b/test/fixture/long-running.js index 9dae43a3a..1fbe61b09 100644 --- a/test/fixture/long-running.js +++ b/test/fixture/long-running.js @@ -11,7 +11,11 @@ test('long running', function (t) { while(Date.now() - start < 2000) { //synchronously wait for 2 seconds } - process.send({name:'cleanup-completed', data: {completed: true}}); + process.send({ + name: 'cleanup-completed', + data: {completed: true}, + ava: true + }); }, {alwaysLast: true}); setTimeout(function () { diff --git a/test/fork.js b/test/fork.js index dbbb99ba1..06ba2c3ee 100644 --- a/test/fork.js +++ b/test/fork.js @@ -31,7 +31,7 @@ test('rejects on error and streams output', function (t) { t.plan(2); fork(fixture('broken.js')) .on('uncaughtException', function (data) { - var exception = data.uncaughtException; + var exception = data.exception; t.ok(/no such file or directory/.test(exception.message)); }) .catch(function () { diff --git a/test/test.js b/test/test.js index bf1207104..b403d5e2d 100644 --- a/test/test.js +++ b/test/test.js @@ -1326,7 +1326,7 @@ test('test file that immediately exits with 0 exit code ', function (t) { execCli('fixture/immediate-0-exit.js', function (err, stdout, stderr) { t.ok(err); - t.ok(/Never got test results/.test(stderr)); + t.ok(/Test results were not received from/.test(stderr)); t.end(); }); });