Skip to content

Commit

Permalink
[RFC] Multi project runner (jestjs#3156)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
cpojer authored Apr 18, 2017
1 parent 2135898 commit 5805c61
Show file tree
Hide file tree
Showing 27 changed files with 753 additions and 684 deletions.
142 changes: 41 additions & 101 deletions packages/jest-cli/src/SearchSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,26 @@

'use strict';

import type {Config} from 'types/Config';
import type {Context} from 'types/Context';
import type {Glob, Path} from 'types/Config';
import type {ResolveModuleConfig} from 'types/Resolve';
import type {Test} from 'types/TestRunner';

const micromatch = require('micromatch');

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

const chalk = require('chalk');
const changedFiles = require('jest-changed-files');
const path = require('path');
const {
escapePathForRegex,
replacePathSepForRegex,
} = require('jest-regex-util');

type SearchSourceConfig = {
roots: Array<Path>,
testMatch: Array<Glob>,
testRegex: string,
testPathIgnorePatterns: Array<string>,
};

type SearchResult = {|
noSCM?: boolean,
paths: Array<Path>,
stats?: {[key: string]: number},
tests: Array<Test>,
total?: number,
|};

Expand All @@ -47,7 +39,7 @@ type Options = {|
lastCommit?: boolean,
|};

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

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

const matchers = globs.map(each => micromatch.matcher(each, {dot: true}));
return (path: Path) => matchers.some(each => each(path));
return path => matchers.some(each => each(path));
};

const regexToMatcher = (testRegex: string) => {
Expand All @@ -82,12 +72,18 @@ const regexToMatcher = (testRegex: string) => {
}

const regex = new RegExp(pathToRegex(testRegex));
return (path: Path) => regex.test(path);
return path => regex.test(path);
};

const toTests = (context, tests) =>
tests.map(path => ({
context,
duration: undefined,
path,
}));

class SearchSource {
_context: Context;
_config: SearchSourceConfig;
_options: ResolveModuleConfig;
_rootPattern: RegExp;
_testIgnorePattern: ?RegExp;
Expand All @@ -98,13 +94,9 @@ class SearchSource {
testPathIgnorePatterns: (path: Path) => boolean,
};

constructor(
context: Context,
config: SearchSourceConfig,
options?: ResolveModuleConfig,
) {
constructor(context: Context, options?: ResolveModuleConfig) {
const {config} = context;
this._context = context;
this._config = config;
this._options = options || {
skipNodeResolution: false,
};
Expand All @@ -128,12 +120,12 @@ class SearchSource {
}

_filterTestPathsWithStats(
allPaths: Array<Path>,
allPaths: Array<Test>,
testPathPattern?: StrOrRegExpPattern,
): SearchResult {
const data = {
paths: [],
stats: {},
tests: [],
total: allPaths.length,
};

Expand All @@ -144,11 +136,10 @@ class SearchSource {
}

const testCasesKeys = Object.keys(testCases);

data.paths = allPaths.filter(path => {
data.tests = allPaths.filter(test => {
return testCasesKeys.reduce(
(flag, key) => {
if (testCases[key](path)) {
if (testCases[key](test.path)) {
data.stats[key] = ++data.stats[key] || 1;
return flag && true;
}
Expand All @@ -164,7 +155,7 @@ class SearchSource {

_getAllTestPaths(testPathPattern: StrOrRegExpPattern): SearchResult {
return this._filterTestPathsWithStats(
this._context.hasteFS.getAllFiles(),
toTests(this._context, this._context.hasteFS.getAllFiles()),
testPathPattern,
);
}
Expand All @@ -184,12 +175,15 @@ class SearchSource {
this._context.hasteFS,
);
return {
paths: dependencyResolver.resolveInverse(
allPaths,
this.isTestFilePath.bind(this),
{
skipNodeResolution: this._options.skipNodeResolution,
},
tests: toTests(
this._context,
dependencyResolver.resolveInverse(
allPaths,
this.isTestFilePath.bind(this),
{
skipNodeResolution: this._options.skipNodeResolution,
},
),
),
};
}
Expand All @@ -199,15 +193,17 @@ class SearchSource {
const resolvedPaths = paths.map(p => path.resolve(process.cwd(), p));
return this.findRelatedTests(new Set(resolvedPaths));
}
return {paths: []};
return {tests: []};
}

findChangedTests(options: Options): Promise<SearchResult> {
return Promise.all(this._config.roots.map(determineSCM)).then(repos => {
return Promise.all(
this._context.config.roots.map(determineSCM),
).then(repos => {
if (!repos.every(([gitRepo, hgRepo]) => gitRepo || hgRepo)) {
return {
noSCM: true,
paths: [],
tests: [],
};
}
return Promise.all(
Expand All @@ -223,73 +219,17 @@ class SearchSource {
});
}

getNoTestsFoundMessage(
patternInfo: PatternInfo,
config: Config,
data: SearchResult,
): string {
if (patternInfo.onlyChanged) {
return chalk.bold(
'No tests found related to files changed since last commit.\n',
) +
chalk.dim(
patternInfo.watch
? 'Press `a` to run all tests, or run Jest with `--watchAll`.'
: 'Run Jest without `-o` to run all tests.',
);
}

const testPathPattern = SearchSource.getTestPathPattern(patternInfo);
const stats = data.stats || {};
const statsMessage = Object.keys(stats)
.map(key => {
const value = key === 'testPathPattern' ? testPathPattern : config[key];
if (value) {
const matches = pluralize('match', stats[key], 'es');
return ` ${key}: ${chalk.yellow(value)} - ${matches}`;
}
return null;
})
.filter(line => line)
.join('\n');

return chalk.bold('No tests found') +
'\n' +
(data.total
? ` ${pluralize('file', data.total || 0, 's')} checked.\n` +
statsMessage
: `No files found in ${config.rootDir}.\n` +
`Make sure Jest's configuration does not exclude this directory.` +
`\nTo set up Jest, make sure a package.json file exists.\n` +
`Jest Documentation: ` +
`facebook.github.io/jest/docs/configuration.html`);
}

getTestPaths(patternInfo: PatternInfo): Promise<SearchResult> {
if (patternInfo.onlyChanged) {
return this.findChangedTests({lastCommit: patternInfo.lastCommit});
} else if (patternInfo.findRelatedTests && patternInfo.paths) {
return Promise.resolve(
this.findRelatedTestsFromPattern(patternInfo.paths),
);
} else if (patternInfo.testPathPattern != null) {
return Promise.resolve(
this.findMatchingTests(patternInfo.testPathPattern),
);
getTestPaths(pattern: PathPattern): Promise<SearchResult> {
if (pattern.onlyChanged) {
return this.findChangedTests({lastCommit: pattern.lastCommit});
} else if (pattern.findRelatedTests && pattern.paths) {
return Promise.resolve(this.findRelatedTestsFromPattern(pattern.paths));
} else if (pattern.testPathPattern != null) {
return Promise.resolve(this.findMatchingTests(pattern.testPathPattern));
} else {
return Promise.resolve({paths: []});
return Promise.resolve({tests: []});
}
}

static getTestPathPattern(patternInfo: PatternInfo): string {
const pattern = patternInfo.testPathPattern;
const input = patternInfo.input;
const formattedPattern = `/${pattern || ''}/`;
const formattedInput = patternInfo.shouldTreatInputAsPattern
? `/${input || ''}/`
: `"${input || ''}"`;
return input === pattern ? formattedInput : formattedPattern;
}
}

module.exports = SearchSource;
46 changes: 24 additions & 22 deletions packages/jest-cli/src/TestPathPatternPrompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@
'use strict';

import type {Context} from 'types/Context';
import type {Config, Path} from 'types/Config';
import type {Test} from 'types/TestRunner';
import type SearchSource from './SearchSource';

const ansiEscapes = require('ansi-escapes');
const chalk = require('chalk');
const {getTerminalWidth} = require('./lib/terminalUtils');
const highlight = require('./lib/highlight');
const stringLength = require('string-length');
const {trimAndFormatPath} = require('./reporters/utils');
const SearchSource = require('./SearchSource');
const Prompt = require('./lib/Prompt');

type SearchSources = Array<{|
context: Context,
searchSource: SearchSource,
|}>;

const pluralizeFile = (total: number) => total === 1 ? 'file' : 'files';

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

module.exports = class TestPathPatternPrompt {
_config: Config;
_pipe: stream$Writable | tty$WriteStream;
_prompt: Prompt;
_searchSource: SearchSource;

constructor(
config: Config,
pipe: stream$Writable | tty$WriteStream,
prompt: Prompt,
) {
this._config = config;
_searchSources: SearchSources;

constructor(pipe: stream$Writable | tty$WriteStream, prompt: Prompt) {
this._pipe = pipe;
this._prompt = prompt;
}
Expand All @@ -65,16 +64,19 @@ module.exports = class TestPathPatternPrompt {
regex = new RegExp(pattern, 'i');
} catch (e) {}

const paths = regex
? this._searchSource.findMatchingTests(pattern).paths
: [];
let tests = [];
if (regex) {
this._searchSources.forEach(({searchSource, context}) => {
tests = tests.concat(searchSource.findMatchingTests(pattern).tests);
});
}

this._pipe.write(ansiEscapes.eraseLine);
this._pipe.write(ansiEscapes.cursorLeft);
this._printTypeahead(pattern, paths, 10);
this._printTypeahead(pattern, tests, 10);
}

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

results
.map(rawPath => {
.map(({path, context}) => {
const filePath = trimAndFormatPath(
padding,
this._config,
rawPath,
context.config,
path,
width,
);
return highlight(rawPath, filePath, pattern, this._config.rootDir);
return highlight(path, filePath, pattern, context.config.rootDir);
})
.forEach(filePath =>
this._pipe.write(`\n ${chalk.dim('\u203A')} ${filePath}`));
Expand All @@ -129,7 +131,7 @@ module.exports = class TestPathPatternPrompt {
this._pipe.write(ansiEscapes.cursorRestorePosition);
}

updateSearchSource(context: Context) {
this._searchSource = new SearchSource(context, this._config);
updateSearchSources(searchSources: SearchSources) {
this._searchSources = searchSources;
}
};
Loading

0 comments on commit 5805c61

Please sign in to comment.