diff --git a/bin/jest.js b/bin/jest.js index 11d9d761cd41..8a9ace582a1b 100755 --- a/bin/jest.js +++ b/bin/jest.js @@ -9,103 +9,10 @@ /* jshint node: true */ "use strict"; -var child_process = require('child_process'); -var defaultTestResultHandler = require('../src/defaultTestResultHandler'); var fs = require('fs'); var harmonize = require('harmonize'); var optimist = require('optimist'); var path = require('path'); -var Q = require('q'); -var TestRunner = require('../src/TestRunner'); -var colors = require('../src/lib/colors'); -var utils = require('../src/lib/utils'); - -var _jestVersion = null; -function _getJestVersion() { - if (_jestVersion === null) { - var pkgJsonPath = path.resolve(__dirname, '..', 'package.json'); - _jestVersion = require(pkgJsonPath).version; - } - return _jestVersion; -} - -function _findChangedFiles(dirPath) { - var deferred = Q.defer(); - - var args = - ['diff', '--name-only', '--diff-filter=ACMR']; - var child = child_process.spawn('git', args, {cwd: dirPath}); - - var stdout = ''; - child.stdout.on('data', function(data) { - stdout += data; - }); - - var stderr = ''; - child.stderr.on('data', function(data) { - stderr += data; - }); - - child.on('close', function(code) { - if (code === 0) { - stdout = stdout.trim(); - if (stdout === '') { - deferred.resolve([]); - } else { - deferred.resolve(stdout.split('\n').map(function(changedPath) { - return path.resolve(dirPath, changedPath); - })); - } - } else { - deferred.reject(code + ': ' + stderr); - } - }); - - return deferred.promise; -} - -function _onResultReady(config, result) { - return defaultTestResultHandler(config, result); -} - -function _onRunComplete(completionData) { - var numFailedTests = completionData.numFailedTests; - var numTotalTests = completionData.numTotalTests; - var numPassedTests = numTotalTests - numFailedTests; - var startTime = completionData.startTime; - var endTime = completionData.endTime; - - var results = ''; - if (numFailedTests) { - results += - colors.colorize( - [numFailedTests, (numFailedTests > 1 ? 'tests' : 'test'), 'failed'].join(' '), - colors.RED + colors.BOLD - ); - results += ', '; - } - results += - colors.colorize( - [numPassedTests, (numPassedTests > 1 ? 'tests' : 'test'), 'passed'].join(' '), - colors.GREEN + colors.BOLD - ); - results += ' (' + numTotalTests + ' total)'; - - console.log(results); - console.log('Run time: ' + ((endTime - startTime) / 1000) + 's'); -} - -function _verifyIsGitRepository(dirPath) { - var deferred = Q.defer(); - - child_process.spawn('git', ['rev-parse', '--git-dir'], {cwd: dirPath}) - .on('close', function(code) { - var isGitRepo = code === 0; - deferred.resolve(isGitRepo); - }); - - return deferred.promise; -} /** * Takes a description string, puts it on the next line, indents it, and makes @@ -129,273 +36,120 @@ function _wrapDesc(desc) { }, ['']).join(indent); } -function runCLI(argv, packageRoot, onComplete) { - argv = argv || {}; - - // Old versions of the Jest CLI do not pass an onComplete callback, so it's - // important that we provide backward compatibility in the API - if (!onComplete) { - onComplete = function() {}; - } - - if (argv.version) { - console.log('v' + _getJestVersion()); - onComplete(true); - return; - } - - var config; - if (argv.config) { - if (typeof argv.config === 'string') { - config = utils.loadConfigFromFile(argv.config); - } else if (typeof argv.config === 'object') { - config = Q(utils.normalizeConfig(argv.config)); - } - } else { - var pkgJsonPath = path.join(packageRoot, 'package.json'); - var pkgJson = fs.existsSync(pkgJsonPath) ? require(pkgJsonPath) : {}; - - // First look to see if there is a package.json file with a jest config in it - if (pkgJson.jest) { - if (!pkgJson.jest.hasOwnProperty('rootDir')) { - pkgJson.jest.rootDir = packageRoot; - } else { - pkgJson.jest.rootDir = path.resolve(packageRoot, pkgJson.jest.rootDir); - } - config = utils.normalizeConfig(pkgJson.jest); - config.name = pkgJson.name; - config = Q(config); - - // If not, use a sane default config - } else { - config = Q(utils.normalizeConfig({ - name: packageRoot.replace(/[/\\]/g, '_'), - rootDir: packageRoot, - testPathDirs: [packageRoot], - testPathIgnorePatterns: ['/node_modules/.+'] - })); - } - } - - config.done(function(config) { - var pathPattern = - argv.testPathPattern || - (argv._ && argv._.length ? new RegExp(argv._.join('|')) : /.*/); - - var testRunnerOpts = {}; - if (argv.maxWorkers) { - testRunnerOpts.maxWorkers = argv.maxWorkers; +harmonize(); + +var argv = optimist + .usage('Usage: $0 [--config=] [TestPathRegExp]') + .options({ + config: { + alias: 'c', + description: _wrapDesc( + 'The path to a jest config file specifying how to find and execute ' + + 'tests.' + ), + type: 'string' + }, + coverage: { + description: _wrapDesc( + 'Indicates that test coverage information should be collected and ' + + 'reported in the output.' + ), + type: 'boolean' + }, + maxWorkers: { + alias: 'w', + description: _wrapDesc( + 'Specifies the maximum number of workers the worker-pool will spawn ' + + 'for running tests. This defaults to the number of the cores ' + + 'available on your machine. (its usually best not to override this ' + + 'default)' + ), + type: 'string' // no, optimist -- its a number.. :( + }, + onlyChanged: { + alias: 'o', + description: _wrapDesc( + 'Attempts to identify which tests to run based on which files have ' + + 'changed in the current repository. Only works if you\'re running ' + + 'tests in a git repository at the moment.' + ), + type: 'boolean' + }, + runInBand: { + alias: 'i', + description: _wrapDesc( + 'Run all tests serially in the current process (rather than creating ' + + 'a worker pool of child processes that run tests). This is sometimes ' + + 'useful for debugging, but such use cases are pretty rare.' + ), + type: 'boolean' + }, + version: { + alias: 'v', + description: _wrapDesc('Print the version and exit'), + type: 'boolean' } - - if (argv.coverage) { - config.collectCoverage = true; + }) + .check(function(argv) { + if (argv.runInBand && argv.hasOwnProperty('maxWorkers')) { + throw ( + 'Both --runInBand and --maxWorkers were specified, but these two ' + + 'options do not make sense together. Which is it?' + ); } - var testRunner = new TestRunner(config, testRunnerOpts); - - function _runTestsOnPathPattern(pathPattern) { - var testPathStream = testRunner.streamTestPathsMatching(pathPattern); - - var deferred = Q.defer(); - var foundPaths = []; - testPathStream.on('data', function(pathStr) { - foundPaths.push(pathStr); - }); - testPathStream.on('error', function(err) { - deferred.reject(err); - }); - testPathStream.on('end', function() { - deferred.resolve(foundPaths); - }); - - return deferred.promise - .then(function(matchingTestPaths) { - var numMatching = matchingTestPaths.length; - var pluralizedTest = numMatching > 1 ? 'tests' : 'test'; - - console.log( - 'Found ' + numMatching + ' matching ' + pluralizedTest + '...' - ); - if (argv.runInBand) { - return testRunner.runTestsInBand(matchingTestPaths, _onResultReady); - } else { - return testRunner.runTestsParallel(matchingTestPaths, _onResultReady); - } - }) - .then(function(completionData) { - _onRunComplete(completionData); - onComplete(completionData.numFailedTests === 0); - }); + if (argv.onlyChanged && argv._.length > 0) { + throw ( + 'Both --onlyChanged and a path pattern were specified, but these two ' + + 'options do not make sense together. Which is it? Do you want to run ' + + 'tests for changed files? Or for a specific set of files?' + ); } + }) + .argv - if (argv.onlyChanged) { - console.log('Looking for changed files...'); - - var testPathDirsAreGit = config.testPathDirs.map(_verifyIsGitRepository); - Q.all(testPathDirsAreGit).then(function(results) { - if (!results.every(function(result) { return result; })) { - console.error( - 'It appears that one of your testPathDirs does not exist ' + - 'with in a git repository. Currently --onlyChanged only works ' + - 'with git projects.\n' - ); - onComplete(false); - } - - return Q.all(config.testPathDirs.map(_findChangedFiles)); - }).then(function(changedPathSets) { - // Collapse changed files from each of the testPathDirs into a single list - // of changed file paths - var changedPaths = []; - changedPathSets.forEach(function(pathSet) { - changedPaths = changedPaths.concat(pathSet); - }); - - var deferred = Q.defer(); - var affectedPathStream = - testRunner.streamTestPathsRelatedTo(changedPaths); - - var affectedTestPaths = []; - affectedPathStream.on('data', function(pathStr) { - affectedTestPaths.push(pathStr); - }); - affectedPathStream.on('error', function(err) { - deferred.reject(err); - }); - affectedPathStream.on('end', function() { - deferred.resolve(affectedTestPaths); - }); - - return deferred.promise; - }).done(function(affectedTestPaths) { - if (affectedTestPaths.length > 0) { - _runTestsOnPathPattern(new RegExp(affectedTestPaths.join('|'))).done(); - } else { - console.log('No tests to run!'); - } - }); - } else { - _runTestsOnPathPattern(pathPattern).done(); - } - }); +if (argv.help) { + optimist.showHelp(); + process.exit(0); } -function _main(onComplete) { - var argv = optimist - .usage('Usage: $0 [--config=] [TestPathRegExp]') - .options({ - config: { - alias: 'c', - description: _wrapDesc( - 'The path to a jest config file specifying how to find and execute ' + - 'tests.' - ), - type: 'string' - }, - coverage: { - description: _wrapDesc( - 'Indicates that test coverage information should be collected and ' + - 'reported in the output.' - ), - type: 'boolean' - }, - maxWorkers: { - alias: 'w', - description: _wrapDesc( - 'Specifies the maximum number of workers the worker-pool will spawn ' + - 'for running tests. This defaults to the number of the cores ' + - 'available on your machine. (its usually best not to override this ' + - 'default)' - ), - type: 'string' // no, optimist -- its a number.. :( - }, - onlyChanged: { - alias: 'o', - description: _wrapDesc( - 'Attempts to identify which tests to run based on which files have ' + - 'changed in the current repository. Only works if you\'re running ' + - 'tests in a git repository at the moment.' - ), - type: 'boolean' - }, - runInBand: { - alias: 'i', - description: _wrapDesc( - 'Run all tests serially in the current process (rather than creating ' + - 'a worker pool of child processes that run tests). This is sometimes ' + - 'useful for debugging, but such use cases are pretty rare.' - ), - type: 'boolean' - }, - version: { - alias: 'v', - description: _wrapDesc('Print the version and exit'), - type: 'boolean' - } - }) - .check(function(argv) { - if (argv.runInBand && argv.hasOwnProperty('maxWorkers')) { - throw ( - "Both --runInBand and --maxWorkers were specified, but these two " + - "options don't make sense together. Which is it?" - ); - } +var cwd = process.cwd(); - if (argv.onlyChanged && argv._.length > 0) { - throw ( - "Both --onlyChanged and a path pattern were specified, but these two " + - "options don't make sense together. Which is it? Do you want to run " + - "tests for changed files? Or for a specific set of files?" - ); - } - }) - .argv - - if (argv.help) { - optimist.showHelp(); - process.exit(0); +// Is the cwd somewhere within an npm package? +var cwdPackageRoot = cwd; +while (!fs.existsSync(path.join(cwdPackageRoot, 'package.json'))) { + if (cwdPackageRoot === '/') { + cwdPackageRoot = cwd; + break; } + cwdPackageRoot = path.resolve(cwdPackageRoot, '..'); +} - var cwd = process.cwd(); - - // Is the cwd somewhere within an npm package? - var cwdPackageRoot = cwd; - while (!fs.existsSync(path.join(cwdPackageRoot, 'package.json'))) { - if (cwdPackageRoot === '/') { - cwdPackageRoot = cwd; - break; - } - cwdPackageRoot = path.resolve(cwdPackageRoot, '..'); - } +// Is there a package.json at our cwdPackageRoot that indicates that there +// should be a version of Jest installed? +var cwdPkgJsonPath = path.join(cwdPackageRoot, 'package.json'); - // Is there a package.json at our cwdPackageRoot that indicates that there - // should be a version of Jest installed? - var cwdPkgJsonPath = path.join(cwdPackageRoot, 'package.json'); +// Is there a version of Jest installed at our cwdPackageRoot? +var cwdJestBinPath = path.join(cwdPackageRoot, 'node_modules/jest-cli'); - // Is there a version of Jest installed at our cwdPackageRoot? - var cwdJestBinPath = path.join( - cwdPackageRoot, - 'node_modules', - 'jest-cli', - 'bin', - 'jest.js' - ); +// Get a jest instance +var jest; - // If a version of Jest was found installed in the CWD package, run using that - if (fs.existsSync(cwdJestBinPath)) { - var jestBinary = require(cwdJestBinPath); - if (!jestBinary.runCLI) { - console.error( - 'This project requires an older version of Jest than what you have ' + - 'installed globally.\n' + - 'Please upgrade this project past Jest version 0.1.5' - ); - process.exit(1); - } +if (fs.existsSync(cwdJestBinPath)) { + // If a version of Jest was found installed in the CWD package, use that. + jest = require(cwdJestBinPath); - jestBinary.runCLI(argv, cwdPackageRoot, onComplete); - return; + if (!jest.runCLI) { + console.error( + 'This project requires an older version of Jest than what you have ' + + 'installed globally.\n' + + 'Please upgrade this project past Jest version 0.1.5' + ); + process.exit(1); } +} else { + // Otherwise, load this version of Jest. + jest = require('../'); // If a package.json was found in the CWD package indicating a specific // version of Jest to be used, bail out and ask the user to `npm install` @@ -405,8 +159,6 @@ function _main(onComplete) { var cwdPkgDeps = cwdPkgJson.dependencies; var cwdPkgDevDeps = cwdPkgJson.devDependencies; - var thisJestVersion = _getJestVersion(); - if (cwdPkgDeps && cwdPkgDeps['jest-cli'] || cwdPkgDevDeps && cwdPkgDevDeps['jest-cli']) { console.error( @@ -416,18 +168,12 @@ function _main(onComplete) { process.exit(1); } } - - if (!argv.version && cwdPackageRoot) { - console.log('Using Jest CLI v' + _getJestVersion()); - } - runCLI(argv, cwdPackageRoot, onComplete); } -exports.runCLI = runCLI; - -if (require.main === module) { - harmonize(); - _main(function (success) { - process.exit(success ? 0 : 1); - }); +if (!argv.version) { + console.log('Using Jest CLI v' + jest.getVersion()); } + +jest.runCLI(argv, cwdPackageRoot, function (success) { + process.exit(success ? 0 : 1); +}); diff --git a/package.json b/package.json index 89559eb39f58..70f11bec81e3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "jest-cli", "description": "Painless JavaScript Unit Testing.", "version": "0.1.18", - "main": "bin/jest.js", + "main": "src/jest.js", "dependencies": { "coffee-script": "1.7.1", "cover": "~0.2.8", diff --git a/src/TestRunner.js b/src/TestRunner.js index 9ae206abd337..171f886821a6 100644 --- a/src/TestRunner.js +++ b/src/TestRunner.js @@ -19,6 +19,16 @@ var Console = require('./Console'); var TEST_WORKER_PATH = require.resolve('./TestWorker'); var DEFAULT_OPTIONS = { + + /** + * When true, runs all tests serially in the current process, rather than + * creating a worker pool of child processes. + * + * This can be useful for debugging, or when the environment limits to a + * single process. + */ + runInBand: false, + /** * The maximum number of workers to run tests concurrently with. * @@ -364,79 +374,98 @@ TestRunner.prototype.runTest = function(testFilePath) { }; /** - * Run all given test paths serially (in the current process). - * - * This is mostly useful for debugging issues with jest itself, but may also be - * useful for scenarios where you don't want jest to start up a worker pool of - * its own. + * Run all given test paths. * * @param {Array} testPaths Array of paths to test files - * @param {Function} onResult Callback called once for each test result + * @param {Object} reporter Collection of callbacks called on test events * @return {Promise} Fulfilled with aggregate pass/fail information * about all tests that were run */ -TestRunner.prototype.runTestsInBand = function(testPaths, onResult) { +TestRunner.prototype.runTests = function(testPaths, reporter) { + if (!reporter) { + reporter = require('./defaultTestReporter'); + } var config = this._config; var aggregatedResults = { numFailedTests: 0, + numPassedTests: 0, numTotalTests: testPaths.length, startTime: Date.now(), endTime: null }; + reporter.onRunStart && reporter.onRunStart(config, aggregatedResults); + + var onTestResult = function (testPath, testResult) { + if (testResult.numFailingTests > 0) { + aggregatedResults.numFailedTests++; + } else { + aggregatedResults.numPassedTests++; + } + reporter.onTestResult && reporter.onTestResult( + config, + testResult, + aggregatedResults + ); + }; + + var onRunFailure = function (testPath, err) { + aggregatedResults.numFailedTests++; + reporter.onTestResult && reporter.onTestResult(config, { + testFilePath: testPath, + testExecError: err, + suites: {}, + tests: {}, + logMessages: [] + }, aggregatedResults); + }; + + var testRun = this._createTestRun(testPaths, onTestResult, onRunFailure); + + return testRun.then(function() { + aggregatedResults.endTime = Date.now(); + reporter.onRunComplete && reporter.onRunComplete(config, aggregatedResults); + return aggregatedResults.numFailedTests === 0; + }); +}; + +TestRunner.prototype._createTestRun = function( + testPaths, onTestResult, onRunFailure +) { + if (this._opts.runInBand) { + return this._createInBandTestRun(testPaths, onTestResult, onRunFailure); + } else { + return this._createParallelTestRun(testPaths, onTestResult, onRunFailure); + } +}; + +TestRunner.prototype._createInBandTestRun = function( + testPaths, onTestResult, onRunFailure +) { var testSequence = q(); testPaths.forEach(function(testPath) { testSequence = testSequence.then(this.runTest.bind(this, testPath)) .then(function(testResult) { - if (testResult.numFailingTests > 0) { - aggregatedResults.numFailedTests++; - } - onResult && onResult(config, testResult); + onTestResult(testPath, testResult); }) .catch(function(err) { - aggregatedResults.numFailedTests++; - onResult && onResult(config, { - testFilePath: testPath, - testExecError: err, - suites: {}, - tests: {}, - logMessages: [] - }); + onRunFailure(testPath, err); }); }, this); - - return testSequence.then(function() { - aggregatedResults.endTime = Date.now(); - return aggregatedResults; - }); + return testSequence; }; -/** - * Run all given test paths in parallel using a worker pool. - * - * @param {Array} testPaths Array of paths to test files - * @param {Function} onResult Callback called once for each test result - * @return {Promise} Fulfilled with aggregate pass/fail information - * about all tests that were run - */ -TestRunner.prototype.runTestsParallel = function(testPaths, onResult) { - var config = this._config; - - var aggregatedResults = { - numFailedTests: 0, - numTotalTests: testPaths.length, - startTime: Date.now(), - endTime: null - }; - +TestRunner.prototype._createParallelTestRun = function( + testPaths, onTestResult, onRunFailure +) { var workerPool = new WorkerPool( this._opts.maxWorkers, this._opts.nodePath, this._opts.nodeArgv.concat([ '--harmony', TEST_WORKER_PATH, - '--config=' + JSON.stringify(config) + '--config=' + JSON.stringify(this._config) ]) ); @@ -451,20 +480,10 @@ TestRunner.prototype.runTestsParallel = function(testPaths, onResult) { return q.all(testPaths.map(function(testPath) { return workerPool.sendMessage({testFilePath: testPath}) .then(function(testResult) { - if (testResult.numFailingTests > 0) { - aggregatedResults.numFailedTests++; - } - onResult && onResult(config, testResult); + onTestResult(testPath, testResult); }) .catch(function(err) { - aggregatedResults.numFailedTests++; - onResult(config, { - testFilePath: testPath, - testExecError: err.stack || err.message || err, - suites: {}, - tests: {}, - logMessages: [] - }); + onRunFailure(testPath, err); // Jest uses regular worker messages to initialize workers, so // there's no way for node-worker-pool to understand how to @@ -495,10 +514,7 @@ TestRunner.prototype.runTestsParallel = function(testPaths, onResult) { })); }) .then(function() { - return workerPool.destroy().then(function() { - aggregatedResults.endTime = Date.now(); - return aggregatedResults; - }); + return workerPool.destroy(); }); }; diff --git a/src/defaultTestResultHandler.js b/src/defaultTestReporter.js similarity index 68% rename from src/defaultTestResultHandler.js rename to src/defaultTestReporter.js index 72d5494faa88..3cfba64136d2 100644 --- a/src/defaultTestResultHandler.js +++ b/src/defaultTestReporter.js @@ -53,7 +53,33 @@ function _getResultHeader(passed, testName, columns) { ].concat(columns || []).join(' '); } -function defaultTestResultHandler(config, testResult) { +function _printWaitingOn(aggregatedResults) { + var completedTests = + aggregatedResults.numPassedTests + + aggregatedResults.numFailedTests; + var remainingTests = aggregatedResults.numTotalTests - completedTests; + if (remainingTests > 0) { + var pluralTests = remainingTests === 1 ? 'test' : 'tests'; + process.stdout.write( + colors.colorize( + 'Waiting on ' + remainingTests + ' ' + pluralTests + '...', + colors.GRAY + colors.BOLD + ) + ); + } +} + +function _clearWaitingOn() { + process.stdout.write('\r\x1B[K'); +} + +function onRunStart(config, aggregatedResults) { + _printWaitingOn(aggregatedResults); +} + +function onTestResult(config, testResult, aggregatedResults) { + _clearWaitingOn(); + var pathStr = config.rootDir ? path.relative(config.rootDir, testResult.testFilePath) @@ -125,6 +151,36 @@ function defaultTestResultHandler(config, testResult) { }); }); } + + _printWaitingOn(aggregatedResults); +} + +function onRunComplete(config, aggregatedResults) { + var numFailedTests = aggregatedResults.numFailedTests; + var numPassedTests = aggregatedResults.numPassedTests; + var numTotalTests = aggregatedResults.numTotalTests; + var startTime = aggregatedResults.startTime; + var endTime = aggregatedResults.endTime; + + var results = ''; + if (numFailedTests) { + results += colors.colorize( + numFailedTests + ' test' + (numFailedTests === 1 ? '' : 's') + ' failed', + colors.RED + colors.BOLD + ); + results += ', '; + } + results += colors.colorize( + numPassedTests + ' test' + (numPassedTests === 1 ? '' : 's') + ' passed', + colors.GREEN + colors.BOLD + ); + results += ' (' + numTotalTests + ' total)'; + + console.log(results); + console.log('Run time: ' + ((endTime - startTime) / 1000) + 's'); } -module.exports = defaultTestResultHandler; + +exports.onRunStart = onRunStart; +exports.onTestResult = onTestResult; +exports.onRunComplete = onRunComplete; diff --git a/src/jest.js b/src/jest.js new file mode 100644 index 000000000000..ce8e8fb7a6a1 --- /dev/null +++ b/src/jest.js @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var childProcess = require('child_process'); +var fs = require('fs'); +var path = require('path'); +var q = require('q'); +var TestRunner = require('./TestRunner'); +var utils = require('./lib/utils'); + +var _jestVersion = null; +function getVersion() { + if (_jestVersion === null) { + var pkgJsonPath = path.resolve(__dirname, '..', 'package.json'); + _jestVersion = require(pkgJsonPath).version; + } + return _jestVersion; +} + +function _findChangedFiles(dirPath) { + var deferred = q.defer(); + + var args = + ['diff', '--name-only', '--diff-filter=ACMR']; + var child = childProcess.spawn('git', args, {cwd: dirPath}); + + var stdout = ''; + child.stdout.on('data', function(data) { + stdout += data; + }); + + var stderr = ''; + child.stderr.on('data', function(data) { + stderr += data; + }); + + child.on('close', function(code) { + if (code === 0) { + stdout = stdout.trim(); + if (stdout === '') { + deferred.resolve([]); + } else { + deferred.resolve(stdout.split('\n').map(function(changedPath) { + return path.resolve(dirPath, changedPath); + })); + } + } else { + deferred.reject(code + ': ' + stderr); + } + }); + + return deferred.promise; +} + +function _verifyIsGitRepository(dirPath) { + var deferred = q.defer(); + + childProcess.spawn('git', ['rev-parse', '--git-dir'], {cwd: dirPath}) + .on('close', function(code) { + var isGitRepo = code === 0; + deferred.resolve(isGitRepo); + }); + + return deferred.promise; +} + +function _testRunnerOptions(argv) { + var options = {}; + if (argv.runInBand) { + options.runInBand = argv.runInBand; + } + if (argv.maxWorkers) { + options.maxWorkers = argv.maxWorkers; + } + return options; +} + +function _promiseConfig(argv, packageRoot) { + return _promiseRawConfig(argv, packageRoot).then(function (config) { + if (argv.coverage) { + config.collectCoverage = true; + } + return config; + }); +} + +function _promiseRawConfig(argv, packageRoot) { + if (typeof argv.config === 'string') { + return utils.loadConfigFromFile(argv.config); + } + + if (typeof argv.config === 'object') { + return q(utils.normalizeConfig(argv.config)); + } + + var pkgJsonPath = path.join(packageRoot, 'package.json'); + var pkgJson = fs.existsSync(pkgJsonPath) ? require(pkgJsonPath) : {}; + + // Look to see if there is a package.json file with a jest config in it + if (pkgJson.jest) { + if (!pkgJson.jest.hasOwnProperty('rootDir')) { + pkgJson.jest.rootDir = packageRoot; + } else { + pkgJson.jest.rootDir = path.resolve(packageRoot, pkgJson.jest.rootDir); + } + var config = utils.normalizeConfig(pkgJson.jest); + config.name = pkgJson.name; + return q(config); + } + + // Sane default config + return q(utils.normalizeConfig({ + name: packageRoot.replace(/[/\\]/g, '_'), + rootDir: packageRoot, + testPathDirs: [packageRoot], + testPathIgnorePatterns: ['/node_modules/.+'] + })); +} + +function _promiseTestPaths(argv, testRunner, config) { + var testPathStreamPromise = argv.onlyChanged ? + _promiseStreamOnlyChangedTestPaths(testRunner, config) : + _promiseStreamPatternMatchingTestPaths(argv, testRunner); + + return testPathStreamPromise.then(function (testPathStream) { + var testPaths = q.defer(); + + var foundPaths = []; + testPathStream.on('data', function(pathStr) { + foundPaths.push(pathStr); + }); + testPathStream.on('error', function(err) { + testPaths.reject(err); + }); + testPathStream.on('end', function() { + testPaths.resolve(foundPaths); + }); + + return testPaths.promise; + }); +} + +function _promiseStreamOnlyChangedTestPaths(testRunner, config) { + var testPathDirsAreGit = config.testPathDirs.map(_verifyIsGitRepository); + return q.all(testPathDirsAreGit) + .then(function(results) { + if (!results.every(function(result) { return result; })) { + throw ( + 'It appears that one of your testPathDirs does not exist ' + + 'with in a git repository. Currently --onlyChanged only works ' + + 'with git projects.\n' + ); + } + + return q.all(config.testPathDirs.map(_findChangedFiles)); + }) + .then(function(changedPathSets) { + // Collapse changed files from each of the testPathDirs into a single list + // of changed file paths + var changedPaths = []; + changedPathSets.forEach(function(pathSet) { + changedPaths = changedPaths.concat(pathSet); + }); + return testRunner.streamTestPathsRelatedTo(changedPaths); + }); +} + +function _promiseStreamPatternMatchingTestPaths(argv, testRunner) { + return q(testRunner.streamTestPathsMatching( + argv.testPathPattern || + (argv._ && argv._.length ? new RegExp(argv._.join('|')) : /.*/) + )); +} + +function runCLI(argv, packageRoot, onComplete) { + argv = argv || {}; + + if (argv.version) { + console.log('v' + getVersion()); + onComplete && onComplete(true); + return; + } + + _promiseConfig(argv, packageRoot).then(function(config) { + var testRunner = new TestRunner(config, _testRunnerOptions(argv)); + var testPaths = _promiseTestPaths(argv, testRunner, config); + return testPaths.then(function (testPaths) { + return testRunner.runTests(testPaths); + }); + }).then(function (didRunSucceed) { + onComplete && onComplete(didRunSucceed); + }).catch(function (error) { + console.error('Failed with unexpected error.'); + process.nextTick(function () { + throw error; + }); + }); +} + +exports.TestRunner = TestRunner; +exports.getVersion = getVersion; +exports.runCLI = runCLI;