Skip to content

Commit

Permalink
Split TestRunner off of TestScheduler (jestjs#4233)
Browse files Browse the repository at this point in the history
  • Loading branch information
cpojer authored Aug 10, 2017
1 parent 28551b9 commit 4205c56
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 216 deletions.
98 changes: 98 additions & 0 deletions packages/jest-cli/src/__tests__/test_runner.test.js
Original file line number Diff line number Diff line change
@@ -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),
],
]);
});
});
85 changes: 0 additions & 85 deletions packages/jest-cli/src/__tests__/test_scheduler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand All @@ -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),
],
]);
});
});
});
162 changes: 162 additions & 0 deletions packages/jest-cli/src/test_runner.js
Original file line number Diff line number Diff line change
@@ -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<Test>,
watcher: TestWatcher,
onStart: OnTestStart,
onResult: OnTestSuccess,
onFailure: OnTestFailure,
options: TestRunnerOptions,
): Promise<void> {
return await (options.serial
? this._createInBandTestRun(tests, watcher, onStart, onResult, onFailure)
: this._createParallelTestRun(
tests,
watcher,
onStart,
onResult,
onFailure,
));
}

async _createInBandTestRun(
tests: Array<Test>,
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<Test>,
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;
Loading

0 comments on commit 4205c56

Please sign in to comment.