Browse Source

Close #279 PR: Add programmatic API. Fixes #83

babel-plugin-for-integration-tests
vdemedes 9 years ago
committed by Sindre Sorhus
parent
commit
c424ceb3d3
  1. 230
      api.js
  2. 208
      cli.js
  3. 9
      index.js
  4. 2
      lib/babel.js
  5. 32
      lib/fork.js
  6. 23
      lib/logger.js
  7. 6
      lib/runner.js
  8. 5
      test/fork.js
  9. 2
      test/hooks.js

230
api.js

@ -0,0 +1,230 @@
'use strict';
var EventEmitter = require('events').EventEmitter;
var path = require('path');
var util = require('util');
var fs = require('fs');
var flatten = require('arr-flatten');
var Promise = require('bluebird');
var figures = require('figures');
var assign = require('object-assign');
var globby = require('globby');
var chalk = require('chalk');
var fork = require('./lib/fork');
function Api(files, options) {
if (!(this instanceof Api)) {
return new Api(files, options);
}
EventEmitter.call(this);
assign(this, options);
this.rejectionCount = 0;
this.exceptionCount = 0;
this.passCount = 0;
this.failCount = 0;
this.fileCount = 0;
this.testCount = 0;
this.errors = [];
this.stats = [];
this.tests = [];
this.files = files || [];
Object.keys(Api.prototype).forEach(function (key) {
this[key] = this[key].bind(this);
}, this);
}
util.inherits(Api, EventEmitter);
module.exports = Api;
Api.prototype._runFile = function (file) {
var args = [file];
if (this.failFast) {
args.push('--fail-fast');
}
if (this.serial) {
args.push('--serial');
}
// Forward the `time-require` `--sorted` flag.
// Intended for internal optimization tests only.
if (this._sorted) {
args.push('--sorted');
}
return fork(args)
.on('stats', this._handleStats)
.on('test', this._handleTest)
.on('unhandledRejections', this._handleRejections)
.on('uncaughtException', this._handleExceptions);
};
Api.prototype._handleRejections = function (data) {
this.rejectionCount += data.rejections.length;
data.rejections.forEach(function (err) {
err.type = 'rejection';
err.file = data.file;
this.emit('error', err);
this.errors.push(err);
}, this);
};
Api.prototype._handleExceptions = function (err) {
this.exceptionCount++;
err.type = 'exception';
this.emit('error', err);
this.errors.push(err);
};
Api.prototype._handleStats = function (stats) {
this.testCount += stats.testCount;
};
Api.prototype._handleTest = function (test) {
test.title = this._prefixTitle(test.file) + test.title;
var isError = test.error.message;
if (isError) {
this.errors.push(test);
} else {
test.error = null;
}
this.emit('test', test);
};
Api.prototype._prefixTitle = function (file) {
if (this.fileCount === 1) {
return '';
}
var separator = ' ' + chalk.gray.dim(figures.pointerSmall) + ' ';
var base = path.dirname(this.files[0]);
if (base === '.') {
base = this.files[0] || 'test';
}
base += path.sep;
var prefix = path.relative('.', file)
.replace(base, '')
.replace(/\.spec/, '')
.replace(/test\-/g, '')
.replace(/\.js$/, '')
.split(path.sep)
.join(separator);
if (prefix.length > 0) {
prefix += separator;
}
return prefix;
};
Api.prototype.run = function () {
var self = this;
return handlePaths(this.files)
.map(function (file) {
return path.resolve(file);
})
.then(function (files) {
if (files.length === 0) {
return Promise.reject(new Error('Couldn\'t find any files to test'));
}
self.fileCount = files.length;
var tests = files.map(self._runFile);
// receive test count from all files and then run the tests
var statsCount = 0;
var deferred = Promise.pending();
tests.forEach(function (test) {
var counted = false;
function tryRun() {
if (counted) {
return;
}
if (++statsCount === self.fileCount) {
self.emit('ready');
var method = self.serial ? 'mapSeries' : 'map';
deferred.resolve(Promise[method](files, function (file, index) {
return tests[index].run();
}));
}
}
test.on('stats', tryRun);
test.catch(tryRun);
});
return deferred.promise;
})
.then(function (results) {
// assemble stats from all tests
self.stats = results.map(function (result) {
return result.stats;
});
self.tests = results.map(function (result) {
return result.tests;
});
self.tests = flatten(self.tests);
self.passCount = sum(self.stats, 'passCount');
self.failCount = sum(self.stats, 'failCount');
});
};
function handlePaths(files) {
if (files.length === 0) {
files = [
'test.js',
'test-*.js',
'test/*.js'
];
}
files.push('!**/node_modules/**');
// convert pinkie-promise to Bluebird promise
files = Promise.resolve(globby(files));
return files
.map(function (file) {
if (fs.statSync(file).isDirectory()) {
return handlePaths([path.join(file, '*.js')]);
}
return file;
})
.then(flatten)
.filter(function (file) {
return path.extname(file) === '.js' && path.basename(file)[0] !== '_';
});
}
function sum(arr, key) {
var result = 0;
arr.forEach(function (item) {
result += item[key];
});
return result;
}

208
cli.js

@ -17,17 +17,12 @@ if (debug.enabled) {
require('time-require');
}
var fs = require('fs');
var path = require('path');
var figures = require('figures');
var flatten = require('arr-flatten');
var globby = require('globby');
var meow = require('meow');
var updateNotifier = require('update-notifier');
var chalk = require('chalk');
var Promise = require('bluebird');
var fork = require('./lib/fork');
var log = require('./lib/logger');
var Api = require('./api');
// Bluebird specific
Promise.longStackTraces();
@ -58,133 +53,55 @@ var cli = meow([
]
});
var rejectionCount = 0;
var exceptionCount = 0;
var testCount = 0;
var fileCount = 0;
var errors = [];
function prefixTitle(file) {
var separator = ' ' + chalk.gray.dim(figures.pointerSmall) + ' ';
var base = path.dirname(cli.input[0]);
if (base === '.') {
base = cli.input[0] || 'test';
}
base += path.sep;
var prefix = path.relative('.', file)
.replace(base, '')
.replace(/\.spec/, '')
.replace(/test\-/g, '')
.replace(/\.js$/, '')
.split(path.sep)
.join(separator);
if (prefix.length > 0) {
prefix += separator;
}
return prefix;
}
updateNotifier({pkg: cli.pkg}).notify();
function stats(stats) {
testCount += stats.testCount;
if (cli.flags.init) {
require('ava-init')();
return;
}
function test(data) {
var isError = data.error.message;
if (fileCount > 1) {
data.title = prefixTitle(data.file) + data.title;
}
log.write();
if (isError) {
log.error(data.title, chalk.red(data.error.message));
var api = new Api(cli.input, {
failFast: cli.flags.failFast,
serial: cli.flags.serial
});
errors.push(data);
api.on('test', function (test) {
if (test.error) {
log.error(test.title, chalk.red(test.error.message));
} else {
// don't log it if there's only one file and one anonymous test
if (fileCount === 1 && testCount === 1 && data.title === '[anonymous]') {
if (api.fileCount === 1 && api.testCount === 1 && test.title === '[anonymous]') {
return;
}
log.test(data);
log.test(test);
}
}
function run(file) {
var args = [file];
if (cli.flags.failFast) {
args.push('--fail-fast');
}
if (cli.flags.serial) {
args.push('--serial');
}
// Forward the `time-require` `--sorted` flag.
// Intended for internal optimization tests only.
if (cli.flags.sorted) {
args.push('--sorted');
}
return fork(args)
.on('stats', stats)
.on('test', test)
.on('unhandledRejections', handleRejections)
.on('uncaughtException', handleExceptions);
}
function handleRejections(data) {
log.unhandledRejections(data.file, data.rejections);
rejectionCount += data.rejections.length;
}
function handleExceptions(data) {
log.uncaughtException(data.file, data.exception);
exceptionCount++;
}
function sum(arr, key) {
var result = 0;
});
arr.forEach(function (item) {
result += item[key];
});
api.on('error', function (data) {
log.unhandledError(data.type, data.file, data);
});
return result;
}
api.run()
.then(function () {
log.write();
log.report(api.passCount, api.failCount, api.rejectionCount, api.exceptionCount);
log.write();
function exit(results) {
// assemble stats from all tests
var stats = results.map(function (result) {
return result.stats;
});
if (api.failCount > 0) {
log.errors(api.tests);
}
var tests = results.map(function (result) {
return result.tests;
process.stdout.write('');
flushIoAndExit(api.failCount > 0 || api.rejectionCount > 0 || api.exceptionCount > 0 ? 1 : 0);
})
.catch(function (err) {
log.error(err.message);
flushIoAndExit(1);
});
var passed = sum(stats, 'passCount');
var failed = sum(stats, 'failCount');
log.write();
log.report(passed, failed, rejectionCount, exceptionCount);
log.write();
if (failed > 0) {
log.errors(flatten(tests));
}
process.stdout.write('');
flushIoAndExit(failed > 0 || rejectionCount > 0 || exceptionCount > 0 ? 1 : 0);
}
function flushIoAndExit(code) {
// TODO: figure out why this needs to be here to
// correctly flush the output when multiple test files
@ -196,62 +113,3 @@ function flushIoAndExit(code) {
process.exit(code);
}, process.env.AVA_APPVEYOR ? 500 : 0);
}
function init(files) {
log.write();
return handlePaths(files)
.map(function (file) {
return path.resolve(file);
})
.then(function (files) {
if (files.length === 0) {
log.error('Couldn\'t find any files to test\n');
process.exit(1);
}
fileCount = files.length;
return cli.flags.serial ? Promise.mapSeries(files, run)
: Promise.all(files.map(run));
});
}
function handlePaths(files) {
if (files.length === 0) {
files = [
'test.js',
'test-*.js',
'test/*.js'
];
}
files.push('!**/node_modules/**');
// convert pinkie-promise to Bluebird promise
files = Promise.resolve(globby(files));
return files
.map(function (file) {
if (fs.statSync(file).isDirectory()) {
return handlePaths([path.join(file, '*.js')]);
}
return file;
})
.then(flatten)
.filter(function (file) {
return path.extname(file) === '.js' && path.basename(file)[0] !== '_';
});
}
updateNotifier({pkg: cli.pkg}).notify();
if (cli.flags.init) {
require('ava-init')();
} else {
init(cli.input).then(exit).catch(function (err) {
console.error(err.stack);
flushIoAndExit(1);
});
}

9
index.js

@ -66,8 +66,15 @@ function exit() {
}
globals.setImmediate(function () {
send('stats', {
testCount: runner.select({type: 'test'}).length
});
runner.on('test', test);
runner.run().then(exit);
process.on('ava-run', function () {
runner.run().then(exit);
});
});
module.exports = runner.test;

2
lib/babel.js

@ -83,7 +83,7 @@ requireFromString(transpiled.code, testPath, {
// 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');
send('no-tests');
}
// parse and re-emit ava messages

32
lib/fork.js

@ -51,6 +51,12 @@ module.exports = function (args) {
reject(new Error('Test results were not received from: ' + file));
}
});
ps.on('no-tests', function () {
send(ps, 'teardown');
reject(new Error('No tests found in ' + path.relative('.', file) + ', make sure to import "ava" at the top of your test file'));
});
});
// emit `test` and `stats` events
@ -82,5 +88,31 @@ module.exports = function (args) {
return promise;
};
promise.send = function (name, data) {
send(ps, name, data);
return promise;
};
// send 'run' event only when fork is listening for it
var isReady = false;
ps.on('stats', function () {
isReady = true;
});
promise.run = function () {
if (isReady) {
send(ps, 'run');
return promise;
}
ps.on('stats', function () {
send(ps, 'run');
});
return promise;
};
return promise;
};

23
lib/logger.js

@ -84,26 +84,13 @@ x.report = function (passed, failed, unhandled, 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();
});
var types = {
rejection: 'Unhandled Rejection',
exception: 'Uncaught Exception'
};
x.uncaughtException = function (file, error) {
log.write(chalk.red('Uncaught Exception: ', file));
x.unhandledError = function (type, file, error) {
log.write(chalk.red(types[type] + ':', file));
if (error.stack) {
log.writelpad(chalk.red(beautifyStack(error.stack)));

6
lib/runner.js

@ -5,7 +5,6 @@ var Promise = require('bluebird');
var hasFlag = require('has-flag');
var Test = require('./test');
var Hook = require('./hook');
var send = require('./send');
var objectAssign = require('object-assign');
function noop() {}
@ -184,11 +183,6 @@ Runner.prototype.run = function () {
testCount: serial.length + concurrent.length
};
// Runner is executed directly in tests, in that case `process.send() === undefined`
if (process.send) {
send('stats', stats);
}
return eachSeries(this.select({type: 'before', skipped: false}), this._runTest, this)
.catch(noop)
.then(function () {

5
test/fork.js

@ -11,6 +11,7 @@ test('emits test event', function (t) {
t.plan(1);
fork(fixture('generators.js'))
.run()
.on('test', function (tt) {
t.is(tt.title, 'generator function');
t.end();
@ -23,6 +24,7 @@ test('resolves promise with tests info', function (t) {
var file = fixture('generators.js');
fork(file)
.run()
.then(function (info) {
t.is(info.stats.passCount, 1);
t.is(info.tests.length, 1);
@ -35,6 +37,7 @@ test('rejects on error and streams output', function (t) {
t.plan(2);
fork(fixture('broken.js'))
.run()
.on('uncaughtException', function (data) {
t.true(/no such file or directory/.test(data.exception.message));
})
@ -51,6 +54,7 @@ test('exit after tests are finished', function (t) {
var cleanupCompleted = false;
fork(fixture('long-running.js'))
.run()
.on('exit', function () {
t.true(Date.now() - start < 10000, 'test waited for a pending setTimeout');
t.true(cleanupCompleted, 'cleanup did not complete');
@ -62,6 +66,7 @@ test('exit after tests are finished', function (t) {
test('fake timers do not break duration', function (t) {
fork(fixture('fake-timers.js'))
.run()
.then(function (info) {
var duration = info.tests[0].duration;
t.true(duration < 1000, duration + ' < 1000');

2
test/hooks.js

@ -298,6 +298,7 @@ test('don\'t display hook title if it did not fail', function (t) {
t.plan(2);
fork(path.join(__dirname, 'fixture', 'hooks-passing.js'))
.run()
.on('test', function (test) {
t.same(test.error, {});
t.is(test.title, 'pass');
@ -311,6 +312,7 @@ test('display hook title if it failed', function (t) {
t.plan(2);
fork(path.join(__dirname, 'fixture', 'hooks-failing.js'))
.run()
.on('test', function (test) {
t.is(test.error.name, 'AssertionError');
t.is(test.title, 'beforeEach for "pass"');

Loading…
Cancel
Save