From 8835c3903c206cb1f0fe2e4c0ec8f899b31c0fd6 Mon Sep 17 00:00:00 2001 From: Christoph Pojer Date: Fri, 17 Mar 2017 15:26:14 +0900 Subject: [PATCH] Refactor runner (#3166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve runJest by breaking up the promise chain into functions. * Use async/await. * Add a TestSequencer and “Test” type to clarify responsibilities and improve type interfaces. * async/await in runTests. * Minor cleanups * Fix the test. * Move `runCLI` into the cli folder. * Updates to TestSequencer + test the code. --- packages/jest-cli/src/TestRunner.js | 260 ++++++------------ packages/jest-cli/src/TestSequencer.js | 111 ++++++++ .../jest-cli/src/__tests__/TestRunner-test.js | 4 +- .../src/__tests__/TestSequencer-test.js | 134 +++++++++ packages/jest-cli/src/cli/index.js | 2 + packages/jest-cli/src/cli/runCLI.js | 96 +++++++ packages/jest-cli/src/jest.js | 99 +------ packages/jest-cli/src/runJest.js | 152 +++++----- types/TestRunner.js | 20 ++ 9 files changed, 532 insertions(+), 346 deletions(-) create mode 100644 packages/jest-cli/src/TestSequencer.js create mode 100644 packages/jest-cli/src/__tests__/TestSequencer-test.js create mode 100644 packages/jest-cli/src/cli/runCLI.js create mode 100644 types/TestRunner.js diff --git a/packages/jest-cli/src/TestRunner.js b/packages/jest-cli/src/TestRunner.js index 10a26b11f75b..ff2b679cd737 100644 --- a/packages/jest-cli/src/TestRunner.js +++ b/packages/jest-cli/src/TestRunner.js @@ -14,14 +14,14 @@ import type { SerializableError as TestError, TestResult, } from 'types/TestResult'; -import type {Config, Path} from 'types/Config'; +import type {Config} from 'types/Config'; import type {HasteContext, HasteFS} from 'types/HasteMap'; import type {RunnerContext} from 'types/Reporters'; +import type {Test, Tests} from 'types/TestRunner'; import type BaseReporter from './reporters/BaseReporter'; const {formatExecError} = require('jest-message-util'); -const fs = require('graceful-fs'); -const getCacheFilePath = require('jest-haste-map').getCacheFilePath; + const DefaultReporter = require('./reporters/DefaultReporter'); const NotifyReporter = require('./reporters/NotifyReporter'); const SummaryReporter = require('./reporters/SummaryReporter'); @@ -33,9 +33,7 @@ const throat = require('throat'); const workerFarm = require('worker-farm'); const TestWatcher = require('./TestWatcher'); -const FAIL = 0; const SLOW_TEST_TIME = 3000; -const SUCCESS = 1; class CancelRun extends Error { constructor(message: ?string) { @@ -49,9 +47,8 @@ type Options = {| getTestSummary: () => string, |}; -type OnRunFailure = (path: string, err: TestError) => void; - -type OnTestResult = (path: string, result: TestResult) => void; +type OnTestFailure = (test: Test, err: TestError) => void; +type OnTestSuccess = (test: Test, result: TestResult) => void; const TEST_WORKER_PATH = require.resolve('./TestWorker'); @@ -61,7 +58,6 @@ class TestRunner { _options: Options; _startRun: () => *; _dispatcher: ReporterDispatcher; - _testPerformanceCache: Object; constructor( hasteContext: HasteContext, @@ -78,10 +74,6 @@ class TestRunner { this._options = options; this._startRun = startRun; this._setupReporters(); - - // Map from testFilePath -> time it takes to run the test. Used to - // optimally schedule bigger test runs. - this._testPerformanceCache = {}; } addReporter(reporter: BaseReporter) { @@ -92,95 +84,36 @@ class TestRunner { this._dispatcher.unregister(ReporterClass); } - _getTestPerformanceCachePath() { - const config = this._config; - return getCacheFilePath(config.cacheDirectory, 'perf-cache-' + config.name); - } - - _sortTests(testPaths: Array) { - // When running more tests than we have workers available, sort the tests - // by size - big test files usually take longer to complete, so we run - // them first in an effort to minimize worker idle time at the end of a - // long test run. - // - // After a test run we store the time it took to run a test and on - // subsequent runs we use that to run the slowest tests first, yielding the - // fastest results. - try { - if (this._config.cache) { - this._testPerformanceCache = JSON.parse( - fs.readFileSync(this._getTestPerformanceCachePath(), 'utf8'), - ); - } else { - this._testPerformanceCache = {}; - } - } catch (e) { - this._testPerformanceCache = {}; - } - - const cache = this._testPerformanceCache; + async runTests(tests: Tests, watcher: TestWatcher) { const timings = []; - const stats = {}; - const getFileSize = filePath => - stats[filePath] || (stats[filePath] = fs.statSync(filePath).size); - const getTestRunTime = filePath => { - if (cache[filePath]) { - return cache[filePath][0] === FAIL ? Infinity : cache[filePath][1]; - } - return null; - }; - - testPaths = testPaths.sort((pathA, pathB) => { - const timeA = getTestRunTime(pathA); - const timeB = getTestRunTime(pathB); - if (timeA != null && timeB != null) { - return timeA < timeB ? 1 : -1; - } - return getFileSize(pathA) < getFileSize(pathB) ? 1 : -1; - }); - - testPaths.forEach(filePath => { - const timing = cache[filePath] && cache[filePath][1]; - if (timing) { - timings.push(timing); + tests.forEach(test => { + if (test.duration) { + timings.push(test.duration); } }); - return {testPaths, timings}; - } - - _cacheTestResults(aggregatedResults: AggregatedResult) { - const cache = this._testPerformanceCache; - aggregatedResults.testResults.forEach(test => { - if (test && !test.skipped) { - const perf = test.perfStats; - cache[test.testFilePath] = [ - test.numFailingTests ? FAIL : SUCCESS, - perf.end - perf.start || 0, - ]; - } - }); - fs.writeFileSync( - this._getTestPerformanceCachePath(), - JSON.stringify(cache), - ); - } - - runTests(paths: Array, watcher: TestWatcher) { const config = this._config; - const {testPaths, timings} = this._sortTests(paths); - const aggregatedResults = createAggregatedResults(testPaths.length); + const aggregatedResults = createAggregatedResults(tests.length); const estimatedTime = Math.ceil( getEstimatedTime(timings, this._options.maxWorkers) / 1000, ); - const onResult = (testPath: Path, testResult: TestResult) => { + // Run in band if we only have one test or one worker available. + // If we are confident from previous runs that the tests will finish quickly + // we also run in band to reduce the overhead of spawning workers. + const runInBand = this._options.maxWorkers <= 1 || + tests.length <= 1 || + (tests.length <= 20 && + timings.length > 0 && + timings.every(timing => timing < SLOW_TEST_TIME)); + + const onResult = (test: Test, testResult: TestResult) => { if (watcher.isInterrupted()) { return; } if (testResult.testResults.length === 0) { const message = 'Your test suite must contain at least one test.'; - onFailure(testPath, { + onFailure(test, { message, stack: new Error(message).stack, }); @@ -191,26 +124,20 @@ class TestRunner { this._bailIfNeeded(aggregatedResults, watcher); }; - const onFailure = (testPath: Path, error: TestError) => { + const onFailure = (test: Test, error: TestError) => { if (watcher.isInterrupted()) { return; } - const testResult = buildFailureTestResult(testPath, error); - testResult.failureMessage = formatExecError(testResult, config, testPath); + const testResult = buildFailureTestResult(test.path, error); + testResult.failureMessage = formatExecError( + testResult, + test.config, + test.path, + ); addResult(aggregatedResults, testResult); this._dispatcher.onTestResult(config, testResult, aggregatedResults); }; - // Run in band if we only have one test or one worker available. - // If we are confident from previous runs that the tests will finish quickly - // we also run in band to reduce the overhead of spawning workers. - const shouldRunInBand = () => - this._options.maxWorkers <= 1 || - testPaths.length <= 1 || - (testPaths.length <= 20 && - timings.length > 0 && - timings.every(timing => timing < SLOW_TEST_TIME)); - const updateSnapshotState = () => { const status = snapshot.cleanup( this._hasteContext.hasteFS, @@ -224,74 +151,66 @@ class TestRunner { aggregatedResults.snapshot.filesRemoved)); }; - const runInBand = shouldRunInBand(); - this._dispatcher.onRunStart(config, aggregatedResults, { estimatedTime, showStatus: !runInBand, }); - const testRun = runInBand - ? this._createInBandTestRun(testPaths, watcher, onResult, onFailure) - : this._createParallelTestRun(testPaths, watcher, onResult, onFailure); + try { + await (runInBand + ? this._createInBandTestRun(tests, watcher, onResult, onFailure) + : this._createParallelTestRun(tests, watcher, onResult, onFailure)); + } catch (error) { + if (!watcher.isInterrupted()) { + throw error; + } + } - return testRun - .catch(error => { - if (!watcher.isInterrupted()) { - throw error; - } - }) - .then(() => { - updateSnapshotState(); - aggregatedResults.wasInterrupted = watcher.isInterrupted(); + updateSnapshotState(); + aggregatedResults.wasInterrupted = watcher.isInterrupted(); - this._dispatcher.onRunComplete(config, aggregatedResults); + this._dispatcher.onRunComplete(config, aggregatedResults); - const anyTestFailures = !(aggregatedResults.numFailedTests === 0 && - aggregatedResults.numRuntimeErrorTestSuites === 0); - const anyReporterErrors = this._dispatcher.hasErrors(); + const anyTestFailures = !(aggregatedResults.numFailedTests === 0 && + aggregatedResults.numRuntimeErrorTestSuites === 0); + const anyReporterErrors = this._dispatcher.hasErrors(); - aggregatedResults.success = !(anyTestFailures || - aggregatedResults.snapshot.failure || - anyReporterErrors); + aggregatedResults.success = !(anyTestFailures || + aggregatedResults.snapshot.failure || + anyReporterErrors); - this._cacheTestResults(aggregatedResults); - return aggregatedResults; - }); + return aggregatedResults; } _createInBandTestRun( - testPaths: Array, + tests: Tests, watcher: TestWatcher, - onResult: OnTestResult, - onFailure: OnRunFailure, + onResult: OnTestSuccess, + onFailure: OnTestFailure, ) { const mutex = throat(1); - return testPaths.reduce( - (promise, path) => - mutex(() => - promise - .then(() => { - if (watcher.isInterrupted()) { - throw new CancelRun(); - } - - this._dispatcher.onTestStart(this._config, path); - return runTest(path, this._config, this._hasteContext.resolver); - }) - .then(result => onResult(path, result)) - .catch(err => onFailure(path, err))), + 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._hasteContext.resolver); + }) + .then(result => onResult(test, result)) + .catch(err => onFailure(test, err))), Promise.resolve(), ); } _createParallelTestRun( - testPaths: Array, + tests: Tests, watcher: TestWatcher, - onResult: OnTestResult, - onFailure: OnRunFailure, + onResult: OnTestSuccess, + onFailure: OnTestFailure, ) { - const config = this._config; const farm = workerFarm( { autoStart: true, @@ -306,23 +225,22 @@ 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._hasteContext.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._hasteContext.moduleMap.getRawModuleMap() + : null, }); + }); - const onError = (err, path) => { - onFailure(path, err); + const onError = (err, test) => { + onFailure(test, err); if (err.type === 'ProcessTerminatedError') { console.error( 'A worker process has quit unexpectedly! ' + @@ -341,15 +259,13 @@ class TestRunner { }); const runAllTests = Promise.all( - testPaths.map(path => { - return runTestInWorker({config, path}) - .then(testResult => onResult(path, testResult)) - .catch(error => onError(error, path)); - }), + tests.map(test => + runTestInWorker(test) + .then(testResult => onResult(test, testResult)) + .catch(error => onError(error, test))), ); const cleanup = () => workerFarm.end(farm); - return Promise.race([runAllTests, onInterrupt]).then(cleanup, cleanup); } @@ -499,8 +415,6 @@ const buildFailureTestResult = ( }; }; -// Proxy class that holds all reporter and dispatchers events to each -// of them. class ReporterDispatcher { _disabled: boolean; _reporters: Array; @@ -563,11 +477,9 @@ const getEstimatedTime = (timings, workers) => { } const max = Math.max.apply(null, timings); - if (timings.length <= workers) { - return max; - } - - return Math.max(timings.reduce((sum, time) => sum + time) / workers, max); + return timings.length <= workers + ? max + : Math.max(timings.reduce((sum, time) => sum + time) / workers, max); }; module.exports = TestRunner; diff --git a/packages/jest-cli/src/TestSequencer.js b/packages/jest-cli/src/TestSequencer.js new file mode 100644 index 000000000000..4af0d39add77 --- /dev/null +++ b/packages/jest-cli/src/TestSequencer.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2014-present, 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. + * + * @flow + */ +'use strict'; + +import type {AggregatedResult} from 'types/TestResult'; +import type {Config} from 'types/Config'; +import type {Tests} from 'types/TestRunner'; + +const fs = require('fs'); +const getCacheFilePath = require('jest-haste-map').getCacheFilePath; + +const FAIL = 0; +const SUCCESS = 1; + +class TestSequencer { + _config: Config; + _cache: Object; + + constructor(config: Config) { + this._config = config; + this._cache = {}; + } + + _getTestPerformanceCachePath() { + return getCacheFilePath( + this._config.cacheDirectory, + 'perf-cache-' + this._config.name, + ); + } + + // When running more tests than we have workers available, sort the tests + // by size - big test files usually take longer to complete, so we run + // them first in an effort to minimize worker idle time at the end of a + // long test run. + // + // After a test run we store the time it took to run a test and on + // subsequent runs we use that to run the slowest tests first, yielding the + // fastest results. + sort(testPaths: Array): Tests { + const config = this._config; + const stats = {}; + const fileSize = filePath => + stats[filePath] || (stats[filePath] = fs.statSync(filePath).size); + const failed = filePath => + this._cache[filePath] && this._cache[filePath][0] === FAIL; + const time = filePath => this._cache[filePath] && this._cache[filePath][1]; + + this._cache = {}; + try { + if (this._config.cache) { + this._cache = JSON.parse( + fs.readFileSync(this._getTestPerformanceCachePath(), 'utf8'), + ); + } + } catch (e) {} + + testPaths = testPaths.sort((pathA, pathB) => { + const failedA = failed(pathA); + const failedB = failed(pathB); + if (failedA !== failedB) { + return failedA ? -1 : 1; + } + const timeA = time(pathA); + const timeB = time(pathB); + const hasTimeA = timeA != null; + const hasTimeB = timeB != null; + // Check if only one of two tests has timing information + if (hasTimeA != hasTimeB) { + return hasTimeA ? 1 : -1; + } + if (timeA != null && !timeB != null) { + return timeA < timeB ? 1 : -1; + } + return fileSize(pathA) < fileSize(pathB) ? 1 : -1; + }); + + return testPaths.map(path => ({ + config, + duration: this._cache[path] && this._cache[path][1], + path, + })); + } + + cacheResults(tests: Tests, results: AggregatedResult) { + const cache = this._cache; + const map = Object.create(null); + tests.forEach(({path}) => map[path] = true); + results.testResults.forEach(testResult => { + if (testResult && map[testResult.testFilePath] && !testResult.skipped) { + const perf = testResult.perfStats; + cache[testResult.testFilePath] = [ + testResult.numFailingTests ? FAIL : SUCCESS, + perf.end - perf.start || 0, + ]; + } + }); + fs.writeFileSync( + this._getTestPerformanceCachePath(), + JSON.stringify(cache), + ); + } +} + +module.exports = TestSequencer; diff --git a/packages/jest-cli/src/__tests__/TestRunner-test.js b/packages/jest-cli/src/__tests__/TestRunner-test.js index 42a8c4efdeb9..a00188c525d3 100644 --- a/packages/jest-cli/src/__tests__/TestRunner-test.js +++ b/packages/jest-cli/src/__tests__/TestRunner-test.js @@ -48,7 +48,7 @@ describe('_createInBandTestRun()', () => { return runner ._createParallelTestRun( - ['./file-test.js', './file2-test.js'], + [{config, path: './file-test.js'}, {config, path: './file2-test.js'}], new TestWatcher({isWatchMode: config.watch}), () => {}, () => {}, @@ -75,7 +75,7 @@ describe('_createInBandTestRun()', () => { return runner ._createParallelTestRun( - ['./file-test.js', './file2-test.js'], + [{config, path: './file-test.js'}, {config, path: './file2-test.js'}], new TestWatcher({isWatchMode: config.watch}), () => {}, () => {}, diff --git a/packages/jest-cli/src/__tests__/TestSequencer-test.js b/packages/jest-cli/src/__tests__/TestSequencer-test.js new file mode 100644 index 000000000000..76054157897c --- /dev/null +++ b/packages/jest-cli/src/__tests__/TestSequencer-test.js @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2014-present, 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'; + +jest.mock('fs'); + +const TestSequencer = require('../TestSequencer'); + +const fs = require('fs'); + +const FAIL = 0; +const SUCCESS = 1; + +let sequencer; + +const config = { + cache: true, + cacheDirectory: '/cache', + name: 'test', +}; + +beforeEach(() => { + sequencer = new TestSequencer(config); + + fs.readFileSync = jest.fn(() => '{}'); + fs.statSync = jest.fn(filePath => ({size: filePath.length})); +}); + +test('sorts by file size if there is no timing information', () => { + expect(sequencer.sort(['/test-a.js', '/test-ab.js'])).toEqual([ + {config, duration: undefined, path: '/test-ab.js'}, + {config, duration: undefined, path: '/test-a.js'}, + ]); +}); + +test('sorts based on timing information', () => { + fs.readFileSync = jest.fn(() => JSON.stringify({ + '/test-a.js': [SUCCESS, 5], + '/test-ab.js': [SUCCESS, 3], + })); + expect(sequencer.sort(['/test-a.js', '/test-ab.js'])).toEqual([ + {config, duration: 5, path: '/test-a.js'}, + {config, duration: 3, path: '/test-ab.js'}, + ]); +}); + +test('sorts based on failures and timing information', () => { + fs.readFileSync = jest.fn(() => JSON.stringify({ + '/test-a.js': [SUCCESS, 5], + '/test-ab.js': [FAIL, 0], + '/test-c.js': [FAIL, 6], + '/test-d.js': [SUCCESS, 2], + })); + expect( + sequencer.sort(['/test-a.js', '/test-ab.js', '/test-c.js', '/test-d.js']), + ).toEqual([ + {config, duration: 6, path: '/test-c.js'}, + {config, duration: 0, path: '/test-ab.js'}, + {config, duration: 5, path: '/test-a.js'}, + {config, duration: 2, path: '/test-d.js'}, + ]); +}); + +test('sorts based on failures, timing information and file size', () => { + fs.readFileSync = jest.fn(() => JSON.stringify({ + '/test-a.js': [SUCCESS, 5], + '/test-ab.js': [FAIL, 1], + '/test-c.js': [FAIL], + '/test-d.js': [SUCCESS, 2], + '/test-efg.js': [FAIL], + })); + expect( + sequencer.sort([ + '/test-a.js', + '/test-ab.js', + '/test-c.js', + '/test-d.js', + '/test-efg.js', + ]), + ).toEqual([ + {config, duration: undefined, path: '/test-efg.js'}, + {config, duration: undefined, path: '/test-c.js'}, + {config, duration: 1, path: '/test-ab.js'}, + {config, duration: 5, path: '/test-a.js'}, + {config, duration: 2, path: '/test-d.js'}, + ]); +}); + +test('writes the cache based on the results', () => { + fs.readFileSync = jest.fn(() => JSON.stringify({ + '/test-a.js': [SUCCESS, 5], + '/test-b.js': [FAIL, 1], + '/test-c.js': [FAIL], + })); + + const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; + const tests = sequencer.sort(testPaths); + sequencer.cacheResults(tests, { + testResults: [ + { + numFailingTests: 0, + perfStats: {end: 2, start: 1}, + testFilePath: '/test-a.js', + }, + { + numFailingTests: 0, + perfStats: {end: 0, start: 0}, + skipped: true, + testFilePath: '/test-b.js', + }, + { + numFailingTests: 1, + perfStats: {end: 4, start: 1}, + testFilePath: '/test-c.js', + }, + { + numFailingTests: 1, + perfStats: {end: 2, start: 1}, + testFilePath: '/test-x.js', + }, + ], + }); + const fileData = JSON.parse(fs.writeFileSync.mock.calls[0][1]); + expect(fileData).toEqual({ + '/test-a.js': [SUCCESS, 1], + '/test-b.js': [FAIL, 1], + '/test-c.js': [FAIL, 3], + }); +}); diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 12a80c72c5e8..8819bf08b236 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -15,6 +15,7 @@ import type {Path} from 'types/Config'; const args = require('./args'); const getJest = require('./getJest'); const pkgDir = require('pkg-dir'); +const runCLI = require('./runCLI'); const validateCLIOptions = require('jest-util').validateCLIOptions; const yargs = require('yargs'); @@ -49,3 +50,4 @@ function run(argv?: Object, root?: Path) { } exports.run = run; +exports.runCLI = runCLI; diff --git a/packages/jest-cli/src/cli/runCLI.js b/packages/jest-cli/src/cli/runCLI.js new file mode 100644 index 000000000000..ed454bec763f --- /dev/null +++ b/packages/jest-cli/src/cli/runCLI.js @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2014-present, 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. + * + * @flow + */ +'use strict'; + +import type {AggregatedResult} from 'types/TestResult'; +import type {Path} from 'types/Config'; + +const Runtime = require('jest-runtime'); + +const chalk = require('chalk'); +const {Console, clearLine} = require('jest-util'); +const {createDirectory} = require('jest-util'); +const createHasteContext = require('../lib/createHasteContext'); +const getMaxWorkers = require('../lib/getMaxWorkers'); +const logDebugMessages = require('../lib/logDebugMessages'); +const preRunMessage = require('../preRunMessage'); +const readConfig = require('jest-config').readConfig; +const runJest = require('../runJest'); +const TestWatcher = require('../TestWatcher'); +const watch = require('../watch'); + +const VERSION = require('../../package.json').version; + +module.exports = ( + argv: Object, + root: Path, + onComplete: (results: ?AggregatedResult) => void, +) => { + const realFs = require('fs'); + const fs = require('graceful-fs'); + fs.gracefulify(realFs); + + const pipe = argv.json ? process.stderr : process.stdout; + argv = argv || {}; + if (argv.version) { + pipe.write(`v${VERSION}\n`); + onComplete && onComplete(); + return; + } + + const _run = async ({config, hasDeprecationWarnings}) => { + if (argv.debug) { + logDebugMessages(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) { + return watch( + config, + pipe, + argv, + hasteMapInstance, + hasteContext, + hasDeprecationWarnings, + ); + } else { + const startRun = () => { + preRunMessage.print(pipe); + const testWatcher = new TestWatcher({isWatchMode: false}); + return runJest( + hasteContext, + config, + argv, + pipe, + testWatcher, + startRun, + onComplete, + ); + }; + return startRun(); + } + }; + + readConfig(argv, root).then(_run).catch(error => { + clearLine(process.stderr); + clearLine(process.stdout); + console.error(chalk.red(error.stack)); + process.exit(1); + }); +}; diff --git a/packages/jest-cli/src/jest.js b/packages/jest-cli/src/jest.js index 154b5b2015e7..5d5998ec4b83 100644 --- a/packages/jest-cli/src/jest.js +++ b/packages/jest-cli/src/jest.js @@ -9,108 +9,13 @@ */ 'use strict'; -import type {AggregatedResult} from 'types/TestResult'; -import type {Path, Config} from 'types/Config'; - -const realFs = require('fs'); -const fs = require('graceful-fs'); -fs.gracefulify(realFs); - -const Runtime = require('jest-runtime'); const SearchSource = require('./SearchSource'); const TestRunner = require('./TestRunner'); - -const chalk = require('chalk'); -const {Console, clearLine} = require('jest-util'); -const {createDirectory} = require('jest-util'); -const createHasteContext = require('./lib/createHasteContext'); -const getMaxWorkers = require('./lib/getMaxWorkers'); -const logDebugMessages = require('./lib/logDebugMessages'); -const preRunMessage = require('./preRunMessage'); -const readConfig = require('jest-config').readConfig; -const {run} = require('./cli'); -const runJest = require('./runJest'); const TestWatcher = require('./TestWatcher'); -const watch = require('./watch'); - -const VERSION = require('../package.json').version; -const runCLI = ( - argv: Object, - root: Path, - onComplete: (results: ?AggregatedResult) => void, -) => { - const pipe = argv.json ? process.stderr : process.stdout; - argv = argv || {}; - if (argv.version) { - pipe.write(`v${VERSION}\n`); - onComplete && onComplete(); - return; - } +const {run, runCLI} = require('./cli'); - readConfig(argv, root) - .then(({ - config, - hasDeprecationWarnings, - }: { - config: Config, - hasDeprecationWarnings: boolean, - }) => { - if (argv.debug) { - logDebugMessages(config, pipe); - } - - createDirectory(config.cacheDirectory); - const jestHasteMap = Runtime.createHasteMap(config, { - console: new Console(pipe, pipe), - maxWorkers: getMaxWorkers(argv), - resetCache: !config.cache, - watch: config.watch, - }); - - return jestHasteMap - .build() - .then( - hasteMap => createHasteContext(config, hasteMap), - error => { - throw error; - }, - ) - .then(hasteContext => { - if (argv.watch || argv.watchAll) { - return watch( - config, - pipe, - argv, - jestHasteMap, - hasteContext, - hasDeprecationWarnings, - ); - } else { - const startRun = () => { - preRunMessage.print(pipe); - const testWatcher = new TestWatcher({isWatchMode: false}); - return runJest( - hasteContext, - config, - argv, - pipe, - testWatcher, - startRun, - onComplete, - ); - }; - return startRun(); - } - }); - }) - .catch(error => { - clearLine(process.stderr); - clearLine(process.stdout); - console.error(chalk.red(error.stack)); - process.exit(1); - }); -}; +const VERSION = require('../package.json').version; module.exports = { SearchSource, diff --git a/packages/jest-cli/src/runJest.js b/packages/jest-cli/src/runJest.js index 61b7ceee4048..c7d44f61f003 100644 --- a/packages/jest-cli/src/runJest.js +++ b/packages/jest-cli/src/runJest.js @@ -17,6 +17,7 @@ const fs = require('graceful-fs'); const SearchSource = require('./SearchSource'); const TestRunner = require('./TestRunner'); +const TestSequencer = require('./TestSequencer'); const getTestPathPatternInfo = require('./lib/getTestPathPatternInfo'); const chalk = require('chalk'); @@ -41,7 +42,7 @@ const getTestSummary = (argv: Object, patternInfo: PatternInfo) => { chalk.dim('.'); }; -const runJest = ( +const runJest = async ( hasteContext: HasteContext, config: Config, argv: Object, @@ -51,84 +52,89 @@ const runJest = ( onComplete: (testResults: any) => void, ) => { const maxWorkers = getMaxWorkers(argv); - const localConsole = new Console(pipe, pipe); + const source = new SearchSource(hasteContext, config); let patternInfo = getTestPathPatternInfo(argv); - return Promise.resolve().then(() => { - const source = new SearchSource(hasteContext, config); - return source - .getTestPaths(patternInfo) - .then(data => { - if (!data.paths.length) { - 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.', - ); - } - } + 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( - source.getNoTestsFoundMessage(patternInfo, config, data), + '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.', ); } - return data; - }) - .then(data => { - if (data.paths.length === 1) { - if (config.silent !== true && config.verbose !== false) { - // $FlowFixMe - config = Object.assign({}, config, {verbose: true}); - } - } + } - return new TestRunner( - hasteContext, - config, - { - getTestSummary: () => getTestSummary(argv, patternInfo), - maxWorkers, - }, - startRun, - ).runTests(data.paths, testWatcher); - }) - .then(runResults => { - if (config.testResultsProcessor) { - /* $FlowFixMe */ - runResults = require(config.testResultsProcessor)(runResults); - } - if (argv.json) { - if (argv.outputFile) { - const outputFile = path.resolve(process.cwd(), argv.outputFile); - - fs.writeFileSync( - outputFile, - JSON.stringify(formatTestResults(runResults)), - ); - process.stdout.write( - `Test results written to: ` + - `${path.relative(process.cwd(), outputFile)}\n`, - ); - } else { - process.stdout.write(JSON.stringify(formatTestResults(runResults))); - } - } - return onComplete && onComplete(runResults); - }) - .catch(error => { - throw error; - }); - }); + localConsole.log( + source.getNoTestsFoundMessage(patternInfo, config, data), + ); + } + + 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 processResults = runResults => { + if (config.testResultsProcessor) { + /* $FlowFixMe */ + runResults = require(config.testResultsProcessor)(runResults); + } + if (argv.json) { + if (argv.outputFile) { + const outputFile = path.resolve(process.cwd(), argv.outputFile); + + fs.writeFileSync( + outputFile, + JSON.stringify(formatTestResults(runResults)), + ); + process.stdout.write( + `Test results written to: ` + + `${path.relative(process.cwd(), outputFile)}\n`, + ); + } else { + process.stdout.write(JSON.stringify(formatTestResults(runResults))); + } + } + 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); + return processResults(results); }; module.exports = runJest; diff --git a/types/TestRunner.js b/types/TestRunner.js new file mode 100644 index 000000000000..50a345dfed36 --- /dev/null +++ b/types/TestRunner.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014-present, 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. + * + * @flow + */ +'use strict'; + +import type {Config, Path} from './Config'; + +export type Test = { + config: Config, + path: Path, + duration?: number, +}; + +export type Tests = Array;