diff --git a/packages/jest-cli/src/TestRunner.js b/packages/jest-cli/src/TestRunner.js index dc2ce2c7f96e..f512abe7d019 100644 --- a/packages/jest-cli/src/TestRunner.js +++ b/packages/jest-cli/src/TestRunner.js @@ -92,7 +92,6 @@ class TestRunner { } }); - const config = this._config; const aggregatedResults = createAggregatedResults(tests.length); const estimatedTime = Math.ceil( getEstimatedTime(timings, this._options.maxWorkers) / 1000, @@ -120,7 +119,7 @@ class TestRunner { return; } addResult(aggregatedResults, testResult); - this._dispatcher.onTestResult(config, testResult, aggregatedResults); + this._dispatcher.onTestResult(test.config, testResult, aggregatedResults); this._bailIfNeeded(aggregatedResults, watcher); }; @@ -135,10 +134,11 @@ class TestRunner { test.path, ); addResult(aggregatedResults, testResult); - this._dispatcher.onTestResult(config, testResult, aggregatedResults); + this._dispatcher.onTestResult(test.config, testResult, aggregatedResults); }; const updateSnapshotState = () => { + const config = this._config; const status = snapshot.cleanup( this._context.hasteFS, config.updateSnapshot, @@ -151,7 +151,7 @@ class TestRunner { aggregatedResults.snapshot.filesRemoved)); }; - this._dispatcher.onRunStart(config, aggregatedResults, { + this._dispatcher.onRunStart(this._config, aggregatedResults, { estimatedTime, showStatus: !runInBand, }); @@ -169,7 +169,7 @@ class TestRunner { updateSnapshotState(); aggregatedResults.wasInterrupted = watcher.isInterrupted(); - this._dispatcher.onRunComplete(config, aggregatedResults); + this._dispatcher.onRunComplete(this._config, aggregatedResults); const anyTestFailures = !(aggregatedResults.numFailedTests === 0 && aggregatedResults.numRuntimeErrorTestSuites === 0); @@ -190,17 +190,19 @@ class TestRunner { ) { const mutex = throat(1); return tests.reduce( - (promise, test) => mutex(() => promise - .then(() => { - if (watcher.isInterrupted()) { - throw new CancelRun(); - } - - this._dispatcher.onTestStart(test.config, test.path); - return runTest(test.path, test.config, this._context.resolver); - }) - .then(result => onResult(test, result)) - .catch(err => onFailure(test, err))), + (promise, test) => + mutex(() => + promise + .then(() => { + if (watcher.isInterrupted()) { + throw new CancelRun(); + } + + this._dispatcher.onTestStart(test.config, test.path); + return runTest(test.path, test.config, this._context.resolver); + }) + .then(result => onResult(test, result)) + .catch(err => onFailure(test, err))), Promise.resolve(), ); } @@ -225,19 +227,20 @@ class TestRunner { // Send test suites to workers continuously instead of all at once to track // the start time of individual tests. - const runTestInWorker = ({config, path}) => mutex(() => { - if (watcher.isInterrupted()) { - return Promise.reject(); - } - this._dispatcher.onTestStart(config, path); - return worker({ - config, - path, - rawModuleMap: watcher.isWatchMode() - ? this._context.moduleMap.getRawModuleMap() - : null, + const runTestInWorker = ({config, path}) => + mutex(() => { + if (watcher.isInterrupted()) { + return Promise.reject(); + } + this._dispatcher.onTestStart(config, path); + return worker({ + config, + path, + rawModuleMap: watcher.isWatchMode() + ? this._context.moduleMap.getRawModuleMap() + : null, + }); }); - }); const onError = (err, test) => { onFailure(test, err); diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index aee0ec265809..52f7ae706756 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -113,6 +113,11 @@ const options = { description: 'Use this flag to show full diffs instead of a patch.', type: 'boolean', }, + experimentalProjects: { + description: 'A list of projects that use Jest to run all tests in a ' + + 'single run.', + type: 'array', + }, findRelatedTests: { description: 'Find related tests for a list of source files that were ' + 'passed in as arguments. Useful for pre-commit hook integration to run ' + diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 8819bf08b236..b438957acff2 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -40,7 +40,13 @@ function run(argv?: Object, root?: Path) { root = pkgDir.sync(); } - getJest(root).runCLI(argv, root, result => { + argv.projects = argv.experimentalProjects; + if (!argv.projects) { + argv.projects = [root]; + } + + const execute = argv.projects.length === 1 ? getJest(root).runCLI : runCLI; + execute(argv, argv.projects, result => { const code = !result || result.success ? 0 : 1; process.on('exit', () => process.exit(code)); if (argv && argv.forceExit) { diff --git a/packages/jest-cli/src/cli/runCLI.js b/packages/jest-cli/src/cli/runCLI.js index b21df53d1b27..e8f63df1f900 100644 --- a/packages/jest-cli/src/cli/runCLI.js +++ b/packages/jest-cli/src/cli/runCLI.js @@ -10,7 +10,7 @@ 'use strict'; import type {AggregatedResult} from 'types/TestResult'; -import type {Path} from 'types/Config'; +import type {Config, Path} from 'types/Config'; const Runtime = require('jest-runtime'); @@ -28,9 +28,9 @@ const watch = require('../watch'); const VERSION = require('../../package.json').version; -module.exports = ( +module.exports = async ( argv: Object, - root: Path, + roots: Array, onComplete: (results: ?AggregatedResult) => void, ) => { const realFs = require('fs'); @@ -45,52 +45,66 @@ module.exports = ( return; } - const _run = async ({config, hasDeprecationWarnings}) => { + const _run = async ( + configs: Array<{config: Config, hasDeprecationWarnings: boolean}>, + ) => { if (argv.debug) { - logDebugMessages(config, pipe); + // TODO fix/remove this: there should be a `--show-config` argument. + logDebugMessages(configs[0].config, pipe); } - createDirectory(config.cacheDirectory); - const hasteMapInstance = Runtime.createHasteMap(config, { - console: new Console(pipe, pipe), - maxWorkers: getMaxWorkers(argv), - resetCache: !config.cache, - watch: config.watch, - }); - - const hasteMap = await hasteMapInstance.build(); - const hasteContext = createHasteContext(config, hasteMap); if (argv.watch || argv.watchAll) { + const {config} = configs[0]; + createDirectory(config.cacheDirectory); + const hasteMapInstance = Runtime.createHasteMap(config, { + console: new Console(pipe, pipe), + maxWorkers: getMaxWorkers(argv), + resetCache: !config.cache, + watch: config.watch, + }); + + const hasteMap = await hasteMapInstance.build(); + const hasteContext = createTestContext(config, hasteMap); return watch( config, pipe, argv, hasteMapInstance, hasteContext, - hasDeprecationWarnings, + // TODO + configs[0].hasDeprecationWarnings, ); } else { + const contexts = await Promise.all( + configs.map(async ({config}) => { + createDirectory(config.cacheDirectory); + return createTestContext( + config, + await Runtime.createHasteMap(config, { + console: new Console(pipe, pipe), + maxWorkers: getMaxWorkers(argv), + resetCache: !config.cache, + watch: config.watch, + }).build(), + ); + }), + ); + const startRun = () => { preRunMessage.print(pipe); const testWatcher = new TestWatcher({isWatchMode: false}); - return runJest( - hasteContext, - config, - argv, - pipe, - testWatcher, - startRun, - onComplete, - ); + runJest(contexts, argv, pipe, testWatcher, startRun, onComplete); }; return startRun(); } }; - readConfig(argv, root).then(_run).catch(error => { + try { + await _run(await Promise.all(roots.map(root => readConfig(argv, root)))); + } catch (error) { clearLine(process.stderr); clearLine(process.stdout); console.error(chalk.red(error.stack)); process.exit(1); - }); + } }; diff --git a/packages/jest-cli/src/runJest.js b/packages/jest-cli/src/runJest.js index f502d8a8d301..e5c751ae36ec 100644 --- a/packages/jest-cli/src/runJest.js +++ b/packages/jest-cli/src/runJest.js @@ -9,9 +9,9 @@ */ 'use strict'; -import type {Config} from 'types/Config'; import type {TestContext, Test} from 'types/TestRunner'; import type {PatternInfo} from './SearchSource'; +import type TestWatcher from './TestWatcher'; const fs = require('graceful-fs'); @@ -42,72 +42,82 @@ const getTestSummary = (argv: Object, patternInfo: PatternInfo) => { chalk.dim('.'); }; +const getTestPaths = async (context, patternInfo, argv, pipe) => { + const source = new SearchSource(context, context.config); + let data = await source.getTestPaths(patternInfo); + if (!data.paths.length) { + const localConsole = new Console(pipe, pipe); + if (patternInfo.onlyChanged && data.noSCM) { + if (context.config.watch) { + // Run all the tests + setState(argv, 'watchAll', { + noSCM: true, + }); + patternInfo = getTestPathPatternInfo(argv); + data = await source.getTestPaths(patternInfo); + } else { + localConsole.log( + 'Jest can only find uncommitted changed files in a git or hg ' + + 'repository. If you make your project a git or hg ' + + 'repository (`git init` or `hg init`), Jest will be able ' + + 'to only run tests related to files changed since the last ' + + 'commit.', + ); + } + } + + localConsole.log( + source.getNoTestsFoundMessage(patternInfo, context.config, data), + ); + } + + return { + data, + patternInfo, + }; +}; + const runJest = async ( - hasteContext: HasteContext, - config: Config, + contexts: Array, argv: Object, pipe: stream$Writable | tty$WriteStream, - testWatcher: any, + testWatcher: TestWatcher, startRun: () => *, onComplete: (testResults: any) => void, ) => { const maxWorkers = getMaxWorkers(argv); - const source = new SearchSource(hasteContext, config); - let patternInfo = getTestPathPatternInfo(argv); - - const processTests = data => { - if (!data.paths.length) { - const localConsole = new Console(pipe, pipe); - if (patternInfo.onlyChanged && data.noSCM) { - if (config.watch) { - // Run all the tests - setState(argv, 'watchAll', { - noSCM: true, - }); - patternInfo = getTestPathPatternInfo(argv); - return source.getTestPaths(patternInfo); - } else { - localConsole.log( - 'Jest can only find uncommitted changed files in a git or hg ' + - 'repository. If you make your project a git or hg ' + - 'repository (`git init` or `hg init`), Jest will be able ' + - 'to only run tests related to files changed since the last ' + - 'commit.', - ); - } - } - - localConsole.log( - source.getNoTestsFoundMessage(patternInfo, config, data), + // TODO + const context = contexts[0]; + const testRunData = await Promise.all( + contexts.map(async context => { + const config = context.config; + const {data, patternInfo} = await getTestPaths( + context, + getTestPathPatternInfo(argv), + argv, + pipe, ); - } - - if ( - data.paths.length === 1 && - config.silent !== true && - config.verbose !== false - ) { - // $FlowFixMe - config = Object.assign({}, config, {verbose: true}); - } - - return data; - }; - - const runTests = async tests => new TestRunner( - hasteContext, - config, - { - getTestSummary: () => getTestSummary(argv, patternInfo), - maxWorkers, - }, - startRun, - ).runTests(tests, testWatcher); + const sequencer = new TestSequencer(config); + const tests = sequencer.sort(data.paths); + return {context, patternInfo, sequencer, tests}; + }), + ); + + const runTests = async tests => + new TestRunner( + context, + context.config, + { + getTestSummary: () => getTestSummary(argv, testRunData[0].patternInfo), + maxWorkers, + }, + startRun, + ).runTests(tests, testWatcher); const processResults = runResults => { - if (config.testResultsProcessor) { + if (context.config.testResultsProcessor) { /* $FlowFixMe */ - runResults = require(config.testResultsProcessor)(runResults); + runResults = require(context.config.testResultsProcessor)(runResults); } if (argv.json) { if (argv.outputFile) { @@ -128,12 +138,26 @@ const runJest = async ( return onComplete && onComplete(runResults); }; - const data = await source.getTestPaths(patternInfo); - processTests(data); - const sequencer = new TestSequencer(config); - const tests = sequencer.sort(data.paths); - const results = await runTests(tests); - sequencer.cacheResults(tests, results); + const allTests = testRunData + .reduce((tests, testRun) => tests.concat(testRun.tests), []) + .sort((a: Test, b: Test) => { + if (a.duration != null && b.duration != null) { + return a.duration < b.duration ? 1 : -1; + } + return a.duration == null ? 1 : 0; + }); + + if ( + allTests.length === 1 && + context.config.silent !== true && + context.config.verbose !== false + ) { + context.config = Object.assign({}, context.config, {verbose: true}); + } + + const results = await runTests(allTests); + testRunData.forEach(({sequencer, tests}) => + sequencer.cacheResults(tests, results)); return processResults(results); }; diff --git a/packages/jest-cli/src/watch.js b/packages/jest-cli/src/watch.js index 10a25e47f3c4..c1fd373c4b3b 100644 --- a/packages/jest-cli/src/watch.js +++ b/packages/jest-cli/src/watch.js @@ -89,20 +89,20 @@ const watch = ( pipe.write(CLEAR); preRunMessage.print(pipe); isRunning = true; - return runJest( - hasteContext, + // $FlowFixMe + hasteContext.config = Object.freeze( // $FlowFixMe - Object.freeze( - // $FlowFixMe - Object.assign( - { - testNamePattern: argv.testNamePattern, - testPathPattern: argv.testPathPattern, - }, - config, - overrideConfig, - ), + Object.assign( + { + testNamePattern: argv.testNamePattern, + testPathPattern: argv.testPathPattern, + }, + config, + overrideConfig, ), + ); + return runJest( + [hasteContext], argv, pipe, testWatcher,