forked from jestjs/jest
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split TestRunner off of TestScheduler (jestjs#4233)
- Loading branch information
Showing
5 changed files
with
297 additions
and
216 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
], | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.