From bcdee2b4c87435b40c2cd60da6c069d49486ca42 Mon Sep 17 00:00:00 2001 From: cpojer Date: Wed, 9 Aug 2017 23:06:02 +0100 Subject: [PATCH] Split TestRunner off of TestScheduler --- .../src/__tests__/test_runner.test.js | 98 +++++++++++ .../src/__tests__/test_scheduler.test.js | 85 --------- packages/jest-cli/src/test_runner.js | 162 ++++++++++++++++++ packages/jest-cli/src/test_scheduler.js | 153 +++-------------- types/TestRunner.js | 15 +- 5 files changed, 297 insertions(+), 216 deletions(-) create mode 100644 packages/jest-cli/src/__tests__/test_runner.test.js create mode 100644 packages/jest-cli/src/test_runner.js diff --git a/packages/jest-cli/src/__tests__/test_runner.test.js b/packages/jest-cli/src/__tests__/test_runner.test.js new file mode 100644 index 000000000000..d1c92012d902 --- /dev/null +++ b/packages/jest-cli/src/__tests__/test_runner.test.js @@ -0,0 +1,98 @@ +/** + * 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. + * + * @emails oncall+jsinfra + */ + +'use strict'; + +const TestRunner = require('../test_runner'); +const TestWatcher = require('../test_watcher'); + +let workerFarmMock; + +jest.mock('worker-farm', () => { + const mock = jest.fn( + (options, worker) => + (workerFarmMock = jest.fn((data, callback) => + require(worker)(data, callback), + )), + ); + mock.end = jest.fn(); + return mock; +}); + +jest.mock('../test_worker', () => {}); + +test('injects the rawModuleMap into each worker in watch mode', () => { + const globalConfig = {maxWorkers: 2, watch: true}; + const config = {rootDir: '/path/'}; + const rawModuleMap = jest.fn(); + const context = { + config, + moduleMap: {getRawModuleMap: () => rawModuleMap}, + }; + return new TestRunner(globalConfig) + .runTests( + [{context, path: './file.test.js'}, {context, path: './file2.test.js'}], + new TestWatcher({isWatchMode: globalConfig.watch}), + () => {}, + () => {}, + () => {}, + {serial: false}, + ) + .then(() => { + expect(workerFarmMock.mock.calls).toEqual([ + [ + {config, globalConfig, path: './file.test.js', rawModuleMap}, + expect.any(Function), + ], + [ + {config, globalConfig, path: './file2.test.js', rawModuleMap}, + expect.any(Function), + ], + ]); + }); +}); + +test('does not inject the rawModuleMap in serial mode', () => { + const globalConfig = {maxWorkers: 1, watch: false}; + const config = {rootDir: '/path/'}; + const context = {config}; + + return new TestRunner(globalConfig) + .runTests( + [{context, path: './file.test.js'}, {context, path: './file2.test.js'}], + new TestWatcher({isWatchMode: globalConfig.watch}), + () => {}, + () => {}, + () => {}, + {serial: false}, + ) + .then(() => { + expect(workerFarmMock.mock.calls).toEqual([ + [ + { + config, + globalConfig, + path: './file.test.js', + rawModuleMap: null, + }, + expect.any(Function), + ], + [ + { + config, + globalConfig, + path: './file2.test.js', + rawModuleMap: null, + }, + expect.any(Function), + ], + ]); + }); +}); diff --git a/packages/jest-cli/src/__tests__/test_scheduler.test.js b/packages/jest-cli/src/__tests__/test_scheduler.test.js index 72b60be7cee8..2fea8f75f83b 100644 --- a/packages/jest-cli/src/__tests__/test_scheduler.test.js +++ b/packages/jest-cli/src/__tests__/test_scheduler.test.js @@ -11,23 +11,8 @@ 'use strict'; const TestScheduler = require('../test_scheduler'); -const TestWatcher = require('../test_watcher'); const SummaryReporter = require('../reporters/summary_reporter'); -let workerFarmMock; - -jest.mock('worker-farm', () => { - const mock = jest.fn( - (options, worker) => - (workerFarmMock = jest.fn((data, callback) => - require(worker)(data, callback), - )), - ); - mock.end = jest.fn(); - return mock; -}); - -jest.mock('../test_worker', () => {}); jest.mock('../reporters/default_reporter'); test('.addReporter() .removeReporter()', () => { @@ -38,73 +23,3 @@ test('.addReporter() .removeReporter()', () => { scheduler.removeReporter(SummaryReporter); expect(scheduler._dispatcher._reporters).not.toContain(reporter); }); - -describe('_createInBandTestRun()', () => { - test('injects the rawModuleMap to each the worker in watch mode', () => { - const globalConfig = {maxWorkers: 2, watch: true}; - const config = {rootDir: '/path/'}; - const rawModuleMap = jest.fn(); - const context = { - config, - moduleMap: {getRawModuleMap: () => rawModuleMap}, - }; - const scheduler = new TestScheduler(globalConfig, {}); - - return scheduler - ._createParallelTestRun( - [{context, path: './file.test.js'}, {context, path: './file2.test.js'}], - new TestWatcher({isWatchMode: globalConfig.watch}), - () => {}, - () => {}, - ) - .then(() => { - expect(workerFarmMock.mock.calls).toEqual([ - [ - {config, globalConfig, path: './file.test.js', rawModuleMap}, - expect.any(Function), - ], - [ - {config, globalConfig, path: './file2.test.js', rawModuleMap}, - expect.any(Function), - ], - ]); - }); - }); - - test('does not inject the rawModuleMap in non watch mode', () => { - const globalConfig = {maxWorkers: 1, watch: false}; - const config = {rootDir: '/path/'}; - const context = {config}; - const scheduler = new TestScheduler(globalConfig, {}); - - return scheduler - ._createParallelTestRun( - [{context, path: './file.test.js'}, {context, path: './file2.test.js'}], - new TestWatcher({isWatchMode: globalConfig.watch}), - () => {}, - () => {}, - ) - .then(() => { - expect(workerFarmMock.mock.calls).toEqual([ - [ - { - config, - globalConfig, - path: './file.test.js', - rawModuleMap: null, - }, - expect.any(Function), - ], - [ - { - config, - globalConfig, - path: './file2.test.js', - rawModuleMap: null, - }, - expect.any(Function), - ], - ]); - }); - }); -}); diff --git a/packages/jest-cli/src/test_runner.js b/packages/jest-cli/src/test_runner.js new file mode 100644 index 000000000000..3b4e8dcf5b9c --- /dev/null +++ b/packages/jest-cli/src/test_runner.js @@ -0,0 +1,162 @@ +/** + * 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 + */ + +import type {GlobalConfig} from 'types/Config'; +import type TestWatcher from './test_watcher'; +import type { + OnTestFailure, + OnTestStart, + OnTestSuccess, + Test, + TestRunnerOptions, +} from 'types/TestRunner'; + +import pify from 'pify'; +import runTest from './run_test'; +import throat from 'throat'; +import workerFarm from 'worker-farm'; + +const TEST_WORKER_PATH = require.resolve('./test_worker'); + +class TestRunner { + _globalConfig: GlobalConfig; + + constructor(globalConfig: GlobalConfig) { + this._globalConfig = globalConfig; + } + + async runTests( + tests: Array, + watcher: TestWatcher, + onStart: OnTestStart, + onResult: OnTestSuccess, + onFailure: OnTestFailure, + options: TestRunnerOptions, + ): Promise { + return await (options.serial + ? this._createInBandTestRun(tests, watcher, onStart, onResult, onFailure) + : this._createParallelTestRun( + tests, + watcher, + onStart, + onResult, + onFailure, + )); + } + + async _createInBandTestRun( + tests: Array, + watcher: TestWatcher, + onStart: OnTestStart, + onResult: OnTestSuccess, + onFailure: OnTestFailure, + ) { + const mutex = throat(1); + return tests.reduce( + (promise, test) => + mutex(() => + promise + .then(async () => { + if (watcher.isInterrupted()) { + throw new CancelRun(); + } + + await onStart(test); + return runTest( + test.path, + this._globalConfig, + test.context.config, + test.context.resolver, + ); + }) + .then(result => onResult(test, result)) + .catch(err => onFailure(test, err)), + ), + Promise.resolve(), + ); + } + + async _createParallelTestRun( + tests: Array, + watcher: TestWatcher, + onStart: OnTestStart, + onResult: OnTestSuccess, + onFailure: OnTestFailure, + ) { + const farm = workerFarm( + { + autoStart: true, + maxConcurrentCallsPerWorker: 1, + maxConcurrentWorkers: this._globalConfig.maxWorkers, + maxRetries: 2, // Allow for a couple of transient errors. + }, + TEST_WORKER_PATH, + ); + const mutex = throat(this._globalConfig.maxWorkers); + const worker = pify(farm); + + // Send test suites to workers continuously instead of all at once to track + // the start time of individual tests. + const runTestInWorker = test => + mutex(async () => { + if (watcher.isInterrupted()) { + return Promise.reject(); + } + await onStart(test); + return worker({ + config: test.context.config, + globalConfig: this._globalConfig, + path: test.path, + rawModuleMap: watcher.isWatchMode() + ? test.context.moduleMap.getRawModuleMap() + : null, + }); + }); + + const onError = async (err, test) => { + await onFailure(test, err); + if (err.type === 'ProcessTerminatedError') { + console.error( + 'A worker process has quit unexpectedly! ' + + 'Most likely this is an initialization error.', + ); + process.exit(1); + } + }; + + const onInterrupt = new Promise((_, reject) => { + watcher.on('change', state => { + if (state.interrupted) { + reject(new CancelRun()); + } + }); + }); + + const runAllTests = Promise.all( + 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); + } +} + +class CancelRun extends Error { + constructor(message: ?string) { + super(message); + this.name = 'CancelRun'; + } +} + +module.exports = TestRunner; diff --git a/packages/jest-cli/src/test_scheduler.js b/packages/jest-cli/src/test_scheduler.js index 0c448ef127e3..6d3a154e211a 100644 --- a/packages/jest-cli/src/test_scheduler.js +++ b/packages/jest-cli/src/test_scheduler.js @@ -8,11 +8,7 @@ * @flow */ -import type { - AggregatedResult, - SerializableError as TestError, - TestResult, -} from 'types/TestResult'; +import type {AggregatedResult, TestResult} from 'types/TestResult'; import type {GlobalConfig, ReporterConfig} from 'types/Config'; import type {Context} from 'types/Context'; import type {Reporter, Test} from 'types/TestRunner'; @@ -23,45 +19,32 @@ import { buildFailureTestResult, makeEmptyAggregatedTestResult, } from './test_result_helpers'; -import snapshot from 'jest-snapshot'; -import pify from 'pify'; -import throat from 'throat'; -import workerFarm from 'worker-farm'; +import CoverageReporter from './reporters/coverage_reporter'; import DefaultReporter from './reporters/default_reporter'; import NotifyReporter from './reporters/notify_reporter'; +import ReporterDispatcher from './reporter_dispatcher'; +import snapshot from 'jest-snapshot'; import SummaryReporter from './reporters/summary_reporter'; -import VerboseReporter from './reporters/verbose_reporter'; -import runTest from './run_test'; +import TestRunner from './test_runner'; import TestWatcher from './test_watcher'; -import CoverageReporter from './reporters/coverage_reporter'; -import ReporterDispatcher from './reporter_dispatcher'; +import VerboseReporter from './reporters/verbose_reporter'; const SLOW_TEST_TIME = 3000; -class CancelRun extends Error { - constructor(message: ?string) { - super(message); - this.name = 'CancelRun'; - } -} - export type TestSchedulerOptions = {| startRun: (globalConfig: GlobalConfig) => *, |}; -type OnTestFailure = (test: Test, err: TestError) => Promise<*>; -type OnTestSuccess = (test: Test, result: TestResult) => Promise<*>; - -const TEST_WORKER_PATH = require.resolve('./test_worker'); - class TestScheduler { + _dispatcher: ReporterDispatcher; _globalConfig: GlobalConfig; _options: TestSchedulerOptions; - _dispatcher: ReporterDispatcher; + _testRunner: TestRunner; constructor(globalConfig: GlobalConfig, options: TestSchedulerOptions) { - this._globalConfig = globalConfig; this._dispatcher = new ReporterDispatcher(); + this._globalConfig = globalConfig; + this._testRunner = new TestRunner(globalConfig); this._options = options; this._setupReporters(); } @@ -75,6 +58,7 @@ class TestScheduler { } async runTests(tests: Array, watcher: TestWatcher) { + const onStart = this._dispatcher.onTestStart.bind(this._dispatcher); const timings = []; const contexts = new Set(); tests.forEach(test => { @@ -116,7 +100,7 @@ class TestScheduler { return this._bailIfNeeded(contexts, aggregatedResults, watcher); }; - const onFailure = async (test: Test, error: TestError) => { + const onFailure = async (test, error) => { if (watcher.isInterrupted()) { return; } @@ -155,9 +139,16 @@ class TestScheduler { }); try { - await (runInBand - ? this._createInBandTestRun(tests, watcher, onResult, onFailure) - : this._createParallelTestRun(tests, watcher, onResult, onFailure)); + await this._testRunner.runTests( + tests, + watcher, + onStart, + onResult, + onFailure, + { + serial: runInBand, + }, + ); } catch (error) { if (!watcher.isInterrupted()) { throw error; @@ -183,104 +174,6 @@ class TestScheduler { return aggregatedResults; } - _createInBandTestRun( - tests: Array, - watcher: TestWatcher, - onResult: OnTestSuccess, - onFailure: OnTestFailure, - ) { - const mutex = throat(1); - return tests.reduce( - (promise, test) => - mutex(() => - promise - .then(async () => { - if (watcher.isInterrupted()) { - throw new CancelRun(); - } - - await this._dispatcher.onTestStart(test); - return runTest( - test.path, - this._globalConfig, - test.context.config, - test.context.resolver, - ); - }) - .then(result => onResult(test, result)) - .catch(err => onFailure(test, err)), - ), - Promise.resolve(), - ); - } - - _createParallelTestRun( - tests: Array, - watcher: TestWatcher, - onResult: OnTestSuccess, - onFailure: OnTestFailure, - ) { - const farm = workerFarm( - { - autoStart: true, - maxConcurrentCallsPerWorker: 1, - maxConcurrentWorkers: this._globalConfig.maxWorkers, - maxRetries: 2, // Allow for a couple of transient errors. - }, - TEST_WORKER_PATH, - ); - const mutex = throat(this._globalConfig.maxWorkers); - const worker = pify(farm); - - // Send test suites to workers continuously instead of all at once to track - // the start time of individual tests. - const runTestInWorker = test => - mutex(async () => { - if (watcher.isInterrupted()) { - return Promise.reject(); - } - await this._dispatcher.onTestStart(test); - return worker({ - config: test.context.config, - globalConfig: this._globalConfig, - path: test.path, - rawModuleMap: watcher.isWatchMode() - ? test.context.moduleMap.getRawModuleMap() - : null, - }); - }); - - const onError = async (err, test) => { - await onFailure(test, err); - if (err.type === 'ProcessTerminatedError') { - console.error( - 'A worker process has quit unexpectedly! ' + - 'Most likely this is an initialization error.', - ); - process.exit(1); - } - }; - - const onInterrupt = new Promise((_, reject) => { - watcher.on('change', state => { - if (state.interrupted) { - reject(new CancelRun()); - } - }); - }); - - const runAllTests = Promise.all( - 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); - } - _shouldAddDefaultReporters(reporters?: Array): boolean { return ( !reporters || @@ -290,7 +183,6 @@ class TestScheduler { _setupReporters() { const {collectCoverage, notify, reporters} = this._globalConfig; - const isDefault = this._shouldAddDefaultReporters(reporters); if (isDefault) { @@ -331,6 +223,7 @@ class TestScheduler { const {options, path} = this._getReporterProps(reporter); try { + // $FlowFixMe const Reporter = require(path); this.addReporter(new Reporter(this._globalConfig, options)); } catch (error) { diff --git a/types/TestRunner.js b/types/TestRunner.js index 55615d79a661..2a0aed75ea5c 100644 --- a/types/TestRunner.js +++ b/types/TestRunner.js @@ -12,8 +12,13 @@ import type {Context} from './Context'; import type {Environment} from 'types/Environment'; import type {GlobalConfig, Path, ProjectConfig} from './Config'; import type {ReporterOnStartOptions} from 'types/Reporters'; -import type {TestResult, AggregatedResult} from 'types/TestResult'; +import type { + AggregatedResult, + SerializableError, + TestResult, +} from 'types/TestResult'; import type Runtime from 'jest-runtime'; +import type TestWatcher from 'jest-cli/src/test_watcher'; export type Test = {| context: Context, @@ -21,6 +26,10 @@ export type Test = {| path: Path, |}; +export type OnTestStart = Test => Promise; +export type OnTestFailure = (Test, SerializableError) => Promise<*>; +export type OnTestSuccess = (Test, TestResult) => Promise<*>; + export type Reporter = { +onTestResult: ( test: Test, @@ -46,3 +55,7 @@ export type TestFramework = ( runtime: Runtime, testPath: string, ) => Promise; + +export type TestRunnerOptions = { + serial: boolean, +};