You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

362 lines
8.4 KiB

'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 isObj = require('is-obj');
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);
};
function normalizeError(err) {
if (!isObj(err)) {
err = {
message: err,
stack: err
};
}
return err;
}
Api.prototype._handleRejections = function (data) {
this.rejectionCount += data.rejections.length;
data.rejections.forEach(function (err) {
err = normalizeError(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 = normalizeError(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) {
tried = true;
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;
}