You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
310 lines
7.5 KiB
JavaScript
310 lines
7.5 KiB
JavaScript
5 years ago
|
'use strict';
|
||
|
var EventEmitter = require('events').EventEmitter;
|
||
|
var path = require('path');
|
||
|
var util = require('util');
|
||
|
var Promise = require('bluebird');
|
||
|
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 debounce = require('lodash.debounce');
|
||
|
var ms = require('ms');
|
||
|
var AvaError = require('./lib/ava-error');
|
||
|
var fork = require('./lib/fork');
|
||
|
var CachingPrecompiler = require('./lib/caching-precompiler');
|
||
|
var AvaFiles = require('./lib/ava-files');
|
||
|
var RunStatus = require('./lib/run-status');
|
||
|
|
||
|
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.match = this.options.match || [];
|
||
|
this.options.require = (this.options.require || []).map(function (moduleId) {
|
||
|
var ret = resolveCwd(moduleId);
|
||
|
if (ret === null) {
|
||
|
throw new Error('Could not resolve required module \'' + moduleId + '\'');
|
||
|
}
|
||
|
|
||
|
return ret;
|
||
|
});
|
||
|
|
||
|
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, runStatus) {
|
||
|
var hash = this.precompiler.precompileFile(file);
|
||
|
var precompiled = {};
|
||
|
precompiled[file] = hash;
|
||
|
|
||
|
var options = objectAssign({}, this.options, {
|
||
|
precompiled: precompiled
|
||
|
});
|
||
|
|
||
|
var emitter = fork(file, options);
|
||
|
|
||
|
runStatus.observeFork(emitter);
|
||
|
|
||
|
return emitter;
|
||
|
};
|
||
|
|
||
|
Api.prototype._onTimeout = function (runStatus) {
|
||
|
var timeout = ms(this.options.timeout);
|
||
|
var message = 'Exited because no new tests completed within the last ' + timeout + 'ms of inactivity';
|
||
|
|
||
|
runStatus.handleExceptions({
|
||
|
exception: new AvaError(message),
|
||
|
file: undefined
|
||
|
});
|
||
|
|
||
|
runStatus.emit('timeout');
|
||
|
};
|
||
|
|
||
|
Api.prototype.run = function (files, options) {
|
||
|
var self = this;
|
||
|
|
||
|
return new AvaFiles(files)
|
||
|
.findTestFiles()
|
||
|
.then(function (files) {
|
||
|
return self._run(files, options);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Api.prototype._run = function (files, _options) {
|
||
|
var self = this;
|
||
|
var runStatus = new RunStatus({
|
||
|
prefixTitles: this.options.explicitTitles || files.length > 1,
|
||
|
runOnlyExclusive: _options && _options.runOnlyExclusive,
|
||
|
base: path.relative('.', commonPathPrefix(files)) + path.sep
|
||
|
});
|
||
|
|
||
|
if (self.options.timeout) {
|
||
|
var timeout = ms(self.options.timeout);
|
||
|
runStatus._restartTimer = debounce(function () {
|
||
|
self._onTimeout(runStatus);
|
||
|
}, timeout);
|
||
|
runStatus._restartTimer();
|
||
|
runStatus.on('test', runStatus._restartTimer);
|
||
|
}
|
||
|
|
||
|
self.emit('test-run', runStatus, files);
|
||
|
|
||
|
if (files.length === 0) {
|
||
|
runStatus.handleExceptions({
|
||
|
exception: new AvaError('Couldn\'t find any files to test'),
|
||
|
file: undefined
|
||
|
});
|
||
|
|
||
|
return Promise.resolve(runStatus);
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
|
||
|
var overwatch;
|
||
|
if (this.options.concurrency > 0) {
|
||
|
overwatch = this._runLimitedPool(files, runStatus, self.options.serial ? 1 : this.options.concurrency);
|
||
|
} else {
|
||
|
// _runNoPool exists to preserve legacy behavior, specifically around `.only`
|
||
|
overwatch = this._runNoPool(files, runStatus);
|
||
|
}
|
||
|
|
||
|
return overwatch;
|
||
|
};
|
||
|
|
||
|
Api.prototype._runNoPool = function (files, runStatus) {
|
||
|
var self = this;
|
||
|
var tests = new Array(self.fileCount);
|
||
|
|
||
|
// TODO: thid should be cleared at the end of the run
|
||
|
runStatus.on('timeout', function () {
|
||
|
tests.forEach(function (fork) {
|
||
|
fork.exit();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
return new Promise(function (resolve) {
|
||
|
function run() {
|
||
|
if (self.options.match.length > 0 && !runStatus.hasExclusive) {
|
||
|
runStatus.handleExceptions({
|
||
|
exception: new AvaError('Couldn\'t find any matching tests'),
|
||
|
file: undefined
|
||
|
});
|
||
|
|
||
|
resolve([]);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var method = self.options.serial ? 'mapSeries' : 'map';
|
||
|
var options = {
|
||
|
runOnlyExclusive: runStatus.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.
|
||
|
runStatus.handleExceptions({
|
||
|
exception: err,
|
||
|
file: path.relative('.', file)
|
||
|
});
|
||
|
|
||
|
return getBlankResults();
|
||
|
});
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
// 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, runStatus);
|
||
|
|
||
|
test.on('stats', tryRun);
|
||
|
test.catch(tryRun);
|
||
|
|
||
|
return true;
|
||
|
} catch (err) {
|
||
|
bailed = true;
|
||
|
|
||
|
runStatus.handleExceptions({
|
||
|
exception: err,
|
||
|
file: path.relative('.', 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) {
|
||
|
// cancel debounced _onTimeout() from firing
|
||
|
if (self.options.timeout) {
|
||
|
runStatus._restartTimer.cancel();
|
||
|
}
|
||
|
|
||
|
runStatus.processResults(results);
|
||
|
return runStatus;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
function getBlankResults() {
|
||
|
return {
|
||
|
stats: {
|
||
|
testCount: 0,
|
||
|
passCount: 0,
|
||
|
knownFailureCount: 0,
|
||
|
skipCount: 0,
|
||
|
todoCount: 0,
|
||
|
failCount: 0
|
||
|
},
|
||
|
tests: []
|
||
|
};
|
||
|
}
|
||
|
|
||
|
Api.prototype._runLimitedPool = function (files, runStatus, concurrency) {
|
||
|
var self = this;
|
||
|
var tests = {};
|
||
|
|
||
|
runStatus.on('timeout', function () {
|
||
|
Object.keys(tests).forEach(function (file) {
|
||
|
var fork = tests[file];
|
||
|
fork.exit();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
return Promise.map(files, function (file) {
|
||
|
var handleException = function (err) {
|
||
|
runStatus.handleExceptions({
|
||
|
exception: err,
|
||
|
file: path.relative('.', file)
|
||
|
});
|
||
|
};
|
||
|
|
||
|
try {
|
||
|
var test = tests[file] = self._runFile(file, runStatus);
|
||
|
|
||
|
return new Promise(function (resolve, reject) {
|
||
|
var runner = function () {
|
||
|
var options = {
|
||
|
// If we're looking for matches, run every single test process in exclusive-only mode
|
||
|
runOnlyExclusive: self.options.match.length > 0
|
||
|
};
|
||
|
test.run(options)
|
||
|
.then(resolve)
|
||
|
.catch(reject);
|
||
|
};
|
||
|
|
||
|
test.on('stats', runner);
|
||
|
test.on('exit', function () {
|
||
|
delete tests[file];
|
||
|
});
|
||
|
test.catch(runner);
|
||
|
}).catch(handleException);
|
||
|
} catch (err) {
|
||
|
handleException(err);
|
||
|
}
|
||
|
}, {concurrency: concurrency})
|
||
|
.then(function (results) {
|
||
|
// Filter out undefined results (usually result of caught exceptions)
|
||
|
results = results.filter(Boolean);
|
||
|
|
||
|
// cancel debounced _onTimeout() from firing
|
||
|
if (self.options.timeout) {
|
||
|
runStatus._restartTimer.cancel();
|
||
|
}
|
||
|
|
||
|
if (self.options.match.length > 0 && !runStatus.hasExclusive) {
|
||
|
// Ensure results are empty
|
||
|
results = [];
|
||
|
runStatus.handleExceptions({
|
||
|
exception: new AvaError('Couldn\'t find any matching tests'),
|
||
|
file: undefined
|
||
|
});
|
||
|
}
|
||
|
|
||
|
runStatus.processResults(results);
|
||
|
return runStatus;
|
||
|
});
|
||
|
};
|