diff --git a/api.js b/api.js index 2b20af9051..4e17393c69 100644 --- a/api.js +++ b/api.js @@ -10,6 +10,7 @@ var uniqueTempDir = require('unique-temp-dir'); var findCacheDir = require('find-cache-dir'); var debounce = require('lodash.debounce'); var ms = require('ms'); +var getPort = require('get-port'); var AvaError = require('./lib/ava-error'); var fork = require('./lib/fork'); var CachingPrecompiler = require('./lib/caching-precompiler'); @@ -42,7 +43,7 @@ function Api(options) { util.inherits(Api, EventEmitter); module.exports = Api; -Api.prototype._runFile = function (file, runStatus) { +Api.prototype._runFile = function (file, runStatus, execArgv) { var hash = this.precompiler.precompileFile(file); var precompiled = {}; precompiled[file] = hash; @@ -51,7 +52,7 @@ Api.prototype._runFile = function (file, runStatus) { precompiled: precompiled }); - var emitter = fork(file, options); + var emitter = fork(file, options, execArgv); runStatus.observeFork(emitter); @@ -127,6 +128,36 @@ Api.prototype._run = function (files, _options) { return overwatch; }; +Api.prototype.computeForkExecArgs = function (files) { + var execArgv = this.options.testOnlyExecArgv || process.execArgv; + var debugArgIndex = -1; + execArgv.some(function (arg, index) { + if (arg === '--debug' || arg === '--debug-brk' || arg.indexOf('--debug-brk=') === 0 || arg.indexOf('--debug=') === 0) { + debugArgIndex = index; + return true; + } + return false; + }); + + if (debugArgIndex === -1) { + return Promise.resolve([]); + } + + return Promise.map(files, getPort) + .then(function (ports) { + return ports.map(function (port) { + var forkExecArgv = execArgv.slice(); + var flagName = '--debug'; + var oldValue = forkExecArgv[debugArgIndex]; + if (oldValue.indexOf('brk') > 0) { + flagName += '-brk'; + } + forkExecArgv[debugArgIndex] = flagName + '=' + port; + return forkExecArgv; + }); + }); +}; + Api.prototype._runNoPool = function (files, runStatus) { var self = this; var tests = new Array(self.fileCount); @@ -138,94 +169,97 @@ Api.prototype._runNoPool = function (files, runStatus) { }); }); - 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 - }); + return self.computeForkExecArgs(files) + .then(function (execArgvList) { + 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; + } - 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) - }); + var method = self.options.serial ? 'mapSeries' : 'map'; + var options = { + runOnlyExclusive: runStatus.hasExclusive + }; - return getBlankResults(); - }); - })); - } + 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; + // 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; + files.every(function (file, index) { + var tried = false; - function tryRun() { - if (!tried && !bailed) { - tried = true; - unreportedFiles--; + function tryRun() { + if (!tried && !bailed) { + tried = true; + unreportedFiles--; - if (unreportedFiles === 0) { - run(); + if (unreportedFiles === 0) { + run(); + } + } } - } - } - try { - var test = tests[index] = self._runFile(file, runStatus); + try { + var test = tests[index] = self._runFile(file, runStatus, execArgvList[index]); - test.on('stats', tryRun); - test.catch(tryRun); + test.on('stats', tryRun); + test.catch(tryRun); - return true; - } catch (err) { - bailed = true; + return true; + } catch (err) { + bailed = true; - runStatus.handleExceptions({ - exception: err, - file: path.relative('.', file) - }); + runStatus.handleExceptions({ + exception: err, + file: path.relative('.', file) + }); - resolve([]); + 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 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(); - } + return results; + }).then(function (results) { + // cancel debounced _onTimeout() from firing + if (self.options.timeout) { + runStatus._restartTimer.cancel(); + } - runStatus.processResults(results); - return runStatus; - }); + runStatus.processResults(results); + return runStatus; + }); + }); }; function getBlankResults() { @@ -253,57 +287,60 @@ Api.prototype._runLimitedPool = function (files, runStatus, concurrency) { }); }); - 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 + 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) + }); }; - 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; - }); + 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 091671ddb8..384326452f 100644 --- a/lib/fork.js +++ b/lib/fork.js @@ -31,7 +31,7 @@ if (env.NODE_PATH) { .join(path.delimiter); } -module.exports = function (file, opts) { +module.exports = function (file, opts, execArgv) { opts = objectAssign({ file: file, baseDir: process.cwd(), @@ -44,7 +44,8 @@ module.exports = function (file, opts) { var ps = childProcess.fork(path.join(__dirname, 'test-worker.js'), [JSON.stringify(opts)], { cwd: path.dirname(file), silent: true, - env: env + env: env, + execArgv: execArgv || process.execArgv }); var relFile = path.relative('.', file); diff --git a/package.json b/package.json index 5c0420454e..63336112f4 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "figures": "^1.4.0", "find-cache-dir": "^0.1.1", "fn-name": "^2.0.0", + "get-port": "^2.1.0", "globby": "^4.0.0", "has-flag": "^2.0.0", "ignore-by-default": "^1.0.0", diff --git a/test/api.js b/test/api.js index 9201f4a06f..59d8e8be9c 100644 --- a/test/api.js +++ b/test/api.js @@ -1010,3 +1010,20 @@ function generateTests(prefix, apiCreator) { ]); }); } + +function generatePassDebugTests(execArgv) { + test('pass ' + execArgv.join(' ') + ' to fork', function (t) { + t.plan(3); + + var api = new Api({testOnlyExecArgv: execArgv}); + return api.computeForkExecArgs(["foo.js"]) + .then(function (result) { + t.true(result.length === 1); + t.true(result[0].length === 1); + t.true(/--debug=\d+/.test(result[0][0])); + }); + }); +} + +generatePassDebugTests(['--debug=0']); +generatePassDebugTests(['--debug']); \ No newline at end of file