From ea25e802bce67b195d5f0fc331291bb02f16b866 Mon Sep 17 00:00:00 2001 From: vdemedes Date: Sat, 1 Oct 2016 13:10:38 +0300 Subject: [PATCH] clean up api.js --- api.js | 479 ++++++++++++++++++++++++++-------------------------- lib/fork.js | 7 + test/api.js | 2 +- 3 files changed, 250 insertions(+), 238 deletions(-) diff --git a/api.js b/api.js index 3033c7f7d..e1d95ca1a 100644 --- a/api.js +++ b/api.js @@ -2,21 +2,33 @@ 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 objectAssign = require('object-assign'); +var resolveCwd = require('resolve-cwd'); var debounce = require('lodash.debounce'); -var ms = require('ms'); var AvaFiles = require('ava-files'); var autoBind = require('auto-bind'); +var Promise = require('bluebird'); var getPort = require('get-port'); -var AvaError = require('./lib/ava-error'); -var fork = require('./lib/fork'); +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 Api(options) { if (!(this instanceof Api)) { @@ -24,6 +36,7 @@ function Api(options) { } EventEmitter.call(this); + autoBind(this); this.options = objectAssign({ cwd: process.cwd(), @@ -31,16 +44,7 @@ function Api(options) { match: [] }, options); - 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; - }); - - autoBind(this); + this.options.require = resolveModules(this.options.require); } util.inherits(Api, EventEmitter); @@ -56,104 +60,132 @@ Api.prototype._runFile = function (file, runStatus, execArgv) { }); var emitter = fork(file, options, execArgv); - runStatus.observeFork(emitter); return emitter; }; +Api.prototype.run = function (files, options) { + var self = this; + + var findOptions = { + cwd: this.options.resolveTestsFrom, + files: files + }; + + return new AvaFiles(findOptions) + .findTestFiles() + .then(function (files) { + return self._run(files, options); + }); +}; + 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 - }); + 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.run = function (files, options) { +Api.prototype._setupTimeout = function (runStatus) { var self = this; + var timeout = ms(this.options.timeout); - return new AvaFiles({files: files, cwd: this.options.resolveTestsFrom}) - .findTestFiles() - .then(function (files) { - return self._run(files, options); + 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) { + cacheDir = findCacheDir({ + name: 'ava', + files: files }); + } + + this.options.cacheDir = cacheDir; + + var isPowerAssertEnabled = this.options.powerAssert !== false; + this.precompiler = new CachingPrecompiler(cacheDir, this.options.babelConfig, isPowerAssertEnabled); }; -Api.prototype._run = function (files, _options) { - var self = this; +Api.prototype._run = function (files, options) { + if (!options) { + options = {}; + } + + var prefixTitles = this.options.explicitTitles || files.length > 1; + var basePath = path.relative('.', commonPathPrefix(files)) + path.sep; + var runStatus = new RunStatus({ - prefixTitles: this.options.explicitTitles || files.length > 1, - runOnlyExclusive: _options && _options.runOnlyExclusive, - base: path.relative('.', commonPathPrefix(files)) + path.sep + runOnlyExclusive: options.runOnlyExclusive, + prefixTitles: prefixTitles, + base: basePath }); - 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); + if (this.options.timeout) { + this._setupTimeout(runStatus); } - self.emit('test-run', runStatus, files); + this.emit('test-run', runStatus, files); if (files.length === 0) { - runStatus.handleExceptions({ - exception: new AvaError('Couldn\'t find any files to test'), - file: undefined - }); + var err = new AvaError('Couldn\'t find any files to test'); + this._handleError(runStatus, err); 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.options.powerAssert); - self.fileCount = files.length; + this._setupPrecompiler(files); + this.fileCount = files.length; var overwatch; if (this.options.concurrency > 0) { - overwatch = this._runLimitedPool(files, runStatus, self.options.serial ? 1 : this.options.concurrency); + var concurrency = this.options.serial ? 1 : this.options.concurrency; + overwatch = this._runWithPool(files, runStatus, concurrency); } else { - // _runNoPool exists to preserve legacy behavior, specifically around `.only` - overwatch = this._runNoPool(files, runStatus); + // _runWithoutPool exists to preserve legacy behavior, specifically around `.only` + overwatch = this._runWithoutPool(files, runStatus); } return overwatch; }; -Api.prototype.computeForkExecArgs = function (files) { +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) { - if (arg === '--inspect' || arg.indexOf('--inspect=') === 0) { + var isDebugArg = arg === '--inspect' || arg.indexOf('--inspect=') === 0; + if (isDebugArg) { debugArgIndex = index; - return true; } - return false; + + return isDebugArg; }); - var isInspect = debugArgIndex !== -1; + var isInspect = debugArgIndex >= 0; if (!isInspect) { execArgv.some(function (arg, index) { - if (arg === '--debug' || arg === '--debug-brk' || arg.indexOf('--debug-brk=') === 0 || arg.indexOf('--debug=') === 0) { + var isDebugArg = arg === '--debug' || arg === '--debug-brk' || arg.indexOf('--debug-brk=') === 0 || arg.indexOf('--debug=') === 0; + if (isDebugArg) { debugArgIndex = index; - return true; } - return false; + + return isDebugArg; }); } @@ -161,131 +193,173 @@ Api.prototype.computeForkExecArgs = function (files) { return Promise.resolve([]); } - return Promise.map(files, getPort) - .then(function (ports) { - return ports.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; - }); + 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._runNoPool = function (files, runStatus) { +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 = new Array(self.fileCount); - // TODO: thid should be cleared at the end of the run + 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 self.computeForkExecArgs(files) - .then(function (execArgvList) { + return this._computeForkExecArgs(files) + .then(function (argvList) { + execArgvList = argvList; + }) + .return(files) + .each(function (file, index) { 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, execArgvList[index]); - - test.on('stats', tryRun); - test.catch(tryRun); - - return true; - } catch (err) { - bailed = true; - - runStatus.handleExceptions({ - exception: err, - file: path.relative('.', file) - }); - - resolve([]); - - return false; - } + 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(); }); - }).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; }); + }) + .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; }); }; function getBlankResults() { return { stats: { + knownFailureCount: 0, testCount: 0, passCount: 0, - knownFailureCount: 0, skipCount: 0, todoCount: 0, failCount: 0 @@ -293,72 +367,3 @@ function getBlankResults() { 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 self.computeForkExecArgs(files) - .then(function (execArgvList) { - return Promise.map(files, function (file, index) { - var handleException = function (err) { - runStatus.handleExceptions({ - exception: err, - file: path.relative('.', file) - }); - }; - - try { - var test = tests[file] = self._runFile(file, runStatus, execArgvList[index]); - - 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; - }); - }); -}; diff --git a/lib/fork.js b/lib/fork.js index 7748c69d2..b61278de0 100644 --- a/lib/fork.js +++ b/lib/fork.js @@ -153,7 +153,14 @@ module.exports = function (file, opts, execArgv) { return promise; }; + var isExitInitiated = false; + promise.exit = function () { + if (isExitInitiated) { + return promise; + } + + isExitInitiated = true; send('init-exit'); return promise; diff --git a/test/api.js b/test/api.js index c3bcb0795..69dd3e8f6 100644 --- a/test/api.js +++ b/test/api.js @@ -1031,7 +1031,7 @@ function generatePassDebugTests(execArgv, expectedInspectIndex) { t.plan(expectedInspectIndex === -1 ? 3 : 2); var api = new Api({testOnlyExecArgv: execArgv}); - return api.computeForkExecArgs(['foo.js']) + return api._computeForkExecArgs(['foo.js']) .then(function (result) { t.true(result.length === 1); if (expectedInspectIndex === -1) {