Skip to content

Commit e8e609e

Browse files
committed
Use one TestSequencer instance for all contexts.
1 parent ac61b80 commit e8e609e

File tree

3 files changed

+118
-67
lines changed

3 files changed

+118
-67
lines changed

packages/jest-cli/src/TestSequencer.js

+44-46
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import type {AggregatedResult} from 'types/TestResult';
1313
import type {Context} from 'types/Context';
14-
import type {Tests} from 'types/TestRunner';
14+
import type {Test, Tests} from 'types/TestRunner';
1515

1616
const fs = require('fs');
1717
const getCacheFilePath = require('jest-haste-map').getCacheFilePath;
@@ -24,19 +24,31 @@ type Cache = {
2424
};
2525

2626
class TestSequencer {
27-
_context: Context;
28-
_cache: Cache;
27+
_cache: Map<Context, Cache>;
2928

30-
constructor(context: Context) {
31-
this._context = context;
32-
this._cache = {};
29+
constructor() {
30+
this._cache = new Map();
3331
}
3432

35-
_getTestPerformanceCachePath() {
36-
const {config} = this._context;
33+
_getCachePath(context: Context) {
34+
const {config} = context;
3735
return getCacheFilePath(config.cacheDirectory, 'perf-cache-' + config.name);
3836
}
3937

38+
_getCache(test: Test) {
39+
const {context} = test;
40+
if (!this._cache.has(context) && context.config.cache) {
41+
try {
42+
this._cache.set(
43+
context,
44+
JSON.parse(fs.readFileSync(this._getCachePath(context), 'utf8')),
45+
);
46+
} catch (e) {}
47+
}
48+
49+
return this._cache.get(context) || {};
50+
}
51+
4052
// When running more tests than we have workers available, sort the tests
4153
// by size - big test files usually take longer to complete, so we run
4254
// them first in an effort to minimize worker idle time at the end of a
@@ -47,62 +59,48 @@ class TestSequencer {
4759
// fastest results.
4860
sort(tests: Tests): Tests {
4961
const stats = {};
50-
const fileSize = filePath =>
51-
stats[filePath] || (stats[filePath] = fs.statSync(filePath).size);
52-
const failed = filePath =>
53-
this._cache[filePath] && this._cache[filePath][0] === FAIL;
54-
const time = filePath => this._cache[filePath] && this._cache[filePath][1];
62+
const fileSize = test =>
63+
stats[test.path] || (stats[test.path] = fs.statSync(test.path).size);
64+
const hasFailed = (cache, test) =>
65+
cache[test.path] && cache[test.path][0] === FAIL;
66+
const time = (cache, test) => cache[test.path] && cache[test.path][1];
5567

56-
this._cache = {};
57-
try {
58-
if (this._context.config.cache) {
59-
this._cache = JSON.parse(
60-
fs.readFileSync(this._getTestPerformanceCachePath(), 'utf8'),
61-
);
62-
}
63-
} catch (e) {}
64-
65-
tests = tests.sort(({path: pathA}, {path: pathB}) => {
66-
const failedA = failed(pathA);
67-
const failedB = failed(pathB);
68+
tests.forEach(test => test.duration = time(this._getCache(test), test));
69+
return tests.sort((testA, testB) => {
70+
const cacheA = this._getCache(testA);
71+
const cacheB = this._getCache(testB);
72+
const failedA = hasFailed(cacheA, testA);
73+
const failedB = hasFailed(cacheB, testB);
74+
const hasTimeA = testA.duration != null;
6875
if (failedA !== failedB) {
6976
return failedA ? -1 : 1;
77+
} else if (hasTimeA != (testB.duration != null)) {
78+
// Check if only one of two tests has timing information
79+
return hasTimeA != null ? 1 : -1;
80+
} else if (testA.duration != null && testB.duration != null) {
81+
return testA.duration < testB.duration ? 1 : -1;
82+
} else {
83+
return fileSize(testA) < fileSize(testB) ? 1 : -1;
7084
}
71-
const timeA = time(pathA);
72-
const timeB = time(pathB);
73-
const hasTimeA = timeA != null;
74-
const hasTimeB = timeB != null;
75-
// Check if only one of two tests has timing information
76-
if (hasTimeA != hasTimeB) {
77-
return hasTimeA ? 1 : -1;
78-
}
79-
if (timeA != null && !timeB != null) {
80-
return timeA < timeB ? 1 : -1;
81-
}
82-
return fileSize(pathA) < fileSize(pathB) ? 1 : -1;
8385
});
84-
85-
tests.forEach(test => test.duration = time(test.path));
86-
return tests;
8786
}
8887

8988
cacheResults(tests: Tests, results: AggregatedResult) {
90-
const cache = this._cache;
9189
const map = Object.create(null);
92-
tests.forEach(({path}) => map[path] = true);
90+
tests.forEach(test => map[test.path] = test);
9391
results.testResults.forEach(testResult => {
9492
if (testResult && map[testResult.testFilePath] && !testResult.skipped) {
93+
const cache = this._getCache(map[testResult.testFilePath]);
9594
const perf = testResult.perfStats;
9695
cache[testResult.testFilePath] = [
9796
testResult.numFailingTests ? FAIL : SUCCESS,
9897
perf.end - perf.start || 0,
9998
];
10099
}
101100
});
102-
fs.writeFileSync(
103-
this._getTestPerformanceCachePath(),
104-
JSON.stringify(cache),
105-
);
101+
102+
this._cache.forEach((cache, context) =>
103+
fs.writeFileSync(this._getCachePath(context), JSON.stringify(cache)));
106104
}
107105
}
108106

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

+65-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ const context = {
2626
},
2727
};
2828

29+
const secondContext = {
30+
config: {
31+
cache: true,
32+
cacheDirectory: '/cache2',
33+
name: 'test2',
34+
},
35+
};
36+
2937
const toTests = paths =>
3038
paths.map(path => ({
3139
context,
@@ -34,10 +42,11 @@ const toTests = paths =>
3442
}));
3543

3644
beforeEach(() => {
37-
sequencer = new TestSequencer(context);
45+
sequencer = new TestSequencer();
3846

3947
fs.readFileSync = jest.fn(() => '{}');
4048
fs.statSync = jest.fn(filePath => ({size: filePath.length}));
49+
fs.writeFileSync = jest.fn();
4150
});
4251

4352
test('sorts by file size if there is no timing information', () => {
@@ -149,3 +158,58 @@ test('writes the cache based on the results', () => {
149158
'/test-c.js': [FAIL, 3],
150159
});
151160
});
161+
162+
test('works with multiple contexts', () => {
163+
fs.readFileSync = jest.fn(
164+
cacheName =>
165+
cacheName.startsWith('/cache/')
166+
? JSON.stringify({
167+
'/test-a.js': [SUCCESS, 5],
168+
'/test-b.js': [FAIL, 1],
169+
})
170+
: JSON.stringify({
171+
'/test-c.js': [FAIL],
172+
}),
173+
);
174+
175+
const testPaths = [
176+
{context, duration: null, path: '/test-a.js'},
177+
{context, duration: null, path: '/test-b.js'},
178+
{context: secondContext, duration: null, path: '/test-c.js'},
179+
];
180+
const tests = sequencer.sort(testPaths);
181+
sequencer.cacheResults(tests, {
182+
testResults: [
183+
{
184+
numFailingTests: 0,
185+
perfStats: {end: 2, start: 1},
186+
testFilePath: '/test-a.js',
187+
},
188+
{
189+
numFailingTests: 0,
190+
perfStats: {end: 0, start: 0},
191+
skipped: true,
192+
testFilePath: '/test-b.js',
193+
},
194+
{
195+
numFailingTests: 0,
196+
perfStats: {end: 4, start: 1},
197+
testFilePath: '/test-c.js',
198+
},
199+
{
200+
numFailingTests: 1,
201+
perfStats: {end: 2, start: 1},
202+
testFilePath: '/test-x.js',
203+
},
204+
],
205+
});
206+
const fileDataA = JSON.parse(fs.writeFileSync.mock.calls[0][1]);
207+
expect(fileDataA).toEqual({
208+
'/test-a.js': [SUCCESS, 1],
209+
'/test-b.js': [FAIL, 1],
210+
});
211+
const fileDataB = JSON.parse(fs.writeFileSync.mock.calls[1][1]);
212+
expect(fileDataB).toEqual({
213+
'/test-c.js': [SUCCESS, 3],
214+
});
215+
});

packages/jest-cli/src/runJest.js

+9-20
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
'use strict';
1111

1212
import type {Context} from 'types/Context';
13-
import type {Test} from 'types/TestRunner';
1413
import type TestWatcher from './TestWatcher';
1514

1615
const fs = require('graceful-fs');
@@ -114,7 +113,6 @@ const getTestPaths = async (context, pattern, argv, pipe) => {
114113
}
115114
}
116115
}
117-
118116
return data;
119117
};
120118

@@ -150,31 +148,23 @@ const runJest = async (
150148
startRun: () => *,
151149
onComplete: (testResults: any) => void,
152150
) => {
153-
const maxWorkers = getMaxWorkers(argv);
154151
const context = contexts[0];
152+
const maxWorkers = getMaxWorkers(argv);
153+
const pattern = getTestPathPattern(argv);
154+
const sequencer = new TestSequencer();
155+
let allTests = [];
155156
const testRunData = await Promise.all(
156157
contexts.map(async context => {
157158
const matches = await getTestPaths(context, pattern, argv, pipe);
158-
const sequencer = new TestSequencer(context);
159-
const tests = sequencer.sort(matches.tests);
160-
return {context, matches, sequencer, tests};
159+
allTests = allTests.concat(matches.tests);
160+
return {context, matches};
161161
}),
162162
);
163163

164-
const allTests = testRunData
165-
.reduce((tests, testRun) => tests.concat(testRun.tests), [])
166-
.sort((a: Test, b: Test) => {
167-
if (a.duration != null && b.duration != null) {
168-
return a.duration < b.duration ? 1 : -1;
169-
}
170-
return a.duration == null ? 1 : 0;
171-
});
172-
164+
allTests = sequencer.sort(allTests);
173165
if (!allTests.length) {
174166
new Console(pipe, pipe).log(getNoTestsFoundMessage(testRunData, pattern));
175-
}
176-
177-
if (
167+
} else if (
178168
allTests.length === 1 &&
179169
context.config.silent !== true &&
180170
context.config.verbose !== false
@@ -200,8 +190,7 @@ const runJest = async (
200190
testPathPattern: formatTestPathPattern(pattern),
201191
}).runTests(allTests, testWatcher);
202192

203-
testRunData.forEach(({sequencer, tests}) =>
204-
sequencer.cacheResults(tests, results));
193+
sequencer.cacheResults(allTests, results);
205194

206195
return processResults(results, {
207196
isJSON: argv.json,

0 commit comments

Comments
 (0)