diff --git a/CHANGELOG.md b/CHANGELOG.md index cef9079de837..0a516378ae79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Fixes +- `[jest-cli]` Refactor `-o` and `--coverage` combined ([#7611](https://github.com/facebook/jest/pull/7611)) + ### Chore & Maintenance - `[*]`: Setup building, linting and testing of TypeScript ([#7808](https://github.com/facebook/jest/pull/7808)) diff --git a/e2e/__tests__/onlyChanged.test.js b/e2e/__tests__/onlyChanged.test.js index 82282142fb8a..1f351e28d81d 100644 --- a/e2e/__tests__/onlyChanged.test.js +++ b/e2e/__tests__/onlyChanged.test.js @@ -139,6 +139,30 @@ test('report test coverage for only changed files', () => { expect(stdout).not.toMatch('b.js'); }); +test('do not pickup non-tested files when reporting coverage on only changed files', () => { + writeFiles(DIR, { + 'a.js': 'module.exports = {}', + 'b.test.js': 'module.exports = {}', + 'package.json': JSON.stringify({name: 'original name'}), + }); + + run(`${GIT} init`, DIR); + run(`${GIT} add .`, DIR); + run(`${GIT} commit --no-gpg-sign -m "first"`, DIR); + + writeFiles(DIR, { + 'b.test.js': 'require("./package.json"); it("passes", () => {})', + 'package.json': JSON.stringify({name: 'new name'}), + }); + + const {stderr, stdout} = runJest(DIR, ['-o', '--coverage']); + + expect(stderr).toEqual( + expect.not.stringContaining('Failed to collect coverage from'), + ); + expect(stdout).toEqual(expect.not.stringContaining('package.json')); +}); + test('onlyChanged in config is overwritten by --all or testPathPattern', () => { writeFiles(DIR, { '.watchmanconfig': '', diff --git a/packages/jest-cli/src/SearchSource.js b/packages/jest-cli/src/SearchSource.js index 3d004d8ef140..52d83bdb3f5a 100644 --- a/packages/jest-cli/src/SearchSource.js +++ b/packages/jest-cli/src/SearchSource.js @@ -10,7 +10,7 @@ import type {Context} from 'types/Context'; import type {Glob, GlobalConfig, Path} from 'types/Config'; import type {Test} from 'types/TestRunner'; -import type {ChangedFilesPromise} from 'types/ChangedFiles'; +import type {ChangedFilesInfo} from 'types/ChangedFiles'; import path from 'path'; import micromatch from 'micromatch'; @@ -24,7 +24,7 @@ import {replacePathSepForGlob} from 'jest-util'; type SearchResult = {| noSCM?: boolean, stats?: {[key: string]: number}, - collectCoverageFrom?: Array, + collectCoverageFrom?: Set, tests: Array, total?: number, |}; @@ -158,29 +158,55 @@ export default class SearchSource { buildSnapshotResolver(this._context.config), ); - const tests = toTests( - this._context, - dependencyResolver.resolveInverse( - allPaths, - this.isTestFilePath.bind(this), - { - skipNodeResolution: this._context.config.skipNodeResolution, - }, - ), - ); - let collectCoverageFrom; - - // If we are collecting coverage, also return collectCoverageFrom patterns - if (collectCoverage) { - collectCoverageFrom = Array.from(allPaths).map(filename => { - filename = replaceRootDirInPath(this._context.config.rootDir, filename); - return path.isAbsolute(filename) - ? path.relative(this._context.config.rootDir, filename) - : filename; - }); + if (!collectCoverage) { + return { + tests: toTests( + this._context, + dependencyResolver.resolveInverse( + allPaths, + this.isTestFilePath.bind(this), + {skipNodeResolution: this._context.config.skipNodeResolution}, + ), + ), + }; } - return {collectCoverageFrom, tests}; + const testModulesMap = dependencyResolver.resolveInverseModuleMap( + allPaths, + this.isTestFilePath.bind(this), + {skipNodeResolution: this._context.config.skipNodeResolution}, + ); + + const allPathsAbsolute = Array.from(allPaths).map(p => path.resolve(p)); + + const collectCoverageFrom = new Set(); + + testModulesMap.forEach(testModule => { + if (!testModule.dependencies) { + return; + } + + testModule.dependencies + .filter(p => allPathsAbsolute.includes(p)) + .map(filename => { + filename = replaceRootDirInPath( + this._context.config.rootDir, + filename, + ); + return path.isAbsolute(filename) + ? path.relative(this._context.config.rootDir, filename) + : filename; + }) + .forEach(filename => collectCoverageFrom.add(filename)); + }); + + return { + collectCoverageFrom, + tests: toTests( + this._context, + testModulesMap.map(testModule => testModule.file), + ), + }; } findTestsByPaths(paths: Array): SearchResult { @@ -207,11 +233,11 @@ export default class SearchSource { return {tests: []}; } - async findTestRelatedToChangedFiles( - changedFilesPromise: ChangedFilesPromise, + findTestRelatedToChangedFiles( + changedFilesInfo: ChangedFilesInfo, collectCoverage: boolean, ) { - const {repos, changedFiles} = await changedFilesPromise; + const {repos, changedFiles} = changedFilesInfo; // no SCM (git/hg/...) is found in any of the roots. const noSCM = Object.keys(repos).every(scm => repos[scm].size === 0); return noSCM @@ -221,42 +247,38 @@ export default class SearchSource { _getTestPaths( globalConfig: GlobalConfig, - changedFilesPromise: ?ChangedFilesPromise, - ): Promise { + changedFiles: ?ChangedFilesInfo, + ): SearchResult { const paths = globalConfig.nonFlagArgs; if (globalConfig.onlyChanged) { - if (!changedFilesPromise) { - throw new Error('This promise must be present when running with -o.'); + if (!changedFiles) { + throw new Error('Changed files must be set when running with -o.'); } return this.findTestRelatedToChangedFiles( - changedFilesPromise, + changedFiles, globalConfig.collectCoverage, ); } else if (globalConfig.runTestsByPath && paths && paths.length) { - return Promise.resolve(this.findTestsByPaths(paths)); + return this.findTestsByPaths(paths); } else if (globalConfig.findRelatedTests && paths && paths.length) { - return Promise.resolve( - this.findRelatedTestsFromPattern(paths, globalConfig.collectCoverage), + return this.findRelatedTestsFromPattern( + paths, + globalConfig.collectCoverage, ); } else if (globalConfig.testPathPattern != null) { - return Promise.resolve( - this.findMatchingTests(globalConfig.testPathPattern), - ); + return this.findMatchingTests(globalConfig.testPathPattern); } else { - return Promise.resolve({tests: []}); + return {tests: []}; } } async getTestPaths( globalConfig: GlobalConfig, - changedFilesPromise: ?ChangedFilesPromise, + changedFiles: ?ChangedFilesInfo, ): Promise { - const searchResult = await this._getTestPaths( - globalConfig, - changedFilesPromise, - ); + const searchResult = this._getTestPaths(globalConfig, changedFiles); const filterPath = globalConfig.filter; diff --git a/packages/jest-cli/src/TestScheduler.js b/packages/jest-cli/src/TestScheduler.js index f183ab33b867..28511803998a 100644 --- a/packages/jest-cli/src/TestScheduler.js +++ b/packages/jest-cli/src/TestScheduler.js @@ -8,7 +8,7 @@ */ import type {AggregatedResult, TestResult} from 'types/TestResult'; -import type {GlobalConfig, ReporterConfig} from 'types/Config'; +import type {GlobalConfig, ReporterConfig, Path} from 'types/Config'; import type {Context} from 'types/Context'; import type {Reporter, Test} from 'types/TestRunner'; @@ -41,6 +41,7 @@ export type TestSchedulerOptions = {| export type TestSchedulerContext = {| firstRun: boolean, previousSuccess: boolean, + changedFiles?: Set, |}; export default class TestScheduler { _dispatcher: ReporterDispatcher; @@ -173,6 +174,7 @@ export default class TestScheduler { // $FlowFixMe testRunners[config.runner] = new (require(config.runner): TestRunner)( this._globalConfig, + {changedFiles: this._context && this._context.changedFiles}, ); } }); @@ -262,7 +264,11 @@ export default class TestScheduler { } if (!isDefault && collectCoverage) { - this.addReporter(new CoverageReporter(this._globalConfig)); + this.addReporter( + new CoverageReporter(this._globalConfig, { + changedFiles: this._context && this._context.changedFiles, + }), + ); } if (notify) { @@ -288,7 +294,11 @@ export default class TestScheduler { ); if (collectCoverage) { - this.addReporter(new CoverageReporter(this._globalConfig)); + this.addReporter( + new CoverageReporter(this._globalConfig, { + changedFiles: this._context && this._context.changedFiles, + }), + ); } this.addReporter(new SummaryReporter(this._globalConfig)); diff --git a/packages/jest-cli/src/__tests__/SearchSource.test.js b/packages/jest-cli/src/__tests__/SearchSource.test.js index 0a5ddef370dc..0f010a136e3d 100644 --- a/packages/jest-cli/src/__tests__/SearchSource.test.js +++ b/packages/jest-cli/src/__tests__/SearchSource.test.js @@ -419,6 +419,20 @@ describe('SearchSource', () => { rootPath, ]); }); + + it('excludes untested files from coverage', () => { + const unrelatedFile = path.join(rootDir, 'JSONFile.json'); + const regular = path.join(rootDir, 'RegularModule.js'); + const requireRegular = path.join(rootDir, 'RequireRegularMode.js'); + + const data = searchSource.findRelatedTests( + new Set([regular, requireRegular, unrelatedFile]), + true, + ); + expect(Array.from(data.collectCoverageFrom)).toEqual([ + 'RegularModule.js', + ]); + }); }); describe('findRelatedTestsFromPattern', () => { diff --git a/packages/jest-cli/src/__tests__/runJestWithCoverage.test.js b/packages/jest-cli/src/__tests__/runJestWithCoverage.test.js deleted file mode 100644 index e0ceb5266755..000000000000 --- a/packages/jest-cli/src/__tests__/runJestWithCoverage.test.js +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - -import runJest from '../runJest'; - -jest.mock('jest-util', () => { - const util = jest.requireActual('jest-util'); - return { - ...jest.genMockFromModule('jest-util'), - replacePathSepForGlob: util.replacePathSepForGlob, - }; -}); - -jest.mock( - '../TestScheduler', - () => - class { - constructor(globalConfig) { - this._globalConfig = globalConfig; - } - - scheduleTests() { - return {_globalConfig: this._globalConfig}; - } - }, -); - -jest.mock( - '../TestSequencer', - () => - class { - sort(allTests) { - return allTests; - } - cacheResults() {} - }, -); - -jest.mock( - '../SearchSource', - () => - class { - constructor(context) { - this._context = context; - } - - async getTestPaths(globalConfig, changedFilesPromise) { - const {files} = await changedFilesPromise; - const paths = files.filter(path => path.match(/__tests__/)); - - return { - collectCoverageFrom: files.filter(path => !path.match(/__tests__/)), - tests: paths.map(path => ({ - context: this._context, - duration: null, - path, - })), - }; - } - }, -); - -const config = {roots: [], testPathIgnorePatterns: [], testRegex: []}; -let globalConfig; -const defaults = { - changedFilesPromise: Promise.resolve({ - files: ['foo.js', '__tests__/foo-test.js', 'dont/cover.js'], - }), - contexts: [{config}], - onComplete: runResults => (globalConfig = runResults._globalConfig), - outputStream: {}, - startRun: {}, - testWatcher: {isInterrupted: () => false}, -}; - -describe('collectCoverageFrom patterns', () => { - it('should apply collectCoverageFrom patterns coming from SearchSource', async () => { - expect.assertions(1); - - await runJest({ - ...defaults, - globalConfig: { - rootDir: '', - }, - }); - expect(globalConfig.collectCoverageFrom).toEqual([ - 'foo.js', - 'dont/cover.js', - ]); - }); - - it('excludes coverage from files outside the global collectCoverageFrom config', async () => { - expect.assertions(1); - - await runJest({ - ...defaults, - globalConfig: { - collectCoverageFrom: ['**/dont/*.js'], - rootDir: '', - }, - }); - expect(globalConfig.collectCoverageFrom).toEqual(['dont/cover.js']); - }); - - it('respects coveragePathIgnorePatterns', async () => { - expect.assertions(1); - - await runJest({ - ...defaults, - globalConfig: { - collectCoverageFrom: ['**/*.js'], - coveragePathIgnorePatterns: ['dont'], - rootDir: '', - }, - }); - expect(globalConfig.collectCoverageFrom).toEqual(['foo.js']); - }); -}); diff --git a/packages/jest-cli/src/generateEmptyCoverage.js b/packages/jest-cli/src/generateEmptyCoverage.js index 65c29fcfb1c9..d43f2de65dab 100644 --- a/packages/jest-cli/src/generateEmptyCoverage.js +++ b/packages/jest-cli/src/generateEmptyCoverage.js @@ -25,8 +25,10 @@ export default function( filename: Path, globalConfig: GlobalConfig, config: ProjectConfig, + changedFiles: ?Set, ): ?CoverageWorkerResult { const coverageOptions = { + changedFiles, collectCoverage: globalConfig.collectCoverage, collectCoverageFrom: globalConfig.collectCoverageFrom, collectCoverageOnlyFrom: globalConfig.collectCoverageOnlyFrom, diff --git a/packages/jest-cli/src/reporters/__tests__/coverage_worker.test.js b/packages/jest-cli/src/reporters/__tests__/coverage_worker.test.js index 8f0fe1c0e7d7..fe9d1f1bb5db 100644 --- a/packages/jest-cli/src/reporters/__tests__/coverage_worker.test.js +++ b/packages/jest-cli/src/reporters/__tests__/coverage_worker.test.js @@ -40,6 +40,7 @@ test('resolves to the result of generateEmptyCoverage upon success', async () => 'banana.js', globalConfig, config, + undefined, ); expect(result).toEqual(42); diff --git a/packages/jest-cli/src/reporters/coverage_reporter.js b/packages/jest-cli/src/reporters/coverage_reporter.js index 81e45ec08a1e..f2fad46f9d27 100644 --- a/packages/jest-cli/src/reporters/coverage_reporter.js +++ b/packages/jest-cli/src/reporters/coverage_reporter.js @@ -16,7 +16,7 @@ import type { } from 'types/TestResult'; import typeof {worker} from './coverage_worker'; -import type {GlobalConfig} from 'types/Config'; +import type {GlobalConfig, Path} from 'types/Config'; import type {Context} from 'types/Context'; import type {Test} from 'types/TestRunner'; @@ -35,16 +35,22 @@ const RUNNING_TEST_COLOR = chalk.bold.dim; type CoverageWorker = {worker: worker}; +export type CoverageReporterOptions = { + changedFiles?: Set, +}; + export default class CoverageReporter extends BaseReporter { _coverageMap: CoverageMap; _globalConfig: GlobalConfig; _sourceMapStore: any; + _options: CoverageReporterOptions; - constructor(globalConfig: GlobalConfig) { + constructor(globalConfig: GlobalConfig, options?: CoverageReporterOptions) { super(); this._coverageMap = istanbulCoverage.createCoverageMap({}); this._globalConfig = globalConfig; this._sourceMapStore = libSourceMaps.createSourceMapStore(); + this._options = options || {}; } onTestResult( @@ -170,6 +176,7 @@ export default class CoverageReporter extends BaseReporter { const result = await worker.worker({ config, globalConfig, + options: this._options, path: filename, }); diff --git a/packages/jest-cli/src/reporters/coverage_worker.js b/packages/jest-cli/src/reporters/coverage_worker.js index c3d53016f248..6a7feded80e1 100644 --- a/packages/jest-cli/src/reporters/coverage_worker.js +++ b/packages/jest-cli/src/reporters/coverage_worker.js @@ -8,6 +8,7 @@ */ import type {GlobalConfig, ProjectConfig, Path} from 'types/Config'; +import type {CoverageReporterOptions} from './coverage_reporter'; import exit from 'exit'; import fs from 'fs'; @@ -18,6 +19,7 @@ export type CoverageWorkerData = {| globalConfig: GlobalConfig, config: ProjectConfig, path: Path, + options?: CoverageReporterOptions, |}; export type {CoverageWorkerResult}; @@ -32,11 +34,13 @@ export function worker({ config, globalConfig, path, + options, }: CoverageWorkerData): ?CoverageWorkerResult { return generateEmptyCoverage( fs.readFileSync(path, 'utf8'), path, globalConfig, config, + options && options.changedFiles, ); } diff --git a/packages/jest-cli/src/runJest.js b/packages/jest-cli/src/runJest.js index 7b4894e6ac0a..a403ac57ca8a 100644 --- a/packages/jest-cli/src/runJest.js +++ b/packages/jest-cli/src/runJest.js @@ -14,12 +14,12 @@ import type {AggregatedResult} from 'types/TestResult'; import type {TestRunData} from 'types/TestRunner'; import type {JestHookEmitter} from 'types/JestHooks'; import type TestWatcher from './TestWatcher'; +import type {TestSchedulerContext} from './TestScheduler'; -import micromatch from 'micromatch'; import chalk from 'chalk'; import path from 'path'; import {sync as realpath} from 'realpath-native'; -import {Console, formatTestResults, replacePathSepForGlob} from 'jest-util'; +import {Console, formatTestResults} from 'jest-util'; import exit from 'exit'; import fs from 'graceful-fs'; import getNoTestsFoundMessage from './getNoTestsFoundMessage'; @@ -36,11 +36,11 @@ const getTestPaths = async ( globalConfig, context, outputStream, - changedFilesPromise, + changedFiles, jestHooks, ) => { const source = new SearchSource(context); - const data = await source.getTestPaths(globalConfig, changedFilesPromise); + const data = await source.getTestPaths(globalConfig, changedFiles); if (!data.tests.length && globalConfig.onlyChanged && data.noSCM) { new Console(outputStream, outputStream).log( @@ -104,7 +104,7 @@ const processResults = (runResults, options) => { return onComplete && onComplete(runResults); }; -const testSchedulerContext = { +const testSchedulerContext: TestSchedulerContext = { firstRun: true, previousSuccess: true, }; @@ -135,6 +135,7 @@ export default (async function runJest({ if (changedFilesPromise && globalConfig.watch) { const {repos} = await changedFilesPromise; + const noSCM = Object.keys(repos).every(scm => repos[scm].size === 0); if (noSCM) { process.stderr.write( @@ -147,57 +148,21 @@ export default (async function runJest({ } } - let collectCoverageFrom = []; - const testRunData: TestRunData = await Promise.all( contexts.map(async context => { const matches = await getTestPaths( globalConfig, context, outputStream, - changedFilesPromise, + changedFilesPromise && (await changedFilesPromise), jestHooks, ); allTests = allTests.concat(matches.tests); - if (matches.collectCoverageFrom) { - collectCoverageFrom = collectCoverageFrom.concat( - matches.collectCoverageFrom.filter(filename => { - if ( - globalConfig.collectCoverageFrom && - !micromatch.some( - replacePathSepForGlob( - path.relative(globalConfig.rootDir, filename), - ), - globalConfig.collectCoverageFrom, - ) - ) { - return false; - } - - if ( - globalConfig.coveragePathIgnorePatterns && - globalConfig.coveragePathIgnorePatterns.some(pattern => - filename.match(pattern), - ) - ) { - return false; - } - - return true; - }), - ); - } - return {context, matches}; }), ); - if (collectCoverageFrom.length) { - const newConfig: GlobalConfig = {...globalConfig, collectCoverageFrom}; - globalConfig = Object.freeze(newConfig); - } - allTests = sequencer.sort(allTests); if (globalConfig.listTests) { @@ -256,6 +221,10 @@ export default (async function runJest({ await runGlobalHook({allTests, globalConfig, moduleName: 'globalSetup'}); } + if (changedFilesPromise) { + testSchedulerContext.changedFiles = (await changedFilesPromise).changedFiles; + } + const results = await new TestScheduler( globalConfig, { diff --git a/packages/jest-resolve-dependencies/src/index.js b/packages/jest-resolve-dependencies/src/index.js index ff1dce7bc612..bebb7a76e212 100644 --- a/packages/jest-resolve-dependencies/src/index.js +++ b/packages/jest-resolve-dependencies/src/index.js @@ -9,7 +9,11 @@ import type {HasteFS} from 'types/HasteMap'; import type {Path} from 'types/Config'; -import type {Resolver, ResolveModuleConfig} from 'types/Resolve'; +import type { + Resolver, + ResolveModuleConfig, + ResolvedModule, +} from 'types/Resolve'; import type {SnapshotResolver} from 'types/SnapshotResolver'; import {isSnapshotPath} from 'jest-snapshot'; @@ -61,17 +65,18 @@ class DependencyResolver { }, []); } - resolveInverse( + resolveInverseModuleMap( paths: Set, filter: (file: Path) => boolean, options?: ResolveModuleConfig, - ): Array { + ): Array { if (!paths.size) { return []; } - const collectModules = (relatedPaths, moduleMap, changed) => { + const collectModules = (related, moduleMap, changed) => { const visitedModules = new Set(); + const result: Array = []; while (changed.size) { changed = new Set( moduleMap.reduce((acc, module) => { @@ -84,7 +89,8 @@ class DependencyResolver { const file = module.file; if (filter(file)) { - relatedPaths.add(file); + result.push(module); + related.delete(file); } visitedModules.add(file); acc.push(module.file); @@ -92,10 +98,10 @@ class DependencyResolver { }, []), ); } - return relatedPaths; + return result.concat(Array.from(related).map(file => ({file}))); }; - const relatedPaths = new Set(); + const relatedPaths = new Set(); const changed = new Set(); for (const path of paths) { if (this._hasteFS.exists(path)) { @@ -115,7 +121,17 @@ class DependencyResolver { file, }); } - return Array.from(collectModules(relatedPaths, modules, changed)); + return collectModules(relatedPaths, modules, changed); + } + + resolveInverse( + paths: Set, + filter: (file: Path) => boolean, + options?: ResolveModuleConfig, + ): Array { + return this.resolveInverseModuleMap(paths, filter, options).map( + module => module.file, + ); } } diff --git a/packages/jest-runner/src/__tests__/testRunner.test.js b/packages/jest-runner/src/__tests__/testRunner.test.js index d375d91a43a3..c4ce341398cd 100644 --- a/packages/jest-runner/src/__tests__/testRunner.test.js +++ b/packages/jest-runner/src/__tests__/testRunner.test.js @@ -31,6 +31,7 @@ test('injects the serializable module map into each worker in watch mode', () => const globalConfig = {maxWorkers: 2, watch: true}; const config = {rootDir: '/path/'}; const serializableModuleMap = jest.fn(); + const runContext = {}; const context = { config, moduleMap: {toJSON: () => serializableModuleMap}, @@ -46,10 +47,19 @@ test('injects the serializable module map into each worker in watch mode', () => ) .then(() => { expect(mockWorkerFarm.worker.mock.calls).toEqual([ - [{config, globalConfig, path: './file.test.js', serializableModuleMap}], [ { config, + context: runContext, + globalConfig, + path: './file.test.js', + serializableModuleMap, + }, + ], + [ + { + config, + context: runContext, globalConfig, path: './file2.test.js', serializableModuleMap, @@ -63,8 +73,9 @@ test('does not inject the serializable module map in serial mode', () => { const globalConfig = {maxWorkers: 1, watch: false}; const config = {rootDir: '/path/'}; const context = {config}; + const runContext = {}; - return new TestRunner(globalConfig) + return new TestRunner(globalConfig, runContext) .runTests( [{context, path: './file.test.js'}, {context, path: './file2.test.js'}], new TestWatcher({isWatchMode: globalConfig.watch}), @@ -78,6 +89,7 @@ test('does not inject the serializable module map in serial mode', () => { [ { config, + context: runContext, globalConfig, path: './file.test.js', serializableModuleMap: null, @@ -86,6 +98,7 @@ test('does not inject the serializable module map in serial mode', () => { [ { config, + context: runContext, globalConfig, path: './file2.test.js', serializableModuleMap: null, diff --git a/packages/jest-runner/src/index.js b/packages/jest-runner/src/index.js index ca4ccc141895..cd339cd37e5c 100644 --- a/packages/jest-runner/src/index.js +++ b/packages/jest-runner/src/index.js @@ -13,6 +13,7 @@ import type { OnTestStart, OnTestSuccess, Test, + TestRunnerContext, TestRunnerOptions, TestWatcher, } from 'types/TestRunner'; @@ -30,9 +31,11 @@ type WorkerInterface = Worker & {worker: worker}; class TestRunner { _globalConfig: GlobalConfig; + _context: TestRunnerContext; - constructor(globalConfig: GlobalConfig) { + constructor(globalConfig: GlobalConfig, context?: TestRunnerContext) { this._globalConfig = globalConfig; + this._context = context || {}; } async runTests( @@ -78,6 +81,7 @@ class TestRunner { this._globalConfig, test.context.config, test.context.resolver, + this._context, ); }) .then(result => onResult(test, result)) @@ -119,6 +123,7 @@ class TestRunner { return worker.worker({ config: test.context.config, + context: this._context, globalConfig: this._globalConfig, path: test.path, serializableModuleMap: watcher.isWatchMode() diff --git a/packages/jest-runner/src/runTest.js b/packages/jest-runner/src/runTest.js index 2ff916f45abd..f609e09d4c8d 100644 --- a/packages/jest-runner/src/runTest.js +++ b/packages/jest-runner/src/runTest.js @@ -10,7 +10,7 @@ import type {EnvironmentClass} from 'types/Environment'; import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; import type {Resolver} from 'types/Resolve'; -import type {TestFramework} from 'types/TestRunner'; +import type {TestFramework, TestRunnerContext} from 'types/TestRunner'; import type {TestResult} from 'types/TestResult'; import type RuntimeClass from 'jest-runtime'; @@ -78,6 +78,7 @@ async function runTestInternal( globalConfig: GlobalConfig, config: ProjectConfig, resolver: Resolver, + context: ?TestRunnerContext, ): Promise { const testSource = fs.readFileSync(path, 'utf8'); const parsedDocblock = docblock.parse(docblock.extract(testSource)); @@ -143,6 +144,7 @@ async function runTestInternal( setGlobal(environment.global, 'console', testConsole); runtime = new Runtime(config, environment, resolver, cacheFS, { + changedFiles: context && context.changedFiles, collectCoverage: globalConfig.collectCoverage, collectCoverageFrom: globalConfig.collectCoverageFrom, collectCoverageOnlyFrom: globalConfig.collectCoverageOnlyFrom, @@ -268,12 +270,14 @@ export default async function runTest( globalConfig: GlobalConfig, config: ProjectConfig, resolver: Resolver, + context: ?TestRunnerContext, ): Promise { const {leakDetector, result} = await runTestInternal( path, globalConfig, config, resolver, + context, ); if (leakDetector) { diff --git a/packages/jest-runner/src/testWorker.js b/packages/jest-runner/src/testWorker.js index c16f2f749a37..1651803861e6 100644 --- a/packages/jest-runner/src/testWorker.js +++ b/packages/jest-runner/src/testWorker.js @@ -11,6 +11,7 @@ import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; import type {SerializableError, TestResult} from 'types/TestResult'; import type {SerializableModuleMap} from 'types/HasteMap'; import type {ErrorWithCode} from 'types/Errors'; +import type {TestRunnerContext} from 'types/TestRunner'; import exit from 'exit'; import HasteMap from 'jest-haste-map'; @@ -23,6 +24,7 @@ export type WorkerData = {| globalConfig: GlobalConfig, path: Path, serializableModuleMap: ?SerializableModuleMap, + context?: TestRunnerContext, |}; // Make sure uncaught errors are logged before we exit. @@ -74,6 +76,7 @@ export async function worker({ globalConfig, path, serializableModuleMap, + context, }: WorkerData): Promise { try { const moduleMap = serializableModuleMap @@ -84,6 +87,7 @@ export async function worker({ globalConfig, config, getResolver(config, moduleMap), + context, ); } catch (error) { throw formatError(error); diff --git a/packages/jest-runtime/src/ScriptTransformer.js b/packages/jest-runtime/src/ScriptTransformer.js index 92d0cb46714b..72ba88f9fabd 100644 --- a/packages/jest-runtime/src/ScriptTransformer.js +++ b/packages/jest-runtime/src/ScriptTransformer.js @@ -33,6 +33,7 @@ import {sync as realpath} from 'realpath-native'; import {enhanceUnexpectedTokenMessage} from './helpers'; export type Options = {| + changedFiles: ?Set, collectCoverage: boolean, collectCoverageFrom: Array, collectCoverageOnlyFrom: ?{[key: string]: boolean, __proto__: null}, diff --git a/packages/jest-runtime/src/index.js b/packages/jest-runtime/src/index.js index 7ff87fbf2827..2f7290c73711 100644 --- a/packages/jest-runtime/src/index.js +++ b/packages/jest-runtime/src/index.js @@ -56,6 +56,7 @@ type InternalModuleOptions = {| |}; type CoverageOptions = { + changedFiles: ?Set, collectCoverage: boolean, collectCoverageFrom: Array, collectCoverageOnlyFrom: ?{[key: string]: boolean, __proto__: null}, @@ -122,6 +123,7 @@ class Runtime { this._cacheFS = cacheFS || Object.create(null); this._config = config; this._coverageOptions = coverageOptions || { + changedFiles: null, collectCoverage: false, collectCoverageFrom: [], collectCoverageOnlyFrom: null, @@ -185,6 +187,7 @@ class Runtime { return shouldInstrument( filename, { + changedFiles: options.changedFiles, collectCoverage: options.collectCoverage, collectCoverageFrom: options.collectCoverageFrom, collectCoverageOnlyFrom: options.collectCoverageOnlyFrom, @@ -671,6 +674,7 @@ class Runtime { const transformedFile = this._scriptTransformer.transform( filename, { + changedFiles: this._coverageOptions.changedFiles, collectCoverage: this._coverageOptions.collectCoverage, collectCoverageFrom: this._coverageOptions.collectCoverageFrom, collectCoverageOnlyFrom: this._coverageOptions.collectCoverageOnlyFrom, diff --git a/packages/jest-runtime/src/shouldInstrument.js b/packages/jest-runtime/src/shouldInstrument.js index 6333ece7814e..ecfe24fb14f1 100644 --- a/packages/jest-runtime/src/shouldInstrument.js +++ b/packages/jest-runtime/src/shouldInstrument.js @@ -112,5 +112,9 @@ export default function shouldInstrument( return false; } + if (options.changedFiles && !options.changedFiles.has(filename)) { + return false; + } + return true; } diff --git a/types/ChangedFiles.js b/types/ChangedFiles.js index 53b116b8783a..9e0914c01016 100644 --- a/types/ChangedFiles.js +++ b/types/ChangedFiles.js @@ -18,10 +18,11 @@ export type Options = {| export type ChangedFiles = Set; export type Repos = {|git: Set, hg: Set|}; -export type ChangedFilesPromise = Promise<{| +export type ChangedFilesInfo = {| repos: Repos, changedFiles: ChangedFiles, -|}>; +|}; +export type ChangedFilesPromise = Promise; export type SCMAdapter = {| findChangedFiles: (cwd: Path, options: Options) => Promise>, diff --git a/types/Resolve.js b/types/Resolve.js index 1d849797cbc4..facd55e4fc56 100644 --- a/types/Resolve.js +++ b/types/Resolve.js @@ -16,4 +16,9 @@ export type ResolveModuleConfig = {| paths?: Path[], |}; +export type ResolvedModule = { + file: string, + dependencies?: string[], +}; + export type Resolver = _Resolver; diff --git a/types/TestRunner.js b/types/TestRunner.js index 2eac8afcd349..2aa7005c6f26 100644 --- a/types/TestRunner.js +++ b/types/TestRunner.js @@ -61,6 +61,10 @@ export type TestRunnerOptions = { serial: boolean, }; +export type TestRunnerContext = { + changedFiles?: Set, +}; + export type TestRunData = Array<{ context: Context, matches: {allTests: number, tests: Array, total: number},