diff --git a/packages/jest-cli/src/SearchSource.js b/packages/jest-cli/src/SearchSource.js index c7af5d95fe31..a2dd14ca09c0 100644 --- a/packages/jest-cli/src/SearchSource.js +++ b/packages/jest-cli/src/SearchSource.js @@ -10,16 +10,15 @@ '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 { @@ -27,17 +26,10 @@ const { replacePathSepForRegex, } = require('jest-regex-util'); -type SearchSourceConfig = { - roots: Array, - testMatch: Array, - testRegex: string, - testPathIgnorePatterns: Array, -}; - type SearchResult = {| noSCM?: boolean, - paths: Array, stats?: {[key: string]: number}, + tests: Array, total?: number, |}; @@ -47,7 +39,7 @@ type Options = {| lastCommit?: boolean, |}; -export type PatternInfo = {| +export type PathPattern = {| input?: string, findRelatedTests?: boolean, lastCommit?: boolean, @@ -64,8 +56,6 @@ 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) => { if (globs == null || globs.length === 0) { @@ -73,7 +63,7 @@ const globsToMatcher = (globs: ?Array) => { } 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) => { @@ -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; @@ -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, }; @@ -128,12 +120,12 @@ class SearchSource { } _filterTestPathsWithStats( - allPaths: Array, + allPaths: Array, testPathPattern?: StrOrRegExpPattern, ): SearchResult { const data = { - paths: [], stats: {}, + tests: [], total: allPaths.length, }; @@ -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; } @@ -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, ); } @@ -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, + }, + ), ), }; } @@ -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 { - 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( @@ -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 { - 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 { + 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; diff --git a/packages/jest-cli/src/TestPathPatternPrompt.js b/packages/jest-cli/src/TestPathPatternPrompt.js index 86531ba9d805..c2a8ca657bf9 100644 --- a/packages/jest-cli/src/TestPathPatternPrompt.js +++ b/packages/jest-cli/src/TestPathPatternPrompt.js @@ -11,7 +11,8 @@ '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'); @@ -19,9 +20,13 @@ 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 = () => @@ -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; } @@ -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, max: number) { + _printTypeahead(pattern: string, allResults: Array, max: number) { const total = allResults.length; const results = allResults.slice(0, max); const inputText = `${chalk.dim(' pattern \u203A')} ${pattern}`; @@ -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}`)); @@ -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; } }; diff --git a/packages/jest-cli/src/TestRunner.js b/packages/jest-cli/src/TestRunner.js index d1a0242cb499..eca2bbf65669 100644 --- a/packages/jest-cli/src/TestRunner.js +++ b/packages/jest-cli/src/TestRunner.js @@ -16,8 +16,7 @@ import type { } from 'types/TestResult'; import type {Config} from 'types/Config'; import type {Context} from 'types/Context'; -import type {HasteFS} from 'types/HasteMap'; -import type {RunnerContext} from 'types/Reporters'; +import type {PathPattern} from './SearchSource'; import type {Test, Tests} from 'types/TestRunner'; import type BaseReporter from './reporters/BaseReporter'; @@ -43,9 +42,12 @@ class CancelRun extends Error { } } -type Options = {| +export type Options = {| maxWorkers: number, - getTestSummary: () => string, + pattern: PathPattern, + startRun: () => *, + testNamePattern: string, + testPathPattern: string, |}; type OnTestFailure = (test: Test, err: TestError) => void; @@ -54,26 +56,14 @@ type OnTestSuccess = (test: Test, result: TestResult) => void; const TEST_WORKER_PATH = require.resolve('./TestWorker'); class TestRunner { - _context: Context; _config: Config; _options: Options; - _startRun: () => *; _dispatcher: ReporterDispatcher; - constructor( - hasteContext: Context, - config: Config, - options: Options, - startRun: () => *, - ) { + constructor(config: Config, options: Options) { this._config = config; - this._dispatcher = new ReporterDispatcher( - hasteContext.hasteFS, - options.getTestSummary, - ); - this._context = hasteContext; + this._dispatcher = new ReporterDispatcher(); this._options = options; - this._startRun = startRun; this._setupReporters(); } @@ -87,13 +77,14 @@ class TestRunner { async runTests(tests: Tests, watcher: TestWatcher) { const timings = []; + const contexts = new Set(); tests.forEach(test => { + contexts.add(test.context); if (test.duration) { timings.push(test.duration); } }); - const config = this._config; const aggregatedResults = createAggregatedResults(tests.length); const estimatedTime = Math.ceil( getEstimatedTime(timings, this._options.maxWorkers) / 1000, @@ -122,7 +113,7 @@ class TestRunner { } addResult(aggregatedResults, testResult); this._dispatcher.onTestResult(test, testResult, aggregatedResults); - this._bailIfNeeded(aggregatedResults, watcher); + this._bailIfNeeded(contexts, aggregatedResults, watcher); }; const onFailure = (test: Test, error: TestError) => { @@ -140,19 +131,21 @@ class TestRunner { }; const updateSnapshotState = () => { - const status = snapshot.cleanup( - this._context.hasteFS, - config.updateSnapshot, - ); - aggregatedResults.snapshot.filesRemoved += status.filesRemoved; - aggregatedResults.snapshot.didUpdate = config.updateSnapshot; - aggregatedResults.snapshot.failure = !!(!config.updateSnapshot && + contexts.forEach(context => { + const status = snapshot.cleanup( + context.hasteFS, + this._config.updateSnapshot, + ); + aggregatedResults.snapshot.filesRemoved += status.filesRemoved; + }); + aggregatedResults.snapshot.didUpdate = this._config.updateSnapshot; + aggregatedResults.snapshot.failure = !!(!this._config.updateSnapshot && (aggregatedResults.snapshot.unchecked || aggregatedResults.snapshot.unmatched || aggregatedResults.snapshot.filesRemoved)); }; - this._dispatcher.onRunStart(config, aggregatedResults, { + this._dispatcher.onRunStart(this._config, aggregatedResults, { estimatedTime, showStatus: !runInBand, }); @@ -170,7 +163,7 @@ class TestRunner { updateSnapshotState(); aggregatedResults.wasInterrupted = watcher.isInterrupted(); - this._dispatcher.onRunComplete(config, aggregatedResults); + this._dispatcher.onRunComplete(contexts, this._config, aggregatedResults); const anyTestFailures = !(aggregatedResults.numFailedTests === 0 && aggregatedResults.numRuntimeErrorTestSuites === 0); @@ -293,18 +286,26 @@ class TestRunner { this.addReporter(new CoverageReporter()); } - this.addReporter(new SummaryReporter()); + this.addReporter(new SummaryReporter(this._options)); if (config.notify) { - this.addReporter(new NotifyReporter(this._startRun)); + this.addReporter(new NotifyReporter(this._options.startRun)); } } - _bailIfNeeded(aggregatedResults: AggregatedResult, watcher: TestWatcher) { + _bailIfNeeded( + contexts: Set, + aggregatedResults: AggregatedResult, + watcher: TestWatcher, + ) { if (this._config.bail && aggregatedResults.numFailedTests !== 0) { if (watcher.isWatchMode()) { watcher.setState({interrupted: true}); } else { - this._dispatcher.onRunComplete(this._config, aggregatedResults); + this._dispatcher.onRunComplete( + contexts, + this._config, + aggregatedResults, + ); process.exit(1); } } @@ -426,10 +427,8 @@ const buildFailureTestResult = ( class ReporterDispatcher { _disabled: boolean; _reporters: Array; - _runnerContext: RunnerContext; - constructor(hasteFS: HasteFS, getTestSummary: () => string) { - this._runnerContext = {getTestSummary, hasteFS}; + constructor() { this._reporters = []; } @@ -445,7 +444,7 @@ class ReporterDispatcher { onTestResult(test, testResult, results) { this._reporters.forEach(reporter => - reporter.onTestResult(test, testResult, results, this._runnerContext)); + reporter.onTestResult(test, testResult, results)); } onTestStart(test) { @@ -454,12 +453,12 @@ class ReporterDispatcher { onRunStart(config, results, options) { this._reporters.forEach(reporter => - reporter.onRunStart(config, results, this._runnerContext, options)); + reporter.onRunStart(config, results, options)); } - onRunComplete(config, results) { + onRunComplete(contexts, config, results) { this._reporters.forEach(reporter => - reporter.onRunComplete(config, results, this._runnerContext)); + reporter.onRunComplete(contexts, config, results)); } // Return a list of last errors for every reporter diff --git a/packages/jest-cli/src/TestSequencer.js b/packages/jest-cli/src/TestSequencer.js index d722f997250c..13b6a9b0c6e6 100644 --- a/packages/jest-cli/src/TestSequencer.js +++ b/packages/jest-cli/src/TestSequencer.js @@ -11,7 +11,7 @@ import type {AggregatedResult} from 'types/TestResult'; import type {Context} from 'types/Context'; -import type {Tests} from 'types/TestRunner'; +import type {Test, Tests} from 'types/TestRunner'; const fs = require('fs'); const getCacheFilePath = require('jest-haste-map').getCacheFilePath; @@ -24,19 +24,31 @@ type Cache = { }; class TestSequencer { - _context: Context; - _cache: Cache; + _cache: Map; - constructor(context: Context) { - this._context = context; - this._cache = {}; + constructor() { + this._cache = new Map(); } - _getTestPerformanceCachePath() { - const {config} = this._context; + _getCachePath(context: Context) { + const {config} = context; return getCacheFilePath(config.cacheDirectory, 'perf-cache-' + config.name); } + _getCache(test: Test) { + const {context} = test; + if (!this._cache.has(context) && context.config.cache) { + try { + this._cache.set( + context, + JSON.parse(fs.readFileSync(this._getCachePath(context), 'utf8')), + ); + } catch (e) {} + } + + return this._cache.get(context) || {}; + } + // When running more tests than we have workers available, sort the tests // by size - big test files usually take longer to complete, so we run // them first in an effort to minimize worker idle time at the end of a @@ -45,57 +57,40 @@ class TestSequencer { // After a test run we store the time it took to run a test and on // subsequent runs we use that to run the slowest tests first, yielding the // fastest results. - sort(testPaths: Array): Tests { - const context = this._context; + sort(tests: Tests): Tests { const stats = {}; - const fileSize = filePath => - stats[filePath] || (stats[filePath] = fs.statSync(filePath).size); - const failed = filePath => - this._cache[filePath] && this._cache[filePath][0] === FAIL; - const time = filePath => this._cache[filePath] && this._cache[filePath][1]; + const fileSize = test => + stats[test.path] || (stats[test.path] = fs.statSync(test.path).size); + const hasFailed = (cache, test) => + cache[test.path] && cache[test.path][0] === FAIL; + const time = (cache, test) => cache[test.path] && cache[test.path][1]; - this._cache = {}; - try { - if (context.config.cache) { - this._cache = JSON.parse( - fs.readFileSync(this._getTestPerformanceCachePath(), 'utf8'), - ); - } - } catch (e) {} - - testPaths = testPaths.sort((pathA, pathB) => { - const failedA = failed(pathA); - const failedB = failed(pathB); + tests.forEach(test => test.duration = time(this._getCache(test), test)); + return tests.sort((testA, testB) => { + const cacheA = this._getCache(testA); + const cacheB = this._getCache(testB); + const failedA = hasFailed(cacheA, testA); + const failedB = hasFailed(cacheB, testB); + const hasTimeA = testA.duration != null; if (failedA !== failedB) { return failedA ? -1 : 1; + } else if (hasTimeA != (testB.duration != null)) { + // Check if only one of two tests has timing information + return hasTimeA != null ? 1 : -1; + } else if (testA.duration != null && testB.duration != null) { + return testA.duration < testB.duration ? 1 : -1; + } else { + return fileSize(testA) < fileSize(testB) ? 1 : -1; } - const timeA = time(pathA); - const timeB = time(pathB); - const hasTimeA = timeA != null; - const hasTimeB = timeB != null; - // Check if only one of two tests has timing information - if (hasTimeA != hasTimeB) { - return hasTimeA ? 1 : -1; - } - if (timeA != null && !timeB != null) { - return timeA < timeB ? 1 : -1; - } - return fileSize(pathA) < fileSize(pathB) ? 1 : -1; }); - - return testPaths.map(path => ({ - context, - duration: this._cache[path] && this._cache[path][1], - path, - })); } cacheResults(tests: Tests, results: AggregatedResult) { - const cache = this._cache; const map = Object.create(null); - tests.forEach(({path}) => map[path] = true); + tests.forEach(test => map[test.path] = test); results.testResults.forEach(testResult => { if (testResult && map[testResult.testFilePath] && !testResult.skipped) { + const cache = this._getCache(map[testResult.testFilePath]); const perf = testResult.perfStats; cache[testResult.testFilePath] = [ testResult.numFailingTests ? FAIL : SUCCESS, @@ -103,10 +98,9 @@ class TestSequencer { ]; } }); - fs.writeFileSync( - this._getTestPerformanceCachePath(), - JSON.stringify(cache), - ); + + this._cache.forEach((cache, context) => + fs.writeFileSync(this._getCachePath(context), JSON.stringify(cache))); } } diff --git a/packages/jest-cli/src/__tests__/SearchSource-test.js b/packages/jest-cli/src/__tests__/SearchSource-test.js index 5b8a49f10f20..7732d589d3b6 100644 --- a/packages/jest-cli/src/__tests__/SearchSource-test.js +++ b/packages/jest-cli/src/__tests__/SearchSource-test.js @@ -19,6 +19,8 @@ const testRegex = path.sep + '__testtests__' + path.sep; const testMatch = ['**/__testtests__/**/*']; const maxWorkers = 1; +const toPaths = tests => tests.map(({path}) => path); + let findMatchingTests; let normalizeConfig; @@ -45,15 +47,14 @@ describe('SearchSource', () => { rootDir: '.', roots: [], }).config; - return Runtime.createContext(config, {maxWorkers}).then(hasteMap => { - searchSource = new SearchSource(hasteMap, config); + return Runtime.createContext(config, {maxWorkers}).then(context => { + searchSource = new SearchSource(context); }); }); // micromatch doesn't support '..' through the globstar ('**') to avoid // infinite recursion. - - it('supports ../ paths and unix separators via textRegex', () => { + it('supports ../ paths and unix separators via testRegex', () => { if (process.platform !== 'win32') { config = normalizeConfig({ name, @@ -64,8 +65,8 @@ describe('SearchSource', () => { }).config; return Runtime.createContext(config, { maxWorkers, - }).then(hasteMap => { - searchSource = new SearchSource(hasteMap, config); + }).then(context => { + searchSource = new SearchSource(context); const path = '/path/to/__tests__/foo/bar/baz/../../../test.js'; expect(searchSource.isTestFilePath(path)).toEqual(true); @@ -95,8 +96,7 @@ describe('SearchSource', () => { findMatchingTests = config => Runtime.createContext(config, { maxWorkers, - }).then(hasteMap => - new SearchSource(hasteMap, config).findMatchingTests()); + }).then(context => new SearchSource(context).findMatchingTests()); }); it('finds tests matching a pattern via testRegex', () => { @@ -108,7 +108,7 @@ describe('SearchSource', () => { testRegex: 'not-really-a-test', }); return findMatchingTests(config).then(data => { - const relPaths = data.paths + const relPaths = toPaths(data.tests) .map(absPath => path.relative(rootDir, absPath)) .sort(); expect(relPaths).toEqual( @@ -129,7 +129,7 @@ describe('SearchSource', () => { testRegex: '', }); return findMatchingTests(config).then(data => { - const relPaths = data.paths + const relPaths = toPaths(data.tests) .map(absPath => path.relative(rootDir, absPath)) .sort(); expect(relPaths).toEqual( @@ -150,7 +150,7 @@ describe('SearchSource', () => { testRegex: 'test\.jsx?', }); return findMatchingTests(config).then(data => { - const relPaths = data.paths.map(absPath => + const relPaths = toPaths(data.tests).map(absPath => path.relative(rootDir, absPath)); expect(relPaths.sort()).toEqual([ path.normalize('__testtests__/test.js'), @@ -168,7 +168,7 @@ describe('SearchSource', () => { testRegex: '', }); return findMatchingTests(config).then(data => { - const relPaths = data.paths.map(absPath => + const relPaths = toPaths(data.tests).map(absPath => path.relative(rootDir, absPath)); expect(relPaths.sort()).toEqual([ path.normalize('__testtests__/test.js'), @@ -185,7 +185,7 @@ describe('SearchSource', () => { testRegex, }); return findMatchingTests(config).then(data => { - const relPaths = data.paths.map(absPath => + const relPaths = toPaths(data.tests).map(absPath => path.relative(rootDir, absPath)); expect(relPaths.sort()).toEqual([ path.normalize('__testtests__/test.js'), @@ -202,7 +202,7 @@ describe('SearchSource', () => { testRegex: '', }); return findMatchingTests(config).then(data => { - const relPaths = data.paths.map(absPath => + const relPaths = toPaths(data.tests).map(absPath => path.relative(rootDir, absPath)); expect(relPaths.sort()).toEqual([ path.normalize('__testtests__/test.js'), @@ -219,7 +219,7 @@ describe('SearchSource', () => { testMatch, }); return findMatchingTests(config).then(data => { - const relPaths = data.paths.map(absPath => + const relPaths = toPaths(data.tests).map(absPath => path.relative(rootDir, absPath)); expect(relPaths).toEqual([path.normalize('__testtests__/test.jsx')]); }); @@ -233,7 +233,7 @@ describe('SearchSource', () => { testMatch, }); return findMatchingTests(config).then(data => { - const relPaths = data.paths.map(absPath => + const relPaths = toPaths(data.tests).map(absPath => path.relative(rootDir, absPath)); expect(relPaths).toEqual([path.normalize('__testtests__/test.foobar')]); }); @@ -247,7 +247,7 @@ describe('SearchSource', () => { testMatch, }); return findMatchingTests(config).then(data => { - const relPaths = data.paths.map(absPath => + const relPaths = toPaths(data.tests).map(absPath => path.relative(rootDir, absPath)); expect(relPaths.sort()).toEqual([ path.normalize('__testtests__/test.js'), @@ -264,7 +264,7 @@ describe('SearchSource', () => { testRegex, }); return findMatchingTests(config).then(data => { - const relPaths = data.paths.map(absPath => + const relPaths = toPaths(data.tests).map(absPath => path.relative(rootDir, absPath)); expect(relPaths.sort()).toEqual([ path.normalize('__testtests__/test.js'), @@ -281,7 +281,7 @@ describe('SearchSource', () => { testRegex: '', }); return findMatchingTests(config).then(data => { - const relPaths = data.paths.map(absPath => + const relPaths = toPaths(data.tests).map(absPath => path.relative(rootDir, absPath)); expect(relPaths.sort()).toEqual([ path.normalize('__testtests__/test.js'), @@ -309,15 +309,15 @@ describe('SearchSource', () => { name: 'SearchSource-findRelatedTests-tests', rootDir, }); - Runtime.createContext(config, {maxWorkers}).then(hasteMap => { - searchSource = new SearchSource(hasteMap, config); + Runtime.createContext(config, {maxWorkers}).then(context => { + searchSource = new SearchSource(context); done(); }); }); it('makes sure a file is related to itself', () => { const data = searchSource.findRelatedTests(new Set([rootPath])); - expect(data.paths).toEqual([rootPath]); + expect(toPaths(data.tests)).toEqual([rootPath]); }); it('finds tests that depend directly on the path', () => { @@ -325,7 +325,7 @@ describe('SearchSource', () => { const loggingDep = path.join(rootDir, 'logging.js'); const parentDep = path.join(rootDir, 'ModuleWithSideEffects.js'); const data = searchSource.findRelatedTests(new Set([filePath])); - expect(data.paths.sort()).toEqual([ + expect(toPaths(data.tests).sort()).toEqual([ parentDep, filePath, loggingDep, @@ -342,8 +342,8 @@ describe('SearchSource', () => { rootDir, testMatch, }); - Runtime.createContext(config, {maxWorkers}).then(hasteMap => { - searchSource = new SearchSource(hasteMap, config); + Runtime.createContext(config, {maxWorkers}).then(context => { + searchSource = new SearchSource(context); done(); }); }); @@ -351,25 +351,25 @@ describe('SearchSource', () => { it('returns empty search result for empty input', () => { const input = []; const data = searchSource.findRelatedTestsFromPattern(input); - expect(data.paths).toEqual([]); + expect(data.tests).toEqual([]); }); it('returns empty search result for invalid input', () => { const input = ['non-existend.js']; const data = searchSource.findRelatedTestsFromPattern(input); - expect(data.paths).toEqual([]); + expect(data.tests).toEqual([]); }); it('returns empty search result if no related tests were found', () => { const input = ['no tests.js']; const data = searchSource.findRelatedTestsFromPattern(input); - expect(data.paths).toEqual([]); + expect(data.tests).toEqual([]); }); it('finds tests for a single file', () => { const input = ['packages/jest-cli/src/__tests__/test_root/module.jsx']; const data = searchSource.findRelatedTestsFromPattern(input); - expect(data.paths.sort()).toEqual([ + expect(toPaths(data.tests).sort()).toEqual([ path.join(rootDir, '__testtests__', 'test.js'), path.join(rootDir, '__testtests__', 'test.jsx'), ]); @@ -381,7 +381,7 @@ describe('SearchSource', () => { 'packages/jest-cli/src/__tests__/test_root/module.foobar', ]; const data = searchSource.findRelatedTestsFromPattern(input); - expect(data.paths.sort()).toEqual([ + expect(toPaths(data.tests).sort()).toEqual([ path.join(rootDir, '__testtests__', 'test.foobar'), path.join(rootDir, '__testtests__', 'test.js'), path.join(rootDir, '__testtests__', 'test.jsx'), diff --git a/packages/jest-cli/src/__tests__/TestRunner-test.js b/packages/jest-cli/src/__tests__/TestRunner-test.js index d5c186d904a0..e3fe9c45afc5 100644 --- a/packages/jest-cli/src/__tests__/TestRunner-test.js +++ b/packages/jest-cli/src/__tests__/TestRunner-test.js @@ -30,7 +30,7 @@ jest.mock('../TestWorker', () => {}); jest.mock('../reporters/DefaultReporter'); test('.addReporter() .removeReporter()', () => { - const runner = new TestRunner({}, {}, {}); + const runner = new TestRunner({}, {}); const reporter = new SummaryReporter(); runner.addReporter(reporter); expect(runner._dispatcher._reporters).toContain(reporter); @@ -43,7 +43,7 @@ describe('_createInBandTestRun()', () => { const config = {watch: true}; const rawModuleMap = jest.fn(); const context = {config, moduleMap: {getRawModuleMap: () => rawModuleMap}}; - const runner = new TestRunner(context, config, {maxWorkers: 2}); + const runner = new TestRunner(config, {maxWorkers: 2}); return runner ._createParallelTestRun( @@ -70,7 +70,7 @@ describe('_createInBandTestRun()', () => { test('does not inject the rawModuleMap in non watch mode', () => { const config = {watch: false}; const context = {config}; - const runner = new TestRunner(context, config, {maxWorkers: 1}); + const runner = new TestRunner(config, {maxWorkers: 1}); return runner ._createParallelTestRun( diff --git a/packages/jest-cli/src/__tests__/TestSequencer-test.js b/packages/jest-cli/src/__tests__/TestSequencer-test.js index e381e545109d..90a4c9aef759 100644 --- a/packages/jest-cli/src/__tests__/TestSequencer-test.js +++ b/packages/jest-cli/src/__tests__/TestSequencer-test.js @@ -12,6 +12,7 @@ jest.mock('fs'); const TestSequencer = require('../TestSequencer'); const fs = require('fs'); +const path = require('path'); const FAIL = 0; const SUCCESS = 1; @@ -26,15 +27,31 @@ const context = { }, }; +const secondContext = { + config: { + cache: true, + cacheDirectory: '/cache2', + name: 'test2', + }, +}; + +const toTests = paths => + paths.map(path => ({ + context, + duration: undefined, + path, + })); + beforeEach(() => { - sequencer = new TestSequencer(context); + sequencer = new TestSequencer(); fs.readFileSync = jest.fn(() => '{}'); fs.statSync = jest.fn(filePath => ({size: filePath.length})); + fs.writeFileSync = jest.fn(); }); test('sorts by file size if there is no timing information', () => { - expect(sequencer.sort(['/test-a.js', '/test-ab.js'])).toEqual([ + expect(sequencer.sort(toTests(['/test-a.js', '/test-ab.js']))).toEqual([ {context, duration: undefined, path: '/test-ab.js'}, {context, duration: undefined, path: '/test-a.js'}, ]); @@ -46,7 +63,7 @@ test('sorts based on timing information', () => { '/test-a.js': [SUCCESS, 5], '/test-ab.js': [SUCCESS, 3], })); - expect(sequencer.sort(['/test-a.js', '/test-ab.js'])).toEqual([ + expect(sequencer.sort(toTests(['/test-a.js', '/test-ab.js']))).toEqual([ {context, duration: 5, path: '/test-a.js'}, {context, duration: 3, path: '/test-ab.js'}, ]); @@ -61,7 +78,9 @@ test('sorts based on failures and timing information', () => { '/test-d.js': [SUCCESS, 2], })); expect( - sequencer.sort(['/test-a.js', '/test-ab.js', '/test-c.js', '/test-d.js']), + sequencer.sort( + toTests(['/test-a.js', '/test-ab.js', '/test-c.js', '/test-d.js']), + ), ).toEqual([ {context, duration: 6, path: '/test-c.js'}, {context, duration: 0, path: '/test-ab.js'}, @@ -80,13 +99,15 @@ test('sorts based on failures, timing information and file size', () => { '/test-efg.js': [FAIL], })); expect( - sequencer.sort([ - '/test-a.js', - '/test-ab.js', - '/test-c.js', - '/test-d.js', - '/test-efg.js', - ]), + sequencer.sort( + toTests([ + '/test-a.js', + '/test-ab.js', + '/test-c.js', + '/test-d.js', + '/test-efg.js', + ]), + ), ).toEqual([ {context, duration: undefined, path: '/test-efg.js'}, {context, duration: undefined, path: '/test-c.js'}, @@ -105,7 +126,7 @@ test('writes the cache based on the results', () => { })); const testPaths = ['/test-a.js', '/test-b.js', '/test-c.js']; - const tests = sequencer.sort(testPaths); + const tests = sequencer.sort(toTests(testPaths)); sequencer.cacheResults(tests, { testResults: [ { @@ -138,3 +159,58 @@ test('writes the cache based on the results', () => { '/test-c.js': [FAIL, 3], }); }); + +test('works with multiple contexts', () => { + fs.readFileSync = jest.fn( + cacheName => + cacheName.startsWith(path.sep + 'cache' + path.sep) + ? JSON.stringify({ + '/test-a.js': [SUCCESS, 5], + '/test-b.js': [FAIL, 1], + }) + : JSON.stringify({ + '/test-c.js': [FAIL], + }), + ); + + const testPaths = [ + {context, duration: null, path: '/test-a.js'}, + {context, duration: null, path: '/test-b.js'}, + {context: secondContext, duration: null, path: '/test-c.js'}, + ]; + const tests = sequencer.sort(testPaths); + sequencer.cacheResults(tests, { + testResults: [ + { + numFailingTests: 0, + perfStats: {end: 2, start: 1}, + testFilePath: '/test-a.js', + }, + { + numFailingTests: 0, + perfStats: {end: 0, start: 0}, + skipped: true, + testFilePath: '/test-b.js', + }, + { + numFailingTests: 0, + perfStats: {end: 4, start: 1}, + testFilePath: '/test-c.js', + }, + { + numFailingTests: 1, + perfStats: {end: 2, start: 1}, + testFilePath: '/test-x.js', + }, + ], + }); + const fileDataA = JSON.parse(fs.writeFileSync.mock.calls[0][1]); + expect(fileDataA).toEqual({ + '/test-a.js': [SUCCESS, 1], + '/test-b.js': [FAIL, 1], + }); + const fileDataB = JSON.parse(fs.writeFileSync.mock.calls[1][1]); + expect(fileDataB).toEqual({ + '/test-c.js': [SUCCESS, 3], + }); +}); diff --git a/packages/jest-cli/src/__tests__/runJest-test.js b/packages/jest-cli/src/__tests__/runJest-test.js index a46850ff21ac..f7bf2e29e3c5 100644 --- a/packages/jest-cli/src/__tests__/runJest-test.js +++ b/packages/jest-cli/src/__tests__/runJest-test.js @@ -30,17 +30,24 @@ jest.mock('../TestRunner', () => { jest.mock('../SearchSource', () => { const SearchSource = require.requireActual('../SearchSource'); - SearchSource.prototype.getTestPaths = () => - Promise.resolve({ - paths: ['/path.js'], + SearchSource.prototype.getTestPaths = function() { + return Promise.resolve({ stats: {}, + tests: [ + { + context: this._context, + path: '/path.js', + }, + ], total: 1, }); + }; return SearchSource; }); jest.mock('../TestSequencer', () => { const TestSequencer = require.requireActual('../TestSequencer'); + TestSequencer.prototype.sort = jest.fn(tests => tests); TestSequencer.prototype.cacheResults = jest.fn(); return TestSequencer; }); @@ -64,15 +71,25 @@ if (process.platform !== 'win32') { }); } -test('passes updateSnapshot to hasteContext.config', async () => { - const hasteContext = { - config: {rootDir}, - hasteFS, - }; +test('passes updateSnapshot to context.config', async () => { + const contexts = [ + { + config, + hasteFS, + }, + { + config: { + rootDir, + roots: [], + testPathIgnorePatterns: [], + }, + hasteFS, + }, + ]; const noop = () => {}; const argv = {}; const pipe = process.stdout; const testWatcher = new TestWatcher({isWatchMode: true}); - await runJest(hasteContext, config, argv, pipe, testWatcher, noop, noop); - expect(hasteContext.config.updateSnapshot).toBe(true); + await runJest(contexts, argv, pipe, testWatcher, noop, noop); + expect(contexts.every(({config}) => config.updateSnapshot)).toBe(true); }); diff --git a/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js b/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js index 0c460b23894c..1d8be3c41602 100644 --- a/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js +++ b/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js @@ -31,6 +31,10 @@ jest.mock( '../SearchSource', () => class { + constructor(context) { + this._context = context; + } + findMatchingTests(pattern) { const paths = [ './path/to/file1-test.js', @@ -46,7 +50,13 @@ jest.mock( './path/to/file11-test.js', ].filter(path => path.match(pattern)); - return {paths}; + return { + tests: paths.map(path => ({ + context: this._context, + duration: null, + path, + })), + }; } }, ); @@ -80,35 +90,23 @@ afterEach(runJestMock.mockReset); describe('Watch mode flows', () => { let pipe; - let hasteMap; + let hasteMapInstances; let argv; - let hasteContext; - let config; - let hasDeprecationWarnings; + let contexts; let stdin; beforeEach(() => { terminalWidth = 80; pipe = {write: jest.fn()}; - hasteMap = {on: () => {}}; + hasteMapInstances = [{on: () => {}}]; argv = {}; - hasteContext = {}; - config = {}; - hasDeprecationWarnings = false; + contexts = [{config: {}}]; stdin = new MockStdin(); }); it('Pressing "P" enters pattern mode', () => { - config = {rootDir: ''}; - watch( - config, - pipe, - argv, - hasteMap, - hasteContext, - hasDeprecationWarnings, - stdin, - ); + contexts[0].config = {rootDir: ''}; + watch(contexts, argv, pipe, hasteMapInstances, stdin); // Write a enter pattern mode stdin.emit(KEYS.P); @@ -144,16 +142,8 @@ describe('Watch mode flows', () => { }); it('Results in pattern mode get truncated appropriately', () => { - config = {rootDir: ''}; - watch( - config, - pipe, - argv, - hasteMap, - hasteContext, - hasDeprecationWarnings, - stdin, - ); + contexts[0].config = {rootDir: ''}; + watch(contexts, argv, pipe, hasteMapInstances, stdin); stdin.emit(KEYS.P); diff --git a/packages/jest-cli/src/__tests__/watch-test-name-pattern-mode-test.js b/packages/jest-cli/src/__tests__/watch-test-name-pattern-mode-test.js index c16b7bb2f6f6..82a9d1d9d0dc 100644 --- a/packages/jest-cli/src/__tests__/watch-test-name-pattern-mode-test.js +++ b/packages/jest-cli/src/__tests__/watch-test-name-pattern-mode-test.js @@ -104,35 +104,23 @@ afterEach(runJestMock.mockReset); describe('Watch mode flows', () => { let pipe; - let hasteMap; + let hasteMapInstances; let argv; - let hasteContext; - let config; - let hasDeprecationWarnings; + let contexts; let stdin; beforeEach(() => { terminalWidth = 80; pipe = {write: jest.fn()}; - hasteMap = {on: () => {}}; + hasteMapInstances = [{on: () => {}}]; argv = {}; - hasteContext = {}; - config = {}; - hasDeprecationWarnings = false; + contexts = [{config: {}}]; stdin = new MockStdin(); }); it('Pressing "T" enters pattern mode', () => { - config = {rootDir: ''}; - watch( - config, - pipe, - argv, - hasteMap, - hasteContext, - hasDeprecationWarnings, - stdin, - ); + contexts[0].config = {rootDir: ''}; + watch(contexts, argv, pipe, hasteMapInstances, stdin); // Write a enter pattern mode stdin.emit(KEYS.T); @@ -168,16 +156,8 @@ describe('Watch mode flows', () => { }); it('Results in pattern mode get truncated appropriately', () => { - config = {rootDir: ''}; - watch( - config, - pipe, - argv, - hasteMap, - hasteContext, - hasDeprecationWarnings, - stdin, - ); + contexts[0].config = {rootDir: ''}; + watch(contexts, argv, pipe, hasteMapInstances, stdin); stdin.emit(KEYS.T); diff --git a/packages/jest-cli/src/__tests__/watch-test.js b/packages/jest-cli/src/__tests__/watch-test.js index 3c10e7d21b6a..16b1e83deed6 100644 --- a/packages/jest-cli/src/__tests__/watch-test.js +++ b/packages/jest-cli/src/__tests__/watch-test.js @@ -37,40 +37,28 @@ afterEach(runJestMock.mockReset); describe('Watch mode flows', () => { let pipe; - let hasteMap; + let hasteMapInstances; let argv; - let hasteContext; - let config; - let hasDeprecationWarnings; + let contexts; let stdin; beforeEach(() => { + const config = {roots: [], testPathIgnorePatterns: [], testRegex: ''}; pipe = {write: jest.fn()}; - hasteMap = {on: () => {}}; + hasteMapInstances = [{on: () => {}}]; argv = {}; - hasteContext = {}; - config = {roots: [], testPathIgnorePatterns: [], testRegex: ''}; - hasDeprecationWarnings = false; + contexts = [{config}]; stdin = new MockStdin(); }); it('Correctly passing test path pattern', () => { argv.testPathPattern = 'test-*'; - config.testPathPattern = 'test-*'; + contexts[0].config.testPathPattern = 'test-*'; - watch( - config, - pipe, - argv, - hasteMap, - hasteContext, - hasDeprecationWarnings, - stdin, - ); + watch(contexts, argv, pipe, hasteMapInstances, stdin); expect(runJestMock).toBeCalledWith( - hasteContext, - config, + contexts, argv, pipe, new TestWatcher({isWatchMode: true}), @@ -81,21 +69,12 @@ describe('Watch mode flows', () => { it('Correctly passing test name pattern', () => { argv.testNamePattern = 'test-*'; - config.testNamePattern = 'test-*'; + contexts[0].config.testNamePattern = 'test-*'; - watch( - config, - pipe, - argv, - hasteMap, - hasteContext, - hasDeprecationWarnings, - stdin, - ); + watch(contexts, argv, pipe, hasteMapInstances, stdin); expect(runJestMock).toBeCalledWith( - hasteContext, - config, + contexts, argv, pipe, new TestWatcher({isWatchMode: true}), @@ -105,18 +84,9 @@ describe('Watch mode flows', () => { }); it('Runs Jest once by default and shows usage', () => { - watch( - config, - pipe, - argv, - hasteMap, - hasteContext, - hasDeprecationWarnings, - stdin, - ); + watch(contexts, argv, pipe, hasteMapInstances, stdin); expect(runJestMock).toBeCalledWith( - hasteContext, - config, + contexts, argv, pipe, new TestWatcher({isWatchMode: true}), @@ -127,15 +97,7 @@ describe('Watch mode flows', () => { }); it('Pressing "o" runs test in "only changed files" mode', () => { - watch( - config, - pipe, - argv, - hasteMap, - hasteContext, - hasDeprecationWarnings, - stdin, - ); + watch(contexts, argv, pipe, hasteMapInstances, stdin); runJestMock.mockReset(); stdin.emit(KEYS.O); @@ -149,15 +111,7 @@ describe('Watch mode flows', () => { }); it('Pressing "a" runs test in "watch all" mode', () => { - watch( - config, - pipe, - argv, - hasteMap, - hasteContext, - hasDeprecationWarnings, - stdin, - ); + watch(contexts, argv, pipe, hasteMapInstances, stdin); runJestMock.mockReset(); stdin.emit(KEYS.A); @@ -171,35 +125,19 @@ describe('Watch mode flows', () => { }); it('Pressing "ENTER" reruns the tests', () => { - watch( - config, - pipe, - argv, - hasteMap, - hasteContext, - hasDeprecationWarnings, - stdin, - ); + watch(contexts, argv, pipe, hasteMapInstances, stdin); expect(runJestMock).toHaveBeenCalledTimes(1); stdin.emit(KEYS.ENTER); expect(runJestMock).toHaveBeenCalledTimes(2); }); it('Pressing "u" reruns the tests in "update snapshot" mode', () => { - watch( - config, - pipe, - argv, - hasteMap, - hasteContext, - hasDeprecationWarnings, - stdin, - ); + watch(contexts, argv, pipe, hasteMapInstances, stdin); runJestMock.mockReset(); stdin.emit(KEYS.U); - expect(runJestMock.mock.calls[0][1]).toEqual({ + expect(runJestMock.mock.calls[0][0][0].config).toEqual({ roots: [], testPathIgnorePatterns: [], testRegex: '', diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index 884e8441e033..e08cdba10d9d 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -113,6 +113,11 @@ const options = { description: 'Use this flag to show full diffs instead of a patch.', type: 'boolean', }, + experimentalProjects: { + description: 'A list of projects that use Jest to run all tests in a ' + + 'single run.', + type: 'array', + }, findRelatedTests: { description: 'Find related tests for a list of source files that were ' + 'passed in as arguments. Useful for pre-commit hook integration to run ' + diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 8819bf08b236..b438957acff2 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -40,7 +40,13 @@ function run(argv?: Object, root?: Path) { root = pkgDir.sync(); } - getJest(root).runCLI(argv, root, result => { + argv.projects = argv.experimentalProjects; + if (!argv.projects) { + argv.projects = [root]; + } + + const execute = argv.projects.length === 1 ? getJest(root).runCLI : runCLI; + execute(argv, argv.projects, result => { const code = !result || result.success ? 0 : 1; process.on('exit', () => process.exit(code)); if (argv && argv.forceExit) { diff --git a/packages/jest-cli/src/cli/runCLI.js b/packages/jest-cli/src/cli/runCLI.js index d0f5526d0dcb..d2f8c28cbe8d 100644 --- a/packages/jest-cli/src/cli/runCLI.js +++ b/packages/jest-cli/src/cli/runCLI.js @@ -10,15 +10,16 @@ 'use strict'; import type {AggregatedResult} from 'types/TestResult'; -import type {Path} from 'types/Config'; +import type {Config, Path} from 'types/Config'; const Runtime = require('jest-runtime'); -const chalk = require('chalk'); const {Console, clearLine} = require('jest-util'); const {createDirectory} = require('jest-util'); +const chalk = require('chalk'); const createContext = require('../lib/createContext'); const getMaxWorkers = require('../lib/getMaxWorkers'); +const handleDeprecationWarnings = require('../lib/handleDeprecationWarnings'); const logDebugMessages = require('../lib/logDebugMessages'); const preRunMessage = require('../preRunMessage'); const readConfig = require('jest-config').readConfig; @@ -28,9 +29,9 @@ const watch = require('../watch'); const VERSION = require('../../package.json').version; -module.exports = ( +module.exports = async ( argv: Object, - root: Path, + roots: Array, onComplete: (results: ?AggregatedResult) => void, ) => { const realFs = require('fs'); @@ -38,51 +39,57 @@ module.exports = ( fs.gracefulify(realFs); const pipe = argv.json ? process.stderr : process.stdout; - argv = argv || {}; if (argv.version) { pipe.write(`v${VERSION}\n`); onComplete && onComplete(); return; } - const _run = async ({config, hasDeprecationWarnings}) => { + const _run = async ( + configs: Array<{config: Config, hasDeprecationWarnings: boolean}>, + ) => { if (argv.debug || argv.showConfig) { - logDebugMessages(config, pipe); + logDebugMessages(configs[0].config, pipe); } if (argv.showConfig) { process.exit(0); } - createDirectory(config.cacheDirectory); - const hasteMapInstance = Runtime.createHasteMap(config, { - console: new Console(pipe, pipe), - maxWorkers: getMaxWorkers(argv), - resetCache: !config.cache, - watch: config.watch, - }); + const hasteMapInstances = Array(configs.length); + const contexts = await Promise.all( + configs.map(async ({config}, index) => { + createDirectory(config.cacheDirectory); + const hasteMapInstance = Runtime.createHasteMap(config, { + console: new Console(pipe, pipe), + maxWorkers: getMaxWorkers(argv), + resetCache: !config.cache, + watch: config.watch, + }); + hasteMapInstances[index] = hasteMapInstance; + return createContext(config, await hasteMapInstance.build()); + }), + ); - const hasteMap = await hasteMapInstance.build(); - const context = createContext(config, hasteMap); if (argv.watch || argv.watchAll) { - return watch( - config, - pipe, - argv, - hasteMapInstance, - context, - hasDeprecationWarnings, - ); + if (configs.some(({hasDeprecationWarnings}) => hasDeprecationWarnings)) { + try { + await handleDeprecationWarnings(pipe, process.stdin); + return watch(contexts, argv, pipe, hasteMapInstances); + } catch (e) { + process.exit(0); + } + } + + return watch(contexts, argv, pipe, hasteMapInstances); } else { const startRun = () => { preRunMessage.print(pipe); - const testWatcher = new TestWatcher({isWatchMode: false}); - return runJest( - context, - config, + runJest( + contexts, argv, pipe, - testWatcher, + new TestWatcher({isWatchMode: false}), startRun, onComplete, ); @@ -91,10 +98,12 @@ module.exports = ( } }; - readConfig(argv, root).then(_run).catch(error => { + try { + await _run(await Promise.all(roots.map(root => readConfig(argv, root)))); + } catch (error) { clearLine(process.stderr); clearLine(process.stdout); console.error(chalk.red(error.stack)); process.exit(1); - }); + } }; diff --git a/packages/jest-cli/src/lib/getTestPathPatternInfo.js b/packages/jest-cli/src/lib/getTestPathPattern.js similarity index 94% rename from packages/jest-cli/src/lib/getTestPathPatternInfo.js rename to packages/jest-cli/src/lib/getTestPathPattern.js index d8a677660026..b53385ee1ab7 100644 --- a/packages/jest-cli/src/lib/getTestPathPatternInfo.js +++ b/packages/jest-cli/src/lib/getTestPathPattern.js @@ -9,7 +9,7 @@ */ 'use strict'; -import type {PatternInfo} from '../SearchSource'; +import type {PathPattern} from '../SearchSource'; const {clearLine} = require('jest-util'); const chalk = require('chalk'); @@ -33,7 +33,7 @@ const showTestPathPatternError = (testPathPattern: string) => { ); }; -module.exports = (argv: Object): PatternInfo => { +module.exports = (argv: Object): PathPattern => { if (argv.onlyChanged) { return { input: '', diff --git a/packages/jest-cli/src/lib/handleDeprecationWarnings.js b/packages/jest-cli/src/lib/handleDeprecationWarnings.js new file mode 100644 index 000000000000..5b3b9191e38f --- /dev/null +++ b/packages/jest-cli/src/lib/handleDeprecationWarnings.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +const chalk = require('chalk'); +const {KEYS} = require('../constants'); + +module.exports = ( + pipe: stream$Writable | tty$WriteStream, + stdin: stream$Readable | tty$ReadStream = process.stdin, +) => { + return new Promise((resolve, reject) => { + if (typeof stdin.setRawMode === 'function') { + const messages = [ + chalk.red('There are deprecation warnings.\n'), + chalk.dim(' \u203A Press ') + 'Enter' + chalk.dim(' to continue.'), + chalk.dim(' \u203A Press ') + 'Esc' + chalk.dim(' to exit.'), + ]; + + pipe.write(messages.join('\n')); + + // $FlowFixMe + stdin.setRawMode(true); + stdin.resume(); + stdin.setEncoding('hex'); + stdin.on('data', (key: string) => { + if (key === KEYS.ENTER) { + resolve(); + } else if ( + [KEYS.ESCAPE, KEYS.CONTROL_C, KEYS.CONTROL_D].indexOf(key) !== -1 + ) { + reject(); + } + }); + } else { + resolve(); + } + }); +}; diff --git a/packages/jest-cli/src/lib/setState.js b/packages/jest-cli/src/lib/setState.js index b2621a5e0bb9..8ec14ba30bfb 100644 --- a/packages/jest-cli/src/lib/setState.js +++ b/packages/jest-cli/src/lib/setState.js @@ -9,7 +9,7 @@ */ 'use strict'; -const getTestPathPatternInfo = require('./getTestPathPatternInfo'); +const getTestPathPattern = require('./getTestPathPattern'); module.exports = (argv: Object, mode: 'watch' | 'watchAll', options?: {}) => { options = options || {}; @@ -36,7 +36,7 @@ module.exports = (argv: Object, mode: 'watch' | 'watchAll', options?: {}) => { } argv.onlyChanged = false; - argv.onlyChanged = getTestPathPatternInfo(argv).input === '' && + argv.onlyChanged = getTestPathPattern(argv).input === '' && !argv.watchAll && !argv.testNamePattern; diff --git a/packages/jest-cli/src/reporters/BaseReporter.js b/packages/jest-cli/src/reporters/BaseReporter.js index 49ace2212b7d..e2b7b84e6a3c 100644 --- a/packages/jest-cli/src/reporters/BaseReporter.js +++ b/packages/jest-cli/src/reporters/BaseReporter.js @@ -11,8 +11,9 @@ import type {AggregatedResult, TestResult} from 'types/TestResult'; import type {Config} from 'types/Config'; +import type {Context} from 'types/Context'; import type {Test} from 'types/TestRunner'; -import type {ReporterOnStartOptions, RunnerContext} from 'types/Reporters'; +import type {ReporterOnStartOptions} from 'types/Reporters'; const preRunMessage = require('../preRunMessage'); @@ -26,7 +27,6 @@ class BaseReporter { onRunStart( config: Config, results: AggregatedResult, - runnerContext: RunnerContext, options: ReporterOnStartOptions, ) { preRunMessage.remove(process.stderr); @@ -37,9 +37,9 @@ class BaseReporter { onTestStart(test: Test) {} onRunComplete( + contexts: Set, config: Config, aggregatedResults: AggregatedResult, - runnerContext: RunnerContext, ): ?Promise {} _setError(error: Error) { diff --git a/packages/jest-cli/src/reporters/CoverageReporter.js b/packages/jest-cli/src/reporters/CoverageReporter.js index 4a5b46231f56..2a8178522299 100644 --- a/packages/jest-cli/src/reporters/CoverageReporter.js +++ b/packages/jest-cli/src/reporters/CoverageReporter.js @@ -11,8 +11,8 @@ import type {AggregatedResult, CoverageMap, TestResult} from 'types/TestResult'; import type {Config} from 'types/Config'; +import type {Context} from 'types/Context'; import type {Test} from 'types/TestRunner'; -import type {RunnerContext} from 'types/Reporters'; const BaseReporter = require('./BaseReporter'); @@ -60,11 +60,11 @@ class CoverageReporter extends BaseReporter { } onRunComplete( + contexts: Set, config: Config, aggregatedResults: AggregatedResult, - runnerContext: RunnerContext, ) { - this._addUntestedFiles(config, runnerContext); + this._addUntestedFiles(contexts); let map = this._coverageMap; let sourceFinder: Object; if (config.mapCoverage) { @@ -104,37 +104,42 @@ class CoverageReporter extends BaseReporter { this._checkThreshold(map, config); } - _addUntestedFiles(config: Config, runnerContext: RunnerContext) { - if (config.collectCoverageFrom && config.collectCoverageFrom.length) { + _addUntestedFiles(contexts: Set) { + const files = []; + contexts.forEach(context => { + const config = context.config; + if (config.collectCoverageFrom && config.collectCoverageFrom.length) { + context.hasteFS + .matchFilesWithGlob(config.collectCoverageFrom, config.rootDir) + .forEach(filePath => + files.push({ + config, + path: filePath, + })); + } + }); + if (files.length) { if (isInteractive) { process.stderr.write( RUNNING_TEST_COLOR('Running coverage on untested files...'), ); } - const files = runnerContext.hasteFS.matchFilesWithGlob( - config.collectCoverageFrom, - config.rootDir, - ); - - files.forEach(filename => { - if (!this._coverageMap.data[filename]) { + files.forEach(({config, path}) => { + if (!this._coverageMap.data[path]) { try { - const source = fs.readFileSync(filename).toString(); - const result = generateEmptyCoverage(source, filename, config); + const source = fs.readFileSync(path).toString(); + const result = generateEmptyCoverage(source, path, config); if (result) { this._coverageMap.addFileCoverage(result.coverage); if (result.sourceMapPath) { - this._sourceMapStore.registerURL( - filename, - result.sourceMapPath, - ); + this._sourceMapStore.registerURL(path, result.sourceMapPath); } } } catch (e) { console.error( chalk.red( ` - Failed to collect coverage from ${filename} + Failed to collect coverage from ${path} ERROR: ${e} STACK: ${e.stack} `, diff --git a/packages/jest-cli/src/reporters/DefaultReporter.js b/packages/jest-cli/src/reporters/DefaultReporter.js index 987090add930..44387138c4a4 100644 --- a/packages/jest-cli/src/reporters/DefaultReporter.js +++ b/packages/jest-cli/src/reporters/DefaultReporter.js @@ -15,7 +15,7 @@ import type {AggregatedResult, TestResult} from 'types/TestResult'; import type {Config, Path} from 'types/Config'; import type {Test} from 'types/TestRunner'; -import type {ReporterOnStartOptions, RunnerContext} from 'types/Reporters'; +import type {ReporterOnStartOptions} from 'types/Reporters'; const BaseReporter = require('./BaseReporter'); const Status = require('./Status'); @@ -34,10 +34,7 @@ const isInteractive = process.stdin.isTTY && !isCI; class DefaultReporter extends BaseReporter { _clear: string; // ANSI clear sequence for the last printed status - _currentlyRunning: Map; - _currentStatusHeight: number; _err: write; - _lastAggregatedResults: AggregatedResult; _out: write; _status: Status; @@ -114,7 +111,6 @@ class DefaultReporter extends BaseReporter { onRunStart( config: Config, aggregatedResults: AggregatedResult, - runnerContext: RunnerContext, options: ReporterOnStartOptions, ) { this._status.runStarted(aggregatedResults, options); diff --git a/packages/jest-cli/src/reporters/NotifyReporter.js b/packages/jest-cli/src/reporters/NotifyReporter.js index 686f43bdf0e8..5ea323511ef2 100644 --- a/packages/jest-cli/src/reporters/NotifyReporter.js +++ b/packages/jest-cli/src/reporters/NotifyReporter.js @@ -11,6 +11,7 @@ import type {AggregatedResult} from 'types/TestResult'; import type {Config} from 'types/Config'; +import type {Context} from 'types/Context'; const BaseReporter = require('./BaseReporter'); const notifier = require('node-notifier'); @@ -29,7 +30,11 @@ class NotifyReporter extends BaseReporter { this._startRun = startRun; } - onRunComplete(config: Config, result: AggregatedResult): void { + onRunComplete( + contexts: Set, + config: Config, + result: AggregatedResult, + ): void { const success = result.numFailedTests === 0 && result.numRuntimeErrorTestSuites === 0; diff --git a/packages/jest-cli/src/reporters/SummaryReporter.js b/packages/jest-cli/src/reporters/SummaryReporter.js index 2014bab55d9f..1bd16e278a60 100644 --- a/packages/jest-cli/src/reporters/SummaryReporter.js +++ b/packages/jest-cli/src/reporters/SummaryReporter.js @@ -11,7 +11,10 @@ import type {AggregatedResult, SnapshotSummary} from 'types/TestResult'; import type {Config} from 'types/Config'; -import type {ReporterOnStartOptions, RunnerContext} from 'types/Reporters'; +import type {Context} from 'types/Context'; +import type {Options as SummaryReporterOptions} from '../TestRunner'; +import type {PathPattern} from '../SearchSource'; +import type {ReporterOnStartOptions} from 'types/Reporters'; const BaseReporter = require('./BaseReporter'); @@ -55,11 +58,13 @@ const NPM_EVENTS = new Set([ 'postrestart', ]); -class SummareReporter extends BaseReporter { +class SummaryReporter extends BaseReporter { _estimatedTime: number; + _options: SummaryReporterOptions; - constructor() { + constructor(options: SummaryReporterOptions) { super(); + this._options = options; this._estimatedTime = 0; } @@ -77,17 +82,16 @@ class SummareReporter extends BaseReporter { onRunStart( config: Config, aggregatedResults: AggregatedResult, - runnerContext: RunnerContext, options: ReporterOnStartOptions, ) { - super.onRunStart(config, aggregatedResults, runnerContext, options); + super.onRunStart(config, aggregatedResults, options); this._estimatedTime = options.estimatedTime; } onRunComplete( + contexts: Set, config: Config, aggregatedResults: AggregatedResult, - runnerContext: RunnerContext, ) { const {numTotalTestSuites, testResults, wasInterrupted} = aggregatedResults; if (numTotalTestSuites) { @@ -109,7 +113,12 @@ class SummareReporter extends BaseReporter { if (numTotalTestSuites) { const testSummary = wasInterrupted ? chalk.bold.red('Test run was interrupted.') - : runnerContext.getTestSummary(); + : this._getTestSummary( + contexts, + this._options.pattern, + this._options.testNamePattern, + this._options.testPathPattern, + ); this.log( getSummary(aggregatedResults, { estimatedTime: this._estimatedTime, @@ -227,6 +236,31 @@ class SummareReporter extends BaseReporter { this.log(''); // print empty line } } + + _getTestSummary( + contexts: Set, + pattern: PathPattern, + testNamePattern: string, + testPathPattern: string, + ) { + const testInfo = pattern.onlyChanged + ? chalk.dim(' related to changed files') + : pattern.input !== '' ? chalk.dim(' matching ') + testPathPattern : ''; + + const nameInfo = testNamePattern + ? chalk.dim(' with tests matching ') + `"${testNamePattern}"` + : ''; + + const contextInfo = contexts.size > 1 + ? chalk.dim(' in ') + contexts.size + chalk.dim(' projects') + : ''; + + return chalk.dim('Ran all test suites') + + testInfo + + nameInfo + + contextInfo + + chalk.dim('.'); + } } -module.exports = SummareReporter; +module.exports = SummaryReporter; diff --git a/packages/jest-cli/src/reporters/__tests__/CoverageReporter-test.js b/packages/jest-cli/src/reporters/__tests__/CoverageReporter-test.js index 2bcd9db1c393..fa5474628a39 100644 --- a/packages/jest-cli/src/reporters/__tests__/CoverageReporter-test.js +++ b/packages/jest-cli/src/reporters/__tests__/CoverageReporter-test.js @@ -83,6 +83,7 @@ describe('onRunComplete', () => { it('getLastError() returns an error when threshold is not met', () => { testReporter.onRunComplete( + new Set(), { collectCoverage: true, coverageThreshold: { @@ -99,6 +100,7 @@ describe('onRunComplete', () => { it('getLastError() returns `undefined` when threshold is met', () => { testReporter.onRunComplete( + new Set(), { collectCoverage: true, coverageThreshold: { diff --git a/packages/jest-cli/src/runJest.js b/packages/jest-cli/src/runJest.js index 3340775fbfaa..5c91917d6158 100644 --- a/packages/jest-cli/src/runJest.js +++ b/packages/jest-cli/src/runJest.js @@ -9,9 +9,8 @@ */ 'use strict'; -import type {Config} from 'types/Config'; import type {Context} from 'types/Context'; -import type {PatternInfo} from './SearchSource'; +import type TestWatcher from './TestWatcher'; const fs = require('graceful-fs'); @@ -19,129 +18,186 @@ const SearchSource = require('./SearchSource'); const TestRunner = require('./TestRunner'); const TestSequencer = require('./TestSequencer'); -const getTestPathPatternInfo = require('./lib/getTestPathPatternInfo'); +const getTestPathPattern = require('./lib/getTestPathPattern'); const chalk = require('chalk'); const {Console, formatTestResults} = require('jest-util'); const getMaxWorkers = require('./lib/getMaxWorkers'); const path = require('path'); const setState = require('./lib/setState'); -const getTestSummary = (argv: Object, patternInfo: PatternInfo) => { - const testPathPattern = SearchSource.getTestPathPattern(patternInfo); - const testInfo = patternInfo.onlyChanged - ? chalk.dim(' related to changed files') - : patternInfo.input !== '' ? chalk.dim(' matching ') + testPathPattern : ''; +const setConfig = (contexts, newConfig) => + contexts.forEach( + context => context.config = Object.assign({}, context.config, newConfig), + ); + +const formatTestPathPattern = pattern => { + const testPattern = pattern.testPathPattern; + const input = pattern.input; + const formattedPattern = `/${testPattern || ''}/`; + const formattedInput = pattern.shouldTreatInputAsPattern + ? `/${input || ''}/` + : `"${input || ''}"`; + return input === testPattern ? formattedInput : formattedPattern; +}; + +const getNoTestsFoundMessage = (testRunData, pattern) => { + if (pattern.onlyChanged) { + return chalk.bold( + 'No tests found related to files changed since last commit.\n', + ) + + chalk.dim( + pattern.watch + ? 'Press `a` to run all tests, or run Jest with `--watchAll`.' + : 'Run Jest without `-o` to run all tests.', + ); + } + + const pluralize = (word: string, count: number, ending: string) => + `${count} ${word}${count === 1 ? '' : ending}`; + const testPathPattern = formatTestPathPattern(pattern); + const individualResults = testRunData.map(testRun => { + const stats = testRun.matches.stats || {}; + const config = testRun.context.config; + const statsMessage = Object.keys(stats) + .map(key => { + if (key === 'roots' && config.roots.length === 1) { + return null; + } + const value = 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 testRun.matches.total + ? `In ${chalk.bold(config.rootDir)}\n` + + ` ${pluralize('file', testRun.matches.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`; + }); + return chalk.bold('No tests found') + + '\n' + + individualResults.join('\n') + + '\n' + + `Pattern: ${chalk.yellow(testPathPattern)} - 0 matches`; +}; + +const getTestPaths = async (context, pattern, argv, pipe) => { + const source = new SearchSource(context); + let data = await source.getTestPaths(pattern); + if (!data.tests.length) { + if (pattern.onlyChanged && data.noSCM) { + if (context.config.watch) { + // Run all the tests + setState(argv, 'watchAll', { + noSCM: true, + }); + pattern = getTestPathPattern(argv); + data = await source.getTestPaths(pattern); + } else { + new Console(pipe, pipe).log( + 'Jest can only find uncommitted changed files in a git or hg ' + + 'repository. If you make your project a git or hg ' + + 'repository (`git init` or `hg init`), Jest will be able ' + + 'to only run tests related to files changed since the last ' + + 'commit.', + ); + } + } + } + return data; +}; - const nameInfo = argv.testNamePattern - ? chalk.dim(' with tests matching ') + `"${argv.testNamePattern}"` - : ''; +const processResults = (runResults, options) => { + if (options.testResultsProcessor) { + /* $FlowFixMe */ + runResults = require(options.testResultsProcessor)(runResults); + } + if (options.isJSON) { + if (options.outputFile) { + const outputFile = path.resolve(process.cwd(), options.outputFile); - return chalk.dim('Ran all test suites') + - testInfo + - nameInfo + - chalk.dim('.'); + fs.writeFileSync( + outputFile, + JSON.stringify(formatTestResults(runResults)), + ); + process.stdout.write( + `Test results written to: ` + + `${path.relative(process.cwd(), outputFile)}\n`, + ); + } else { + process.stdout.write(JSON.stringify(formatTestResults(runResults))); + } + } + return options.onComplete && options.onComplete(runResults); }; const runJest = async ( - hasteContext: Context, - config: Config, + contexts: Array, argv: Object, pipe: stream$Writable | tty$WriteStream, - testWatcher: any, + testWatcher: TestWatcher, startRun: () => *, onComplete: (testResults: any) => void, ) => { + const context = contexts[0]; const maxWorkers = getMaxWorkers(argv); - const source = new SearchSource(hasteContext, config); - const testRunnerOptions = { - getTestSummary: () => getTestSummary(argv, patternInfo), - maxWorkers, - }; - let patternInfo = getTestPathPatternInfo(argv); - - const processTests = data => { - if (!data.paths.length) { - const localConsole = new Console(pipe, pipe); - if (patternInfo.onlyChanged && data.noSCM) { - if (config.watch) { - // Run all the tests - setState(argv, 'watchAll', { - noSCM: true, - }); - patternInfo = getTestPathPatternInfo(argv); - return source.getTestPaths(patternInfo); - } else { - localConsole.log( - 'Jest can only find uncommitted changed files in a git or hg ' + - 'repository. If you make your project a git or hg ' + - 'repository (`git init` or `hg init`), Jest will be able ' + - 'to only run tests related to files changed since the last ' + - 'commit.', - ); - } - } - - localConsole.log( - source.getNoTestsFoundMessage(patternInfo, config, data), - ); - } - - if ( - data.paths.length === 1 && - hasteContext.config.silent !== true && - hasteContext.config.verbose !== false - ) { - // $FlowFixMe - config = (hasteContext.config = Object.assign({}, hasteContext.config, { - verbose: true, - })); - } - - return data; - }; - - const runTests = async tests => - new TestRunner(hasteContext, config, testRunnerOptions, startRun).runTests( - tests, - testWatcher, - ); + const pattern = getTestPathPattern(argv); + const sequencer = new TestSequencer(); + let allTests = []; + const testRunData = await Promise.all( + contexts.map(async context => { + const matches = await getTestPaths(context, pattern, argv, pipe); + allTests = allTests.concat(matches.tests); + return {context, matches}; + }), + ); + + allTests = sequencer.sort(allTests); + if (!allTests.length) { + new Console(pipe, pipe).log(getNoTestsFoundMessage(testRunData, pattern)); + } else if ( + allTests.length === 1 && + context.config.silent !== true && + context.config.verbose !== false + ) { + setConfig(contexts, {verbose: true}); + } - const processResults = runResults => { - if (config.testResultsProcessor) { - /* $FlowFixMe */ - runResults = require(config.testResultsProcessor)(runResults); - } - if (argv.json) { - if (argv.outputFile) { - const outputFile = path.resolve(process.cwd(), argv.outputFile); + if (context.config.updateSnapshot === true) { + setConfig(contexts, {updateSnapshot: true}); + } - fs.writeFileSync( - outputFile, - JSON.stringify(formatTestResults(runResults)), - ); - process.stdout.write( - `Test results written to: ` + - `${path.relative(process.cwd(), outputFile)}\n`, - ); - } else { - process.stdout.write(JSON.stringify(formatTestResults(runResults))); - } - } - return onComplete && onComplete(runResults); - }; - - const data = await source.getTestPaths(patternInfo); - if (config.updateSnapshot === true) { - hasteContext.config = Object.assign({}, hasteContext.config, { - updateSnapshot: true, - }); + // When using more than one context, make all printed paths relative to the + // current cwd. + if (contexts.length > 1) { + setConfig(contexts, {rootDir: process.cwd()}); } - processTests(data); - const sequencer = new TestSequencer(hasteContext); - const tests = sequencer.sort(data.paths); - const results = await runTests(tests); - sequencer.cacheResults(tests, results); - return processResults(results); + + const results = await new TestRunner(context.config, { + maxWorkers, + pattern, + startRun, + testNamePattern: argv.testNamePattern, + testPathPattern: formatTestPathPattern(pattern), + }).runTests(allTests, testWatcher); + + sequencer.cacheResults(allTests, results); + + return processResults(results, { + isJSON: argv.json, + onComplete, + outputFile: argv.outputFile, + testResultsProcessor: context.config.testResultsProcessor, + }); }; module.exports = runJest; diff --git a/packages/jest-cli/src/watch.js b/packages/jest-cli/src/watch.js index 137c5acf989b..7606e7bdc72d 100644 --- a/packages/jest-cli/src/watch.js +++ b/packages/jest-cli/src/watch.js @@ -10,7 +10,6 @@ 'use strict'; import type {Context} from 'types/Context'; -import type {Config} from 'types/Config'; const ansiEscapes = require('ansi-escapes'); const chalk = require('chalk'); @@ -22,29 +21,22 @@ const isValidPath = require('./lib/isValidPath'); const preRunMessage = require('./preRunMessage'); const runJest = require('./runJest'); const setState = require('./lib/setState'); +const SearchSource = require('./SearchSource'); const TestWatcher = require('./TestWatcher'); const Prompt = require('./lib/Prompt'); const TestPathPatternPrompt = require('./TestPathPatternPrompt'); const TestNamePatternPrompt = require('./TestNamePatternPrompt'); const {KEYS, CLEAR} = require('./constants'); +let hasExitListener = false; + const watch = ( - config: Config, - pipe: stream$Writable | tty$WriteStream, + contexts: Array, argv: Object, - hasteMap: HasteMap, - hasteContext: Context, - hasDeprecationWarnings?: boolean, + pipe: stream$Writable | tty$WriteStream, + hasteMapInstances: Array, stdin?: stream$Readable | tty$ReadStream = process.stdin, ) => { - if (hasDeprecationWarnings) { - return handleDeprecatedWarnings(pipe, stdin) - .then(() => { - watch(config, pipe, argv, hasteMap, hasteContext); - }) - .catch(() => process.exit(0)); - } - setState(argv, argv.watch ? 'watch' : 'watchAll', { testNamePattern: argv.testNamePattern, testPathPattern: argv.testPathPattern || @@ -52,35 +44,55 @@ const watch = ( }); const prompt = new Prompt(); - const testPathPatternPrompt = new TestPathPatternPrompt(config, pipe, prompt); + const testPathPatternPrompt = new TestPathPatternPrompt(pipe, prompt); const testNamePatternPrompt = new TestNamePatternPrompt(pipe, prompt); + let searchSources = contexts.map(context => ({ + context, + searchSource: new SearchSource(context), + })); let hasSnapshotFailure = false; let isRunning = false; let testWatcher; let shouldDisplayWatchUsage = true; let isWatchUsageDisplayed = false; - testPathPatternPrompt.updateSearchSource(hasteContext); + testPathPatternPrompt.updateSearchSources(searchSources); - hasteMap.on('change', ({eventsQueue, hasteFS, moduleMap}) => { - const validPaths = eventsQueue.filter(({filePath}) => { - return isValidPath(config, filePath); - }); + hasteMapInstances.forEach((hasteMapInstance, index) => { + hasteMapInstance.on('change', ({eventsQueue, hasteFS, moduleMap}) => { + const validPaths = eventsQueue.filter(({filePath}) => { + return isValidPath(contexts[index].config, filePath); + }); - if (validPaths.length) { - hasteContext = createContext(config, {hasteFS, moduleMap}); - prompt.abort(); - testPathPatternPrompt.updateSearchSource(hasteContext); - startRun(); - } + if (validPaths.length) { + const context = (contexts[index] = createContext( + contexts[index].config, + { + hasteFS, + moduleMap, + }, + )); + prompt.abort(); + searchSources = searchSources.slice(); + searchSources[index] = { + context, + searchSource: new SearchSource(context), + }; + testPathPatternPrompt.updateSearchSources(searchSources); + startRun(); + } + }); }); - process.on('exit', () => { - if (prompt.isEntering()) { - pipe.write(ansiEscapes.cursorDown()); - pipe.write(ansiEscapes.eraseDown); - } - }); + if (!hasExitListener) { + hasExitListener = true; + process.on('exit', () => { + if (prompt.isEntering()) { + pipe.write(ansiEscapes.cursorDown()); + pipe.write(ansiEscapes.eraseDown); + } + }); + } const startRun = (overrideConfig: Object = {}) => { if (isRunning) { @@ -91,44 +103,39 @@ const watch = ( isInteractive && pipe.write(CLEAR); preRunMessage.print(pipe); isRunning = true; - return runJest( - hasteContext, + contexts.forEach(context => { // $FlowFixMe - Object.freeze( + context.config = Object.freeze( // $FlowFixMe Object.assign( { testNamePattern: argv.testNamePattern, testPathPattern: argv.testPathPattern, }, - config, + context.config, overrideConfig, ), - ), - argv, - pipe, - testWatcher, - startRun, - results => { - isRunning = false; - hasSnapshotFailure = !!results.snapshot.failure; - // Create a new testWatcher instance so that re-runs won't be blocked. - // The old instance that was passed to Jest will still be interrupted - // and prevent test runs from the previous run. - testWatcher = new TestWatcher({isWatchMode: true}); - if (shouldDisplayWatchUsage) { - pipe.write(usage(argv, hasSnapshotFailure)); - shouldDisplayWatchUsage = false; // hide Watch Usage after first run - isWatchUsageDisplayed = true; - } else { - pipe.write(showToggleUsagePrompt()); - shouldDisplayWatchUsage = false; - isWatchUsageDisplayed = false; - } + ); + }); + return runJest(contexts, argv, pipe, testWatcher, startRun, results => { + isRunning = false; + hasSnapshotFailure = !!results.snapshot.failure; + // Create a new testWatcher instance so that re-runs won't be blocked. + // The old instance that was passed to Jest will still be interrupted + // and prevent test runs from the previous run. + testWatcher = new TestWatcher({isWatchMode: true}); + if (shouldDisplayWatchUsage) { + pipe.write(usage(argv, hasSnapshotFailure)); + shouldDisplayWatchUsage = false; // hide Watch Usage after first run + isWatchUsageDisplayed = true; + } else { + pipe.write(showToggleUsagePrompt()); + shouldDisplayWatchUsage = false; + isWatchUsageDisplayed = false; + } - testNamePatternPrompt.updateCachedTestResults(results.testResults); - }, - ).then(() => {}, error => console.error(chalk.red(error.stack))); + testNamePatternPrompt.updateCachedTestResults(results.testResults); + }).catch(error => console.error(chalk.red(error.stack))); }; const onKeypress = (key: string) => { @@ -234,39 +241,6 @@ const watch = ( return Promise.resolve(); }; -const handleDeprecatedWarnings = ( - pipe: stream$Writable | tty$WriteStream, - stdin: stream$Readable | tty$ReadStream = process.stdin, -) => { - return new Promise((resolve, reject) => { - if (typeof stdin.setRawMode === 'function') { - const messages = [ - chalk.red('There are deprecation warnings.\n'), - chalk.dim(' \u203A Press ') + 'Enter' + chalk.dim(' to continue.'), - chalk.dim(' \u203A Press ') + 'Esc' + chalk.dim(' to exit.'), - ]; - - pipe.write(messages.join('\n')); - - // $FlowFixMe - stdin.setRawMode(true); - stdin.resume(); - stdin.setEncoding('hex'); - stdin.on('data', (key: string) => { - if (key === KEYS.ENTER) { - resolve(); - } else if ( - [KEYS.ESCAPE, KEYS.CONTROL_C, KEYS.CONTROL_D].indexOf(key) !== -1 - ) { - reject(); - } - }); - } else { - resolve(); - } - }); -}; - const usage = (argv, snapshotFailure, delimiter = '\n') => { /* eslint-disable max-len */ const messages = [ diff --git a/types/Reporters.js b/types/Reporters.js index 1523f0500853..d7a4c9866cb8 100644 --- a/types/Reporters.js +++ b/types/Reporters.js @@ -11,11 +11,6 @@ import type {FS} from 'jest-haste-map'; -export type RunnerContext = {| - hasteFS: FS, - getTestSummary: () => string, -|}; - export type ReporterOnStartOptions = {| estimatedTime: number, showStatus: boolean, diff --git a/types/TestRunner.js b/types/TestRunner.js index 04fbd9554dc3..de0da39dc812 100644 --- a/types/TestRunner.js +++ b/types/TestRunner.js @@ -12,10 +12,10 @@ import type {Context} from './Context'; import type {Path} from './Config'; -export type Test = { +export type Test = {| context: Context, path: Path, - duration?: number, -}; + duration: ?number, +|}; export type Tests = Array;