UNPKG

ava

Version:

Futuristic test runner 🚀

333 lines (276 loc) • 8.34 kB
'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 globby = require('globby'); var chalk = require('chalk'); var objectAssign = require('object-assign'); var commonPathPrefix = require('common-path-prefix'); var resolveCwd = require('resolve-cwd'); var uniqueTempDir = require('unique-temp-dir'); var findCacheDir = require('find-cache-dir'); var slash = require('slash'); var AvaError = require('./lib/ava-error'); var fork = require('./lib/fork'); var formatter = require('./lib/enhance-assert').formatter(); var CachingPrecompiler = require('./lib/caching-precompiler'); function Api(options) { if (!(this instanceof Api)) { throw new TypeError('Class constructor Api cannot be invoked without \'new\''); } EventEmitter.call(this); this.options = options || {}; this.options.require = (this.options.require || []).map(resolveCwd); this.options.match = this.options.match || []; this.excludePatterns = [ '!**/node_modules/**', '!**/fixtures/**', '!**/helpers/**' ]; Object.keys(Api.prototype).forEach(function (key) { this[key] = this[key].bind(this); }, this); this._reset(); } util.inherits(Api, EventEmitter); module.exports = Api; Api.prototype._reset = function () { this.rejectionCount = 0; this.exceptionCount = 0; this.passCount = 0; this.skipCount = 0; this.todoCount = 0; this.failCount = 0; this.fileCount = 0; this.testCount = 0; this.hasExclusive = false; this.errors = []; this.stats = []; this.tests = []; this.base = ''; }; Api.prototype._runFile = function (file) { var options = objectAssign({}, this.options, { precompiled: this.precompiler.generateHashForFile(file) }); return fork(file, options) .on('teardown', this._handleTeardown) .on('stats', this._handleStats) .on('test', this._handleTest) .on('unhandledRejections', this._handleRejections) .on('uncaughtException', this._handleExceptions) .on('stdout', this._handleOutput.bind(this, 'stdout')) .on('stderr', this._handleOutput.bind(this, 'stderr')); }; Api.prototype._handleOutput = function (channel, data) { this.emit(channel, data); }; 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 (data) { this.exceptionCount++; var err = data.exception; err.type = 'exception'; err.file = data.file; this.emit('error', err); this.errors.push(err); }; Api.prototype._handleTeardown = function (data) { this.emit('dependencies', data.file, data.dependencies); }; Api.prototype._handleStats = function (stats) { this.emit('stats', stats); if (this.hasExclusive && !stats.hasExclusive) { return; } if (!this.hasExclusive && stats.hasExclusive) { this.hasExclusive = true; this.testCount = 0; } this.testCount += stats.testCount; }; Api.prototype._handleTest = function (test) { test.title = this._prefixTitle(test.file) + test.title; if (test.error) { if (test.error.powerAssertContext) { var message = formatter(test.error.powerAssertContext); if (test.error.originalMessage) { message = test.error.originalMessage + ' ' + message; } test.error.message = message; } if (test.error.name !== 'AssertionError') { test.error.message = 'failed with "' + test.error.message + '"'; } this.errors.push(test); } this.emit('test', test); }; Api.prototype._prefixTitle = function (file) { if (this.fileCount === 1 && !this.options.explicitTitles) { return ''; } var separator = ' ' + chalk.gray.dim(figures.pointerSmall) + ' '; var prefix = path.relative('.', file) .replace(this.base, '') .replace(/\.spec/, '') .replace(/\.test/, '') .replace(/test\-/g, '') .replace(/\.js$/, '') .split(path.sep) .join(separator); if (prefix.length > 0) { prefix += separator; } return prefix; }; Api.prototype.run = function (files, options) { var self = this; this._reset(); if (options && options.runOnlyExclusive) { this.hasExclusive = true; } return handlePaths(files, this.excludePatterns) .map(function (file) { return path.resolve(file); }) .then(function (files) { if (files.length === 0) { self._handleExceptions({ exception: new AvaError('Couldn\'t find any files to test'), file: undefined }); return []; } var cacheEnabled = self.options.cacheEnabled !== false; var cacheDir = (cacheEnabled && findCacheDir({name: 'ava', files: files})) || uniqueTempDir(); self.options.cacheDir = cacheDir; self.precompiler = new CachingPrecompiler(cacheDir, self.options.babelConfig); self.fileCount = files.length; self.base = path.relative('.', commonPathPrefix(files)) + path.sep; var tests = new Array(self.fileCount); return new Promise(function (resolve) { function run() { if (self.options.match.length > 0 && !self.hasExclusive) { self._handleExceptions({ exception: new AvaError('Couldn\'t find any matching tests'), file: undefined }); resolve([]); return; } self.emit('ready'); var method = self.options.serial ? 'mapSeries' : 'map'; var options = { runOnlyExclusive: self.hasExclusive }; resolve(Promise[method](files, function (file, index) { return tests[index].run(options).catch(function (err) { // The test failed catastrophically. Flag it up as an // exception, then return an empty result. Other tests may // continue to run. self._handleExceptions({ exception: err, file: file }); return { stats: {passCount: 0, skipCount: 0, todoCount: 0, failCount: 0}, tests: [] }; }); })); } // receive test count from all files and then run the tests var unreportedFiles = self.fileCount; var bailed = false; files.every(function (file, index) { var tried = false; function tryRun() { if (!tried && !bailed) { unreportedFiles--; if (unreportedFiles === 0) { run(); } } } try { var test = tests[index] = self._runFile(file); test.on('stats', tryRun); test.catch(tryRun); return true; } catch (err) { bailed = true; self._handleExceptions({ exception: err, file: file }); resolve([]); return false; } }); }).then(function (results) { if (results.length === 0) { // No tests ran, make sure to tear down the child processes. tests.forEach(function (test) { test.send('teardown'); }); } return results; }); }) .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.skipCount = sum(self.stats, 'skipCount'); self.todoCount = sum(self.stats, 'todoCount'); self.failCount = sum(self.stats, 'failCount'); }); }; function handlePaths(files, excludePatterns) { // convert pinkie-promise to Bluebird promise files = Promise.resolve(globby(files.concat(excludePatterns))); return files .map(function (file) { if (fs.statSync(file).isDirectory()) { var pattern = path.join(file, '**', '*.js'); if (process.platform === 'win32') { // Always use / in patterns, harmonizing matching across platforms. pattern = slash(pattern); } return handlePaths([pattern], excludePatterns); } // globby returns slashes even on Windows. Normalize here so the file // paths are consistently platform-accurate as tests are run. return path.normalize(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; }