|
|
|
'use strict';
|
|
|
|
var EventEmitter = require('events').EventEmitter;
|
|
|
|
var path = require('path');
|
|
|
|
var util = require('util');
|
|
|
|
var fs = require('fs');
|
|
|
|
var commonPathPrefix = require('common-path-prefix');
|
|
|
|
var uniqueTempDir = require('unique-temp-dir');
|
|
|
|
var findCacheDir = require('find-cache-dir');
|
|
|
|
var objectAssign = require('object-assign');
|
|
|
|
var resolveCwd = require('resolve-cwd');
|
|
|
|
var debounce = require('lodash.debounce');
|
|
|
|
var AvaFiles = require('ava-files');
|
|
|
|
var autoBind = require('auto-bind');
|
|
|
|
var Promise = require('bluebird');
|
|
|
|
var getPort = require('get-port');
|
|
|
|
var arrify = require('arrify');
|
|
|
|
var ms = require('ms');
|
|
|
|
var CachingPrecompiler = require('./lib/caching-precompiler');
|
|
|
|
var RunStatus = require('./lib/run-status');
|
|
|
|
var AvaError = require('./lib/ava-error');
|
|
|
|
var fork = require('./lib/fork');
|
|
|
|
|
|
|
|
function resolveModules(modules) {
|
|
|
|
return arrify(modules).map(function (name) {
|
|
|
|
var modulePath = resolveCwd(name);
|
|
|
|
if (modulePath === null) {
|
|
|
|
throw new Error('Could not resolve required module \'' + name + '\'');
|
|
|
|
}
|
|
|
|
|
|
|
|
return modulePath;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function getBlankResults() {
|
|
|
|
return {
|
|
|
|
stats: {
|
|
|
|
knownFailureCount: 0,
|
|
|
|
testCount: 0,
|
|
|
|
passCount: 0,
|
|
|
|
skipCount: 0,
|
|
|
|
todoCount: 0,
|
|
|
|
failCount: 0
|
|
|
|
},
|
|
|
|
tests: []
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function Api(options) {
|
|
|
|
if (!(this instanceof Api)) {
|
|
|
|
throw new TypeError('Class constructor Api cannot be invoked without \'new\'');
|
|
|
|
}
|
|
|
|
|
|
|
|
EventEmitter.call(this);
|
|
|
|
autoBind(this);
|
|
|
|
|
|
|
|
this.options = objectAssign({
|
|
|
|
cwd: process.cwd(),
|
|
|
|
resolveTestsFrom: process.cwd(),
|
|
|
|
match: []
|
|
|
|
}, options);
|
|
|
|
|
|
|
|
this.options.require = resolveModules(this.options.require);
|
|
|
|
}
|
|
|
|
|
|
|
|
util.inherits(Api, EventEmitter);
|
|
|
|
module.exports = Api;
|
|
|
|
|
|
|
|
Api.prototype._runFile = function (file, runStatus, execArgv) {
|
|
|
|
var hash = this.precompiler.precompileFile(file);
|
|
|
|
var precompiled = objectAssign({}, this._precompiledHelpers);
|
|
|
|
var resolvedfpath = fs.realpathSync(file);
|
|
|
|
precompiled[resolvedfpath] = hash;
|
|
|
|
|
|
|
|
var options = objectAssign({}, this.options, {
|
|
|
|
precompiled: precompiled
|
|
|
|
});
|
|
|
|
|
|
|
|
var emitter = fork(file, options, execArgv);
|
|
|
|
runStatus.observeFork(emitter);
|
|
|
|
|
|
|
|
return emitter;
|
|
|
|
};
|
|
|
|
|
|
|
|
Api.prototype.run = function (files, options) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
return new AvaFiles({cwd: this.options.resolveTestsFrom, files: files})
|
|
|
|
.findTestFiles()
|
|
|
|
.then(function (files) {
|
|
|
|
return self._run(files, options);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
Api.prototype._onTimeout = function (runStatus) {
|
|
|
|
var timeout = ms(this.options.timeout);
|
|
|
|
var err = new AvaError('Exited because no new tests completed within the last ' + timeout + 'ms of inactivity');
|
|
|
|
this._handleError(runStatus, err);
|
|
|
|
|
|
|
|
runStatus.emit('timeout');
|
|
|
|
};
|
|
|
|
|
|
|
|
Api.prototype._setupTimeout = function (runStatus) {
|
|
|
|
var self = this;
|
|
|
|
var timeout = ms(this.options.timeout);
|
|
|
|
|
|
|
|
runStatus._restartTimer = debounce(function () {
|
|
|
|
self._onTimeout(runStatus);
|
|
|
|
}, timeout);
|
|
|
|
|
|
|
|
runStatus._restartTimer();
|
|
|
|
runStatus.on('test', runStatus._restartTimer);
|
|
|
|
};
|
|
|
|
|
|
|
|
Api.prototype._cancelTimeout = function (runStatus) {
|
|
|
|
runStatus._restartTimer.cancel();
|
|
|
|
};
|
|
|
|
|
|
|
|
Api.prototype._setupPrecompiler = function (files) {
|
|
|
|
var isCacheEnabled = this.options.cacheEnabled !== false;
|
|
|
|
var cacheDir = uniqueTempDir();
|
|
|
|
|
|
|
|
if (isCacheEnabled) {
|
|
|
|
var foundDir = findCacheDir({
|
|
|
|
name: 'ava',
|
|
|
|
files: files
|
|
|
|
});
|
|
|
|
if (foundDir !== null) {
|
|
|
|
cacheDir = foundDir;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.options.cacheDir = cacheDir;
|
|
|
|
|
|
|
|
var isPowerAssertEnabled = this.options.powerAssert !== false;
|
|
|
|
this.precompiler = new CachingPrecompiler({
|
|
|
|
path: cacheDir,
|
|
|
|
babel: this.options.babelConfig,
|
|
|
|
powerAssert: isPowerAssertEnabled
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
Api.prototype._precompileHelpers = function () {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
this._precompiledHelpers = {};
|
|
|
|
|
|
|
|
return new AvaFiles({cwd: this.options.resolveTestsFrom})
|
|
|
|
.findTestHelpers()
|
|
|
|
.map(function (file) { // eslint-disable-line array-callback-return
|
|
|
|
var hash = self.precompiler.precompileFile(file);
|
|
|
|
self._precompiledHelpers[file] = hash;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
Api.prototype._run = function (files, options) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
options = options || {};
|
|
|
|
|
|
|
|
var runStatus = new RunStatus({
|
|
|
|
runOnlyExclusive: options.runOnlyExclusive,
|
|
|
|
prefixTitles: this.options.explicitTitles || files.length > 1,
|
|
|
|
base: path.relative('.', commonPathPrefix(files)) + path.sep,
|
|
|
|
failFast: this.options.failFast
|
|
|
|
});
|
|
|
|
|
|
|
|
this.emit('test-run', runStatus, files);
|
|
|
|
|
|
|
|
if (files.length === 0) {
|
|
|
|
var err = new AvaError('Couldn\'t find any files to test');
|
|
|
|
this._handleError(runStatus, err);
|
|
|
|
|
|
|
|
return Promise.resolve(runStatus);
|
|
|
|
}
|
|
|
|
|
|
|
|
this._setupPrecompiler(files);
|
|
|
|
|
|
|
|
return this._precompileHelpers()
|
|
|
|
.then(function () {
|
|
|
|
if (self.options.timeout) {
|
|
|
|
self._setupTimeout(runStatus);
|
|
|
|
}
|
|
|
|
|
|
|
|
var overwatch;
|
|
|
|
if (self.options.concurrency > 0) {
|
|
|
|
var concurrency = self.options.serial ? 1 : self.options.concurrency;
|
|
|
|
overwatch = self._runWithPool(files, runStatus, concurrency);
|
|
|
|
} else {
|
|
|
|
// _runWithoutPool exists to preserve legacy behavior, specifically around `.only`
|
|
|
|
overwatch = self._runWithoutPool(files, runStatus);
|
|
|
|
}
|
|
|
|
|
|
|
|
return overwatch;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
Api.prototype._computeForkExecArgs = function (files) {
|
|
|
|
var execArgv = this.options.testOnlyExecArgv || process.execArgv;
|
|
|
|
var debugArgIndex = -1;
|
|
|
|
|
|
|
|
// --debug-brk is used in addition to --inspect to break on first line and wait
|
|
|
|
execArgv.some(function (arg, index) {
|
|
|
|
var isDebugArg = arg === '--inspect' || arg.indexOf('--inspect=') === 0;
|
|
|
|
if (isDebugArg) {
|
|
|
|
debugArgIndex = index;
|
|
|
|
}
|
|
|
|
|
|
|
|
return isDebugArg;
|
|
|
|
});
|
|
|
|
|
|
|
|
var isInspect = debugArgIndex >= 0;
|
|
|
|
if (!isInspect) {
|
|
|
|
execArgv.some(function (arg, index) {
|
|
|
|
var isDebugArg = arg === '--debug' || arg === '--debug-brk' || arg.indexOf('--debug-brk=') === 0 || arg.indexOf('--debug=') === 0;
|
|
|
|
if (isDebugArg) {
|
|
|
|
debugArgIndex = index;
|
|
|
|
}
|
|
|
|
|
|
|
|
return isDebugArg;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (debugArgIndex === -1) {
|
|
|
|
return Promise.resolve([]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise
|
|
|
|
.map(files, getPort)
|
|
|
|
.map(function (port) {
|
|
|
|
var forkExecArgv = execArgv.slice();
|
|
|
|
var flagName = isInspect ? '--inspect' : '--debug';
|
|
|
|
var oldValue = forkExecArgv[debugArgIndex];
|
|
|
|
if (oldValue.indexOf('brk') > 0) {
|
|
|
|
flagName += '-brk';
|
|
|
|
}
|
|
|
|
|
|
|
|
forkExecArgv[debugArgIndex] = flagName + '=' + port;
|
|
|
|
|
|
|
|
return forkExecArgv;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
Api.prototype._handleError = function (runStatus, err) {
|
|
|
|
runStatus.handleExceptions({
|
|
|
|
exception: err,
|
|
|
|
file: err.file ? path.relative('.', err.file) : undefined
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
Api.prototype._runWithoutPool = function (files, runStatus) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
var tests = [];
|
|
|
|
var execArgvList;
|
|
|
|
|
|
|
|
// TODO: this should be cleared at the end of the run
|
|
|
|
runStatus.on('timeout', function () {
|
|
|
|
tests.forEach(function (fork) {
|
|
|
|
fork.exit();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return this._computeForkExecArgs(files)
|
|
|
|
.then(function (argvList) {
|
|
|
|
execArgvList = argvList;
|
|
|
|
})
|
|
|
|
.return(files)
|
|
|
|
.each(function (file, index) {
|
|
|
|
return new Promise(function (resolve) {
|
|
|
|
var forkArgs = execArgvList[index];
|
|
|
|
var test = self._runFile(file, runStatus, forkArgs);
|
|
|
|
tests.push(test);
|
|
|
|
|
|
|
|
test.on('stats', resolve);
|
|
|
|
test.catch(resolve);
|
|
|
|
}).catch(function (err) {
|
|
|
|
err.results = [];
|
|
|
|
err.file = file;
|
|
|
|
return Promise.reject(err);
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.then(function () {
|
|
|
|
if (self.options.match.length > 0 && !runStatus.hasExclusive) {
|
|
|
|
var err = new AvaError('Couldn\'t find any matching tests');
|
|
|
|
err.file = undefined;
|
|
|
|
err.results = [];
|
|
|
|
|
|
|
|
return Promise.reject(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
var method = self.options.serial ? 'mapSeries' : 'map';
|
|
|
|
var options = {
|
|
|
|
runOnlyExclusive: runStatus.hasExclusive
|
|
|
|
};
|
|
|
|
|
|
|
|
return Promise[method](files, function (file, index) {
|
|
|
|
return tests[index].run(options).catch(function (err) {
|
|
|
|
err.file = file;
|
|
|
|
self._handleError(runStatus, err);
|
|
|
|
|
|
|
|
return getBlankResults();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.catch(function (err) {
|
|
|
|
self._handleError(runStatus, err);
|
|
|
|
|
|
|
|
return err.results;
|
|
|
|
})
|
|
|
|
.tap(function (results) {
|
|
|
|
// If no tests ran, make sure to tear down the child processes
|
|
|
|
if (results.length === 0) {
|
|
|
|
tests.forEach(function (test) {
|
|
|
|
test.send('teardown');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.then(function (results) {
|
|
|
|
// Cancel debounced _onTimeout() from firing
|
|
|
|
if (self.options.timeout) {
|
|
|
|
self._cancelTimeout(runStatus);
|
|
|
|
}
|
|
|
|
|
|
|
|
runStatus.processResults(results);
|
|
|
|
|
|
|
|
return runStatus;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
Api.prototype._runWithPool = function (files, runStatus, concurrency) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
var tests = [];
|
|
|
|
var execArgvList;
|
|
|
|
|
|
|
|
runStatus.on('timeout', function () {
|
|
|
|
tests.forEach(function (fork) {
|
|
|
|
fork.exit();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return this._computeForkExecArgs(files)
|
|
|
|
.then(function (argvList) {
|
|
|
|
execArgvList = argvList;
|
|
|
|
})
|
|
|
|
.return(files)
|
|
|
|
.map(function (file, index) {
|
|
|
|
return new Promise(function (resolve) {
|
|
|
|
var forkArgs = execArgvList[index];
|
|
|
|
var test = self._runFile(file, runStatus, forkArgs);
|
|
|
|
tests.push(test);
|
|
|
|
|
|
|
|
// If we're looking for matches, run every single test process in exclusive-only mode
|
|
|
|
var options = {
|
|
|
|
runOnlyExclusive: self.options.match.length > 0
|
|
|
|
};
|
|
|
|
|
|
|
|
resolve(test.run(options));
|
|
|
|
}).catch(function (err) {
|
|
|
|
err.file = file;
|
|
|
|
self._handleError(runStatus, err);
|
|
|
|
|
|
|
|
return getBlankResults();
|
|
|
|
});
|
|
|
|
}, {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) {
|
|
|
|
self._cancelTimeout(runStatus);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (self.options.match.length > 0 && !runStatus.hasExclusive) {
|
|
|
|
results = [];
|
|
|
|
|
|
|
|
var err = new AvaError('Couldn\'t find any matching tests');
|
|
|
|
self._handleError(runStatus, err);
|
|
|
|
}
|
|
|
|
|
|
|
|
runStatus.processResults(results);
|
|
|
|
|
|
|
|
return runStatus;
|
|
|
|
});
|
|
|
|
};
|