|
'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'); |
|
|
|
Promise.config({ |
|
// Enable cancellation |
|
cancellation: true |
|
}); |
|
|
|
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([]); |
|
} |
|
|
|
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 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(); |
|
}); |
|
}); |
|
|
|
//// |
|
// Begin @dcousineau's noodling |
|
//// |
|
|
|
const emptyResults = { |
|
stats: { |
|
testCount: 0, |
|
passCount: 0, |
|
skipCount: 0, |
|
todoCount: 0, |
|
failCount: 0 |
|
}, |
|
tests: [] |
|
}; |
|
|
|
return new Promise(function (resolve) { |
|
Promise.map(files, (file, index) => { |
|
function runner(test, resolve, reject) { |
|
return function() { |
|
self.emit('ready'); |
|
var options = {}; |
|
test.run(options) |
|
.then(resolve) |
|
.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) |
|
}); |
|
|
|
resolve(emptyResults); |
|
}); |
|
}; |
|
} |
|
|
|
try { |
|
var test = tests[index] = self._runFile(file, runStatus); |
|
|
|
return new Promise((resolve, reject) => { |
|
test.on('stats', runner(test, resolve, reject)); |
|
test.catch(runner(test, resolve, reject)); |
|
}); |
|
} catch (err) { |
|
runStatus.handleExceptions({ |
|
exception: err, |
|
file: path.relative('.', file) |
|
}); |
|
reject(err); |
|
} |
|
}, {concurrency: 5}) |
|
.then(results => { |
|
// cancel debounced _onTimeout() from firing |
|
if (self.options.timeout) { |
|
runStatus._restartTimer.cancel(); |
|
} |
|
|
|
runStatus.processResults(results); |
|
resolve(runStatus); |
|
}) |
|
.catch(err => { |
|
runStatus.processResults([]); |
|
resolve(runStatus); |
|
}); |
|
}); |
|
|
|
//// |
|
// End @dcousineau's noodling |
|
//// |
|
}; |