Skip to content

DO NOT MERGE: mega less IO promises #219

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

Closed
Closed
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
6 changes: 3 additions & 3 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ environment:
install:
- ps: Install-Product node $env:nodejs_version
- set CI=true
- npm -g install npm@latest
- npm -g install npm@latest || (timeout 30 && npm -g install npm@latest)
- set PATH=%APPDATA%\npm;%PATH%
- npm install
- npm install || (timeout 30 && npm install)
matrix:
fast_finish: true
build: off
Expand All @@ -19,4 +19,4 @@ clone_depth: 1
test_script:
- node --version
- npm --version
- npm run test-win
- npm run test-win || (timeout 30 && npm run test-win)
34 changes: 29 additions & 5 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;

Expand All @@ -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('');

// 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.APPVEYOR ? 500 : 0);
}

function init(files) {
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
38 changes: 33 additions & 5 deletions lib/babel.js
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];
Expand Down Expand Up @@ -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.APPVEYOR ? 100 : 0);
});

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);
});
14 changes: 13 additions & 1 deletion lib/fork.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}

var promise = new Promise(function (resolve, reject) {
var testResults;

Expand All @@ -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);
Expand Down
33 changes: 32 additions & 1 deletion lib/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,41 @@ x.errors = function (results) {
});
};

x.report = function (passed, failed) {
x.report = function (passed, failed, unhandled, uncaught) {
if (failed > 0) {
log.writelpad(chalk.red(failed, plur('test', failed), 'failed'));
} else {
log.writelpad(chalk.green(passed, plur('test', passed), 'passed'));
}
if (unhandled > 0) {
log.writelpad(chalk.red(unhandled, 'unhandled', plur('rejection', unhandled)));
}
if (uncaught > 0) {
log.writelpad(chalk.red(uncaught, 'uncaught', plur('exceptions', uncaught)));
}
};

x.unhandledRejections = function (file, rejections) {
if (!(rejections && rejections.length)) {
return;
}
rejections.forEach(function (rejection) {
log.write(chalk.red('Unhandled Rejection: ', file));
if (rejection.stack) {
log.writelpad(chalk.red(beautifyStack(rejection.stack)));
} else {
log.writelpad(chalk.red(JSON.stringify(rejection)));
}
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();
};
15 changes: 15 additions & 0 deletions lib/serialize-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'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
return '[Function ' + value.name + ']';
}
return value;
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"has-flag": "^1.0.0",
"is-generator-fn": "^1.0.0",
"max-timeout": "^1.0.0",
"loud-rejection": "^1.2.0",
"meow": "^3.3.0",
"plur": "^2.0.0",
"power-assert-formatter": "^1.3.0",
Expand Down
9 changes: 9 additions & 0 deletions test/fixture/loud-rejection.js
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();
}, 0);
});
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
26 changes: 22 additions & 4 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1058,14 +1058,32 @@ 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: The promise ends up rejected, so we need to track this differently
// t.ok(/1 uncaught exception[^s]/.test(stdout));
t.end();
});
});

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

Expand All @@ -1091,9 +1109,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();
});
});
Expand Down