Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Multi project runner #3156

Merged
merged 11 commits into from
Apr 18, 2017
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