Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split TestRunner off of TestScheduler #4233

Merged
merged 1 commit into from
Aug 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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