Skip to content

Commit 4205c56

Browse files
authored
Split TestRunner off of TestScheduler (jestjs#4233)
1 parent 28551b9 commit 4205c56

File tree

5 files changed

+297
-216
lines changed

5 files changed

+297
-216
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
3+
*
4+
* This source code is licensed under the BSD-style license found in the
5+
* LICENSE file in the root directory of this source tree. An additional grant
6+
* of patent rights can be found in the PATENTS file in the same directory.
7+
*
8+
* @emails oncall+jsinfra
9+
*/
10+
11+
'use strict';
12+
13+
const TestRunner = require('../test_runner');
14+
const TestWatcher = require('../test_watcher');
15+
16+
let workerFarmMock;
17+
18+
jest.mock('worker-farm', () => {
19+
const mock = jest.fn(
20+
(options, worker) =>
21+
(workerFarmMock = jest.fn((data, callback) =>
22+
require(worker)(data, callback),
23+
)),
24+
);
25+
mock.end = jest.fn();
26+
return mock;
27+
});
28+
29+
jest.mock('../test_worker', () => {});
30+
31+
test('injects the rawModuleMap into each worker in watch mode', () => {
32+
const globalConfig = {maxWorkers: 2, watch: true};
33+
const config = {rootDir: '/path/'};
34+
const rawModuleMap = jest.fn();
35+
const context = {
36+
config,
37+
moduleMap: {getRawModuleMap: () => rawModuleMap},
38+
};
39+
return new TestRunner(globalConfig)
40+
.runTests(
41+
[{context, path: './file.test.js'}, {context, path: './file2.test.js'}],
42+
new TestWatcher({isWatchMode: globalConfig.watch}),
43+
() => {},
44+
() => {},
45+
() => {},
46+
{serial: false},
47+
)
48+
.then(() => {
49+
expect(workerFarmMock.mock.calls).toEqual([
50+
[
51+
{config, globalConfig, path: './file.test.js', rawModuleMap},
52+
expect.any(Function),
53+
],
54+
[
55+
{config, globalConfig, path: './file2.test.js', rawModuleMap},
56+
expect.any(Function),
57+
],
58+
]);
59+
});
60+
});
61+
62+
test('does not inject the rawModuleMap in serial mode', () => {
63+
const globalConfig = {maxWorkers: 1, watch: false};
64+
const config = {rootDir: '/path/'};
65+
const context = {config};
66+
67+
return new TestRunner(globalConfig)
68+
.runTests(
69+
[{context, path: './file.test.js'}, {context, path: './file2.test.js'}],
70+
new TestWatcher({isWatchMode: globalConfig.watch}),
71+
() => {},
72+
() => {},
73+
() => {},
74+
{serial: false},
75+
)
76+
.then(() => {
77+
expect(workerFarmMock.mock.calls).toEqual([
78+
[
79+
{
80+
config,
81+
globalConfig,
82+
path: './file.test.js',
83+
rawModuleMap: null,
84+
},
85+
expect.any(Function),
86+
],
87+
[
88+
{
89+
config,
90+
globalConfig,
91+
path: './file2.test.js',
92+
rawModuleMap: null,
93+
},
94+
expect.any(Function),
95+
],
96+
]);
97+
});
98+
});

packages/jest-cli/src/__tests__/test_scheduler.test.js

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,8 @@
1111
'use strict';
1212

1313
const TestScheduler = require('../test_scheduler');
14-
const TestWatcher = require('../test_watcher');
1514
const SummaryReporter = require('../reporters/summary_reporter');
1615

17-
let workerFarmMock;
18-
19-
jest.mock('worker-farm', () => {
20-
const mock = jest.fn(
21-
(options, worker) =>
22-
(workerFarmMock = jest.fn((data, callback) =>
23-
require(worker)(data, callback),
24-
)),
25-
);
26-
mock.end = jest.fn();
27-
return mock;
28-
});
29-
30-
jest.mock('../test_worker', () => {});
3116
jest.mock('../reporters/default_reporter');
3217

3318
test('.addReporter() .removeReporter()', () => {
@@ -38,73 +23,3 @@ test('.addReporter() .removeReporter()', () => {
3823
scheduler.removeReporter(SummaryReporter);
3924
expect(scheduler._dispatcher._reporters).not.toContain(reporter);
4025
});
41-
42-
describe('_createInBandTestRun()', () => {
43-
test('injects the rawModuleMap to each the worker in watch mode', () => {
44-
const globalConfig = {maxWorkers: 2, watch: true};
45-
const config = {rootDir: '/path/'};
46-
const rawModuleMap = jest.fn();
47-
const context = {
48-
config,
49-
moduleMap: {getRawModuleMap: () => rawModuleMap},
50-
};
51-
const scheduler = new TestScheduler(globalConfig, {});
52-
53-
return scheduler
54-
._createParallelTestRun(
55-
[{context, path: './file.test.js'}, {context, path: './file2.test.js'}],
56-
new TestWatcher({isWatchMode: globalConfig.watch}),
57-
() => {},
58-
() => {},
59-
)
60-
.then(() => {
61-
expect(workerFarmMock.mock.calls).toEqual([
62-
[
63-
{config, globalConfig, path: './file.test.js', rawModuleMap},
64-
expect.any(Function),
65-
],
66-
[
67-
{config, globalConfig, path: './file2.test.js', rawModuleMap},
68-
expect.any(Function),
69-
],
70-
]);
71-
});
72-
});
73-
74-
test('does not inject the rawModuleMap in non watch mode', () => {
75-
const globalConfig = {maxWorkers: 1, watch: false};
76-
const config = {rootDir: '/path/'};
77-
const context = {config};
78-
const scheduler = new TestScheduler(globalConfig, {});
79-
80-
return scheduler
81-
._createParallelTestRun(
82-
[{context, path: './file.test.js'}, {context, path: './file2.test.js'}],
83-
new TestWatcher({isWatchMode: globalConfig.watch}),
84-
() => {},
85-
() => {},
86-
)
87-
.then(() => {
88-
expect(workerFarmMock.mock.calls).toEqual([
89-
[
90-
{
91-
config,
92-
globalConfig,
93-
path: './file.test.js',
94-
rawModuleMap: null,
95-
},
96-
expect.any(Function),
97-
],
98-
[
99-
{
100-
config,
101-
globalConfig,
102-
path: './file2.test.js',
103-
rawModuleMap: null,
104-
},
105-
expect.any(Function),
106-
],
107-
]);
108-
});
109-
});
110-
});
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
3+
*
4+
* This source code is licensed under the BSD-style license found in the
5+
* LICENSE file in the root directory of this source tree. An additional grant
6+
* of patent rights can be found in the PATENTS file in the same directory.
7+
*
8+
* @flow
9+
*/
10+
11+
import type {GlobalConfig} from 'types/Config';
12+
import type TestWatcher from './test_watcher';
13+
import type {
14+
OnTestFailure,
15+
OnTestStart,
16+
OnTestSuccess,
17+
Test,
18+
TestRunnerOptions,
19+
} from 'types/TestRunner';
20+
21+
import pify from 'pify';
22+
import runTest from './run_test';
23+
import throat from 'throat';
24+
import workerFarm from 'worker-farm';
25+
26+
const TEST_WORKER_PATH = require.resolve('./test_worker');
27+
28+
class TestRunner {
29+
_globalConfig: GlobalConfig;
30+
31+
constructor(globalConfig: GlobalConfig) {
32+
this._globalConfig = globalConfig;
33+
}
34+
35+
async runTests(
36+
tests: Array<Test>,
37+
watcher: TestWatcher,
38+
onStart: OnTestStart,
39+
onResult: OnTestSuccess,
40+
onFailure: OnTestFailure,
41+
options: TestRunnerOptions,
42+
): Promise<void> {
43+
return await (options.serial
44+
? this._createInBandTestRun(tests, watcher, onStart, onResult, onFailure)
45+
: this._createParallelTestRun(
46+
tests,
47+
watcher,
48+
onStart,
49+
onResult,
50+
onFailure,
51+
));
52+
}
53+
54+
async _createInBandTestRun(
55+
tests: Array<Test>,
56+
watcher: TestWatcher,
57+
onStart: OnTestStart,
58+
onResult: OnTestSuccess,
59+
onFailure: OnTestFailure,
60+
) {
61+
const mutex = throat(1);
62+
return tests.reduce(
63+
(promise, test) =>
64+
mutex(() =>
65+
promise
66+
.then(async () => {
67+
if (watcher.isInterrupted()) {
68+
throw new CancelRun();
69+
}
70+
71+
await onStart(test);
72+
return runTest(
73+
test.path,
74+
this._globalConfig,
75+
test.context.config,
76+
test.context.resolver,
77+
);
78+
})
79+
.then(result => onResult(test, result))
80+
.catch(err => onFailure(test, err)),
81+
),
82+
Promise.resolve(),
83+
);
84+
}
85+
86+
async _createParallelTestRun(
87+
tests: Array<Test>,
88+
watcher: TestWatcher,
89+
onStart: OnTestStart,
90+
onResult: OnTestSuccess,
91+
onFailure: OnTestFailure,
92+
) {
93+
const farm = workerFarm(
94+
{
95+
autoStart: true,
96+
maxConcurrentCallsPerWorker: 1,
97+
maxConcurrentWorkers: this._globalConfig.maxWorkers,
98+
maxRetries: 2, // Allow for a couple of transient errors.
99+
},
100+
TEST_WORKER_PATH,
101+
);
102+
const mutex = throat(this._globalConfig.maxWorkers);
103+
const worker = pify(farm);
104+
105+
// Send test suites to workers continuously instead of all at once to track
106+
// the start time of individual tests.
107+
const runTestInWorker = test =>
108+
mutex(async () => {
109+
if (watcher.isInterrupted()) {
110+
return Promise.reject();
111+
}
112+
await onStart(test);
113+
return worker({
114+
config: test.context.config,
115+
globalConfig: this._globalConfig,
116+
path: test.path,
117+
rawModuleMap: watcher.isWatchMode()
118+
? test.context.moduleMap.getRawModuleMap()
119+
: null,
120+
});
121+
});
122+
123+
const onError = async (err, test) => {
124+
await onFailure(test, err);
125+
if (err.type === 'ProcessTerminatedError') {
126+
console.error(
127+
'A worker process has quit unexpectedly! ' +
128+
'Most likely this is an initialization error.',
129+
);
130+
process.exit(1);
131+
}
132+
};
133+
134+
const onInterrupt = new Promise((_, reject) => {
135+
watcher.on('change', state => {
136+
if (state.interrupted) {
137+
reject(new CancelRun());
138+
}
139+
});
140+
});
141+
142+
const runAllTests = Promise.all(
143+
tests.map(test =>
144+
runTestInWorker(test)
145+
.then(testResult => onResult(test, testResult))
146+
.catch(error => onError(error, test)),
147+
),
148+
);
149+
150+
const cleanup = () => workerFarm.end(farm);
151+
return Promise.race([runAllTests, onInterrupt]).then(cleanup, cleanup);
152+
}
153+
}
154+
155+
class CancelRun extends Error {
156+
constructor(message: ?string) {
157+
super(message);
158+
this.name = 'CancelRun';
159+
}
160+
}
161+
162+
module.exports = TestRunner;

0 commit comments

Comments
 (0)