Skip to content

Commit 7703e22

Browse files
cpojerskovhus
authored andcommitted
[RFC] Multi project runner (jestjs#3156)
* Add `—experimentalProjects` to run multiple projects within the same jest-cli test run. * Improve the “no tests found” message for multiple projects. * Do not pass a single context to TestRunner and remove RunnerContext from reporters. * Rename `patternInfo` to `PathPattern` * Remove `hasDeprecationWarnings` from `watch` function, move it up one level. * Make watch mode work with multiple projects. * Refactor runJest and Reporters, show proper relative paths. * SearchSource now returns `tests: Array<Test>`. * Use one TestSequencer instance for all contexts. * Fix runJest-test. * Fix TestSequencer-test on Windows.
1 parent 3d7c87c commit 7703e22

27 files changed

+753
-684
lines changed

packages/jest-cli/src/SearchSource.js

+41-101
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,26 @@
1010

1111
'use strict';
1212

13-
import type {Config} from 'types/Config';
1413
import type {Context} from 'types/Context';
1514
import type {Glob, Path} from 'types/Config';
1615
import type {ResolveModuleConfig} from 'types/Resolve';
16+
import type {Test} from 'types/TestRunner';
1717

1818
const micromatch = require('micromatch');
1919

2020
const DependencyResolver = require('jest-resolve-dependencies');
2121

22-
const chalk = require('chalk');
2322
const changedFiles = require('jest-changed-files');
2423
const path = require('path');
2524
const {
2625
escapePathForRegex,
2726
replacePathSepForRegex,
2827
} = require('jest-regex-util');
2928

30-
type SearchSourceConfig = {
31-
roots: Array<Path>,
32-
testMatch: Array<Glob>,
33-
testRegex: string,
34-
testPathIgnorePatterns: Array<string>,
35-
};
36-
3729
type SearchResult = {|
3830
noSCM?: boolean,
39-
paths: Array<Path>,
4031
stats?: {[key: string]: number},
32+
tests: Array<Test>,
4133
total?: number,
4234
|};
4335

@@ -47,7 +39,7 @@ type Options = {|
4739
lastCommit?: boolean,
4840
|};
4941

50-
export type PatternInfo = {|
42+
export type PathPattern = {|
5143
input?: string,
5244
findRelatedTests?: boolean,
5345
lastCommit?: boolean,
@@ -64,16 +56,14 @@ const hg = changedFiles.hg;
6456
const determineSCM = path =>
6557
Promise.all([git.isGitRepository(path), hg.isHGRepository(path)]);
6658
const pathToRegex = p => replacePathSepForRegex(p);
67-
const pluralize = (word: string, count: number, ending: string) =>
68-
`${count} ${word}${count === 1 ? '' : ending}`;
6959

7060
const globsToMatcher = (globs: ?Array<Glob>) => {
7161
if (globs == null || globs.length === 0) {
7262
return () => true;
7363
}
7464

7565
const matchers = globs.map(each => micromatch.matcher(each, {dot: true}));
76-
return (path: Path) => matchers.some(each => each(path));
66+
return path => matchers.some(each => each(path));
7767
};
7868

7969
const regexToMatcher = (testRegex: string) => {
@@ -82,12 +72,18 @@ const regexToMatcher = (testRegex: string) => {
8272
}
8373

8474
const regex = new RegExp(pathToRegex(testRegex));
85-
return (path: Path) => regex.test(path);
75+
return path => regex.test(path);
8676
};
8777

78+
const toTests = (context, tests) =>
79+
tests.map(path => ({
80+
context,
81+
duration: undefined,
82+
path,
83+
}));
84+
8885
class SearchSource {
8986
_context: Context;
90-
_config: SearchSourceConfig;
9187
_options: ResolveModuleConfig;
9288
_rootPattern: RegExp;
9389
_testIgnorePattern: ?RegExp;
@@ -98,13 +94,9 @@ class SearchSource {
9894
testPathIgnorePatterns: (path: Path) => boolean,
9995
};
10096

101-
constructor(
102-
context: Context,
103-
config: SearchSourceConfig,
104-
options?: ResolveModuleConfig,
105-
) {
97+
constructor(context: Context, options?: ResolveModuleConfig) {
98+
const {config} = context;
10699
this._context = context;
107-
this._config = config;
108100
this._options = options || {
109101
skipNodeResolution: false,
110102
};
@@ -128,12 +120,12 @@ class SearchSource {
128120
}
129121

130122
_filterTestPathsWithStats(
131-
allPaths: Array<Path>,
123+
allPaths: Array<Test>,
132124
testPathPattern?: StrOrRegExpPattern,
133125
): SearchResult {
134126
const data = {
135-
paths: [],
136127
stats: {},
128+
tests: [],
137129
total: allPaths.length,
138130
};
139131

@@ -144,11 +136,10 @@ class SearchSource {
144136
}
145137

146138
const testCasesKeys = Object.keys(testCases);
147-
148-
data.paths = allPaths.filter(path => {
139+
data.tests = allPaths.filter(test => {
149140
return testCasesKeys.reduce(
150141
(flag, key) => {
151-
if (testCases[key](path)) {
142+
if (testCases[key](test.path)) {
152143
data.stats[key] = ++data.stats[key] || 1;
153144
return flag && true;
154145
}
@@ -164,7 +155,7 @@ class SearchSource {
164155

165156
_getAllTestPaths(testPathPattern: StrOrRegExpPattern): SearchResult {
166157
return this._filterTestPathsWithStats(
167-
this._context.hasteFS.getAllFiles(),
158+
toTests(this._context, this._context.hasteFS.getAllFiles()),
168159
testPathPattern,
169160
);
170161
}
@@ -184,12 +175,15 @@ class SearchSource {
184175
this._context.hasteFS,
185176
);
186177
return {
187-
paths: dependencyResolver.resolveInverse(
188-
allPaths,
189-
this.isTestFilePath.bind(this),
190-
{
191-
skipNodeResolution: this._options.skipNodeResolution,
192-
},
178+
tests: toTests(
179+
this._context,
180+
dependencyResolver.resolveInverse(
181+
allPaths,
182+
this.isTestFilePath.bind(this),
183+
{
184+
skipNodeResolution: this._options.skipNodeResolution,
185+
},
186+
),
193187
),
194188
};
195189
}
@@ -199,15 +193,17 @@ class SearchSource {
199193
const resolvedPaths = paths.map(p => path.resolve(process.cwd(), p));
200194
return this.findRelatedTests(new Set(resolvedPaths));
201195
}
202-
return {paths: []};
196+
return {tests: []};
203197
}
204198

205199
findChangedTests(options: Options): Promise<SearchResult> {
206-
return Promise.all(this._config.roots.map(determineSCM)).then(repos => {
200+
return Promise.all(
201+
this._context.config.roots.map(determineSCM),
202+
).then(repos => {
207203
if (!repos.every(([gitRepo, hgRepo]) => gitRepo || hgRepo)) {
208204
return {
209205
noSCM: true,
210-
paths: [],
206+
tests: [],
211207
};
212208
}
213209
return Promise.all(
@@ -223,73 +219,17 @@ class SearchSource {
223219
});
224220
}
225221

226-
getNoTestsFoundMessage(
227-
patternInfo: PatternInfo,
228-
config: Config,
229-
data: SearchResult,
230-
): string {
231-
if (patternInfo.onlyChanged) {
232-
return chalk.bold(
233-
'No tests found related to files changed since last commit.\n',
234-
) +
235-
chalk.dim(
236-
patternInfo.watch
237-
? 'Press `a` to run all tests, or run Jest with `--watchAll`.'
238-
: 'Run Jest without `-o` to run all tests.',
239-
);
240-
}
241-
242-
const testPathPattern = SearchSource.getTestPathPattern(patternInfo);
243-
const stats = data.stats || {};
244-
const statsMessage = Object.keys(stats)
245-
.map(key => {
246-
const value = key === 'testPathPattern' ? testPathPattern : config[key];
247-
if (value) {
248-
const matches = pluralize('match', stats[key], 'es');
249-
return ` ${key}: ${chalk.yellow(value)} - ${matches}`;
250-
}
251-
return null;
252-
})
253-
.filter(line => line)
254-
.join('\n');
255-
256-
return chalk.bold('No tests found') +
257-
'\n' +
258-
(data.total
259-
? ` ${pluralize('file', data.total || 0, 's')} checked.\n` +
260-
statsMessage
261-
: `No files found in ${config.rootDir}.\n` +
262-
`Make sure Jest's configuration does not exclude this directory.` +
263-
`\nTo set up Jest, make sure a package.json file exists.\n` +
264-
`Jest Documentation: ` +
265-
`facebook.github.io/jest/docs/configuration.html`);
266-
}
267-
268-
getTestPaths(patternInfo: PatternInfo): Promise<SearchResult> {
269-
if (patternInfo.onlyChanged) {
270-
return this.findChangedTests({lastCommit: patternInfo.lastCommit});
271-
} else if (patternInfo.findRelatedTests && patternInfo.paths) {
272-
return Promise.resolve(
273-
this.findRelatedTestsFromPattern(patternInfo.paths),
274-
);
275-
} else if (patternInfo.testPathPattern != null) {
276-
return Promise.resolve(
277-
this.findMatchingTests(patternInfo.testPathPattern),
278-
);
222+
getTestPaths(pattern: PathPattern): Promise<SearchResult> {
223+
if (pattern.onlyChanged) {
224+
return this.findChangedTests({lastCommit: pattern.lastCommit});
225+
} else if (pattern.findRelatedTests && pattern.paths) {
226+
return Promise.resolve(this.findRelatedTestsFromPattern(pattern.paths));
227+
} else if (pattern.testPathPattern != null) {
228+
return Promise.resolve(this.findMatchingTests(pattern.testPathPattern));
279229
} else {
280-
return Promise.resolve({paths: []});
230+
return Promise.resolve({tests: []});
281231
}
282232
}
283-
284-
static getTestPathPattern(patternInfo: PatternInfo): string {
285-
const pattern = patternInfo.testPathPattern;
286-
const input = patternInfo.input;
287-
const formattedPattern = `/${pattern || ''}/`;
288-
const formattedInput = patternInfo.shouldTreatInputAsPattern
289-
? `/${input || ''}/`
290-
: `"${input || ''}"`;
291-
return input === pattern ? formattedInput : formattedPattern;
292-
}
293233
}
294234

295235
module.exports = SearchSource;

packages/jest-cli/src/TestPathPatternPrompt.js

+24-22
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@
1111
'use strict';
1212

1313
import type {Context} from 'types/Context';
14-
import type {Config, Path} from 'types/Config';
14+
import type {Test} from 'types/TestRunner';
15+
import type SearchSource from './SearchSource';
1516

1617
const ansiEscapes = require('ansi-escapes');
1718
const chalk = require('chalk');
1819
const {getTerminalWidth} = require('./lib/terminalUtils');
1920
const highlight = require('./lib/highlight');
2021
const stringLength = require('string-length');
2122
const {trimAndFormatPath} = require('./reporters/utils');
22-
const SearchSource = require('./SearchSource');
2323
const Prompt = require('./lib/Prompt');
2424

25+
type SearchSources = Array<{|
26+
context: Context,
27+
searchSource: SearchSource,
28+
|}>;
29+
2530
const pluralizeFile = (total: number) => total === 1 ? 'file' : 'files';
2631

2732
const usage = () =>
@@ -34,17 +39,11 @@ const usage = () =>
3439
const usageRows = usage().split('\n').length;
3540

3641
module.exports = class TestPathPatternPrompt {
37-
_config: Config;
3842
_pipe: stream$Writable | tty$WriteStream;
3943
_prompt: Prompt;
40-
_searchSource: SearchSource;
41-
42-
constructor(
43-
config: Config,
44-
pipe: stream$Writable | tty$WriteStream,
45-
prompt: Prompt,
46-
) {
47-
this._config = config;
44+
_searchSources: SearchSources;
45+
46+
constructor(pipe: stream$Writable | tty$WriteStream, prompt: Prompt) {
4847
this._pipe = pipe;
4948
this._prompt = prompt;
5049
}
@@ -65,16 +64,19 @@ module.exports = class TestPathPatternPrompt {
6564
regex = new RegExp(pattern, 'i');
6665
} catch (e) {}
6766

68-
const paths = regex
69-
? this._searchSource.findMatchingTests(pattern).paths
70-
: [];
67+
let tests = [];
68+
if (regex) {
69+
this._searchSources.forEach(({searchSource, context}) => {
70+
tests = tests.concat(searchSource.findMatchingTests(pattern).tests);
71+
});
72+
}
7173

7274
this._pipe.write(ansiEscapes.eraseLine);
7375
this._pipe.write(ansiEscapes.cursorLeft);
74-
this._printTypeahead(pattern, paths, 10);
76+
this._printTypeahead(pattern, tests, 10);
7577
}
7678

77-
_printTypeahead(pattern: string, allResults: Array<Path>, max: number) {
79+
_printTypeahead(pattern: string, allResults: Array<Test>, max: number) {
7880
const total = allResults.length;
7981
const results = allResults.slice(0, max);
8082
const inputText = `${chalk.dim(' pattern \u203A')} ${pattern}`;
@@ -97,14 +99,14 @@ module.exports = class TestPathPatternPrompt {
9799
const padding = stringLength(prefix) + 2;
98100

99101
results
100-
.map(rawPath => {
102+
.map(({path, context}) => {
101103
const filePath = trimAndFormatPath(
102104
padding,
103-
this._config,
104-
rawPath,
105+
context.config,
106+
path,
105107
width,
106108
);
107-
return highlight(rawPath, filePath, pattern, this._config.rootDir);
109+
return highlight(path, filePath, pattern, context.config.rootDir);
108110
})
109111
.forEach(filePath =>
110112
this._pipe.write(`\n ${chalk.dim('\u203A')} ${filePath}`));
@@ -129,7 +131,7 @@ module.exports = class TestPathPatternPrompt {
129131
this._pipe.write(ansiEscapes.cursorRestorePosition);
130132
}
131133

132-
updateSearchSource(context: Context) {
133-
this._searchSource = new SearchSource(context, this._config);
134+
updateSearchSources(searchSources: SearchSources) {
135+
this._searchSources = searchSources;
134136
}
135137
};

0 commit comments

Comments
 (0)