Skip to content
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

Clean up forking and messages #236

Merged
merged 1 commit into from
Nov 17, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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
);
}

Expand Down
22 changes: 10 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
Expand All @@ -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
});
}

Expand Down
79 changes: 44 additions & 35 deletions lib/babel.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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');
}
52 changes: 28 additions & 24 deletions lib/fork.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,74 +2,78 @@
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 = {
silent: true,
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,
error: new Error('No tests for ' + file),
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);
Expand Down
8 changes: 3 additions & 5 deletions lib/runner.js
Original file line number Diff line number Diff line change
@@ -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() {}

Expand Down Expand Up @@ -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))
Expand Down
18 changes: 18 additions & 0 deletions lib/send.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';

// utility to send messages to processes
function send(ps, name, data) {
if (typeof ps === 'string') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really needed? Do we really pass a custom process anywhere? And even if we do, wouldn't it be better to just have it as the third argument instead? For simplicity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We pass messages in lib/fork.js. Yeah, I can move it to 3rd argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out it will conflict with the following:

send('teardown', ps);

where events don't have any data.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, ok, nvm then.

data = name || {};
name = ps;
ps = process;
}

ps.send({
name: 'ava-' + name,
data: data,
ava: true
});
}

module.exports = send;
6 changes: 5 additions & 1 deletion test/fixture/long-running.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
Loading