From 835df3644d89f0d5484fc996dbd34b2a4c6e76ba Mon Sep 17 00:00:00 2001 From: Justin Bay Date: Sat, 10 Sep 2016 18:28:01 -0400 Subject: [PATCH 1/8] source map support for coverage --- .../coverage-remapping-test.js.snap | 346 ++++++++++++++++++ .../__tests__/coverage-remapping-test.js | 37 ++ .../__tests__/covered-test.ts | 6 + .../coverage-remapping/covered.ts | 12 + .../coverage-remapping/package.json | 18 + .../typescript-preprocessor.js | 25 ++ packages/jest-cli/package.json | 1 + .../src/reporters/CoverageReporter.js | 19 +- .../__tests__/CoverageReporter-test.js | 11 + packages/jest-config/src/defaults.js | 1 + packages/jest-config/src/normalize.js | 1 + packages/jest-config/src/validConfig.js | 1 + packages/jest-runtime/package.json | 3 +- .../__snapshots__/transform-test.js.snap | 10 + .../src/__tests__/transform-test.js | 41 +++ packages/jest-runtime/src/transform.js | 45 ++- scripts/build.js | 5 +- types/Config.js | 3 + types/TestResult.js | 36 +- types/Transform.js | 17 +- 20 files changed, 607 insertions(+), 31 deletions(-) create mode 100644 integration_tests/__tests__/__snapshots__/coverage-remapping-test.js.snap create mode 100644 integration_tests/__tests__/coverage-remapping-test.js create mode 100644 integration_tests/coverage-remapping/__tests__/covered-test.ts create mode 100644 integration_tests/coverage-remapping/covered.ts create mode 100644 integration_tests/coverage-remapping/package.json create mode 100644 integration_tests/coverage-remapping/typescript-preprocessor.js diff --git a/integration_tests/__tests__/__snapshots__/coverage-remapping-test.js.snap b/integration_tests/__tests__/__snapshots__/coverage-remapping-test.js.snap new file mode 100644 index 000000000000..9d2d3815470e --- /dev/null +++ b/integration_tests/__tests__/__snapshots__/coverage-remapping-test.js.snap @@ -0,0 +1,346 @@ +exports[`test maps code coverage against original source 1`] = ` +Object { + "covered.ts": Object { + "b": Object { + "0": Array [ + 1, + 0, + ], + "1": Array [ + 1, + 0, + ], + "2": Array [ + 1, + 0, + 0, + ], + "3": Array [ + 1, + 0, + ], + }, + "branchMap": Object { + "0": Object { + "loc": Object { + "end": Object { + "column": 9, + "line": 5, + }, + "start": Object { + "column": 8, + "line": 5, + }, + }, + "locations": Array [ + Object { + "end": Object { + "column": 9, + "line": 5, + }, + "start": Object { + "column": 8, + "line": 5, + }, + }, + Object { + "end": Object { + "column": 9, + "line": 6, + }, + "start": Object { + "column": 8, + "line": 6, + }, + }, + ], + "type": "cond-expr", + }, + "1": Object { + "loc": Object { + "end": Object { + "column": 37, + "line": 7, + }, + "start": Object { + "column": 36, + "line": 7, + }, + }, + "locations": Array [ + Object { + "end": Object { + "column": 37, + "line": 7, + }, + "start": Object { + "column": 36, + "line": 7, + }, + }, + Object { + "end": Object { + "column": 41, + "line": 7, + }, + "start": Object { + "column": 40, + "line": 7, + }, + }, + ], + "type": "cond-expr", + }, + "2": Object { + "loc": Object { + "end": Object { + "column": 33, + "line": 8, + }, + "start": Object { + "column": 29, + "line": 8, + }, + }, + "locations": Array [ + Object { + "end": Object { + "column": 33, + "line": 8, + }, + "start": Object { + "column": 29, + "line": 8, + }, + }, + Object { + "end": Object { + "column": 41, + "line": 8, + }, + "start": Object { + "column": 37, + "line": 8, + }, + }, + Object { + "end": Object { + "column": 50, + "line": 8, + }, + "start": Object { + "column": 45, + "line": 8, + }, + }, + ], + "type": "binary-expr", + }, + "3": Object { + "loc": Object { + "end": Object { + "column": 42, + "line": 9, + }, + "start": Object { + "column": 32, + "line": 9, + }, + }, + "locations": Array [ + Object { + "end": Object { + "column": 42, + "line": 9, + }, + "start": Object { + "column": 32, + "line": 9, + }, + }, + Object { + "end": Object { + "column": 55, + "line": 9, + }, + "start": Object { + "column": 45, + "line": 9, + }, + }, + ], + "type": "cond-expr", + }, + }, + "f": Object { + "0": 1, + "1": 0, + "2": 0, + }, + "fnMap": Object { + "0": Object { + "decl": Object { + "end": Object { + "column": 28, + "line": 3, + }, + "start": Object { + "column": 9, + "line": 3, + }, + }, + "loc": Object { + "end": Object { + "column": 1, + "line": 12, + }, + "start": Object { + "column": 49, + "line": 3, + }, + }, + "name": "difference", + }, + "1": Object { + "decl": Object { + "end": Object { + "column": 37, + "line": 9, + }, + "start": Object { + "column": 32, + "line": 9, + }, + }, + "loc": Object { + "end": Object { + "column": 42, + "line": 9, + }, + "start": Object { + "column": 32, + "line": 9, + }, + }, + "name": "(anonymous_1)", + }, + "2": Object { + "decl": Object { + "end": Object { + "column": 50, + "line": 9, + }, + "start": Object { + "column": 45, + "line": 9, + }, + }, + "loc": Object { + "end": Object { + "column": 55, + "line": 9, + }, + "start": Object { + "column": 45, + "line": 9, + }, + }, + "name": "(anonymous_2)", + }, + }, + "path": "covered.ts", + "s": Object { + "0": 1, + "1": 1, + "2": 1, + "3": 1, + "4": 1, + "5": 0, + "6": 0, + "7": 1, + }, + "statementMap": Object { + "0": Object { + "end": Object { + "column": 1, + "line": 12, + }, + "start": Object { + "column": 0, + "line": 3, + }, + }, + "1": Object { + "end": Object { + "column": 9, + "line": 6, + }, + "start": Object { + "column": 29, + "line": 4, + }, + }, + "2": Object { + "end": Object { + "column": 41, + "line": 7, + }, + "start": Object { + "column": 29, + "line": 7, + }, + }, + "3": Object { + "end": Object { + "column": 50, + "line": 8, + }, + "start": Object { + "column": 29, + "line": 8, + }, + }, + "4": Object { + "end": Object { + "column": 55, + "line": 9, + }, + "start": Object { + "column": 25, + "line": 9, + }, + }, + "5": Object { + "end": Object { + "column": 42, + "line": 9, + }, + "start": Object { + "column": 38, + "line": 9, + }, + }, + "6": Object { + "end": Object { + "column": 55, + "line": 9, + }, + "start": Object { + "column": 51, + "line": 9, + }, + }, + "7": Object { + "end": Object { + "column": 17, + "line": 11, + }, + "start": Object { + "column": 4, + "line": 11, + }, + }, + }, + }, +} +`; diff --git a/integration_tests/__tests__/coverage-remapping-test.js b/integration_tests/__tests__/coverage-remapping-test.js new file mode 100644 index 000000000000..a4c912f989bb --- /dev/null +++ b/integration_tests/__tests__/coverage-remapping-test.js @@ -0,0 +1,37 @@ +/** + * 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. + */ + +'use strict'; + +const {readFileSync} = require('fs'); +const {run} = require('../utils'); +const path = require('path'); +const runJest = require('../runJest'); +const skipOnWindows = require('skipOnWindows'); + +skipOnWindows.suite(); + +it('maps code coverage against original source', () => { + const dir = path.resolve(__dirname, '../coverage-remapping'); + run('npm install', dir); + runJest(dir, ['--coverage', '--no-cache']); + + const coverageMapFile = path.join( + __dirname, + '../coverage-remapping/coverage/coverage-final.json' + ); + const coverageMap = JSON.parse(readFileSync(coverageMapFile, 'utf-8')); + + // reduce absolute paths embedded in the coverage map to just filenames + Object.keys(coverageMap).forEach(filename => { + coverageMap[filename].path = path.basename(coverageMap[filename].path); + coverageMap[path.basename(filename)] = coverageMap[filename]; + delete coverageMap[filename]; + }); + expect(coverageMap).toMatchSnapshot(); +}); diff --git a/integration_tests/coverage-remapping/__tests__/covered-test.ts b/integration_tests/coverage-remapping/__tests__/covered-test.ts new file mode 100644 index 000000000000..a9f2f7c525ef --- /dev/null +++ b/integration_tests/coverage-remapping/__tests__/covered-test.ts @@ -0,0 +1,6 @@ +// Copyright 2004-present Facebook. All Rights Reserved. +const difference = require('../covered.ts'); + +it('subtracts correctly', () => { + expect(difference(3, 2)).toBe(1); +}); diff --git a/integration_tests/coverage-remapping/covered.ts b/integration_tests/coverage-remapping/covered.ts new file mode 100644 index 000000000000..df5496159f87 --- /dev/null +++ b/integration_tests/coverage-remapping/covered.ts @@ -0,0 +1,12 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +export = function difference(a: number, b: number): number { + const branch1: boolean = true + ? 1 + : 0; + const branch2: boolean = true ? 1 : 0; + const branch3: boolean = true || true || false; + const fn: Function = true ? () => null : () => null; + + return a - b; +} diff --git a/integration_tests/coverage-remapping/package.json b/integration_tests/coverage-remapping/package.json new file mode 100644 index 000000000000..ee9d5db999dc --- /dev/null +++ b/integration_tests/coverage-remapping/package.json @@ -0,0 +1,18 @@ +{ + "jest": { + "rootDir": "./", + "transform": { + "^.+\\.(ts|js)$": "/typescript-preprocessor.js" + }, + "testRegex": "/__tests__/.*\\.(ts|tsx|js)$", + "testEnvironment": "node", + "moduleFileExtensions": [ + "ts", + "tsx", + "js" + ] + }, + "dependencies": { + "typescript": "^1.8.10" + } +} diff --git a/integration_tests/coverage-remapping/typescript-preprocessor.js b/integration_tests/coverage-remapping/typescript-preprocessor.js new file mode 100644 index 000000000000..2cdebdc9b88d --- /dev/null +++ b/integration_tests/coverage-remapping/typescript-preprocessor.js @@ -0,0 +1,25 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +const tsc = require('typescript'); + +module.exports = { + process(src, path) { + if (path.endsWith('.ts') || path.endsWith('.tsx')) { + const result = tsc.transpileModule( + src, + { + compilerOptions: { + module: tsc.ModuleKind.CommonJS, + sourceMap: true, + }, + fileName: path, + } + ); + return { + content: result.outputText, + sourceMap: JSON.parse(result.sourceMapText), + }; + } + return src; + }, +}; diff --git a/packages/jest-cli/package.json b/packages/jest-cli/package.json index 8c0d4f094a13..cd2532165212 100644 --- a/packages/jest-cli/package.json +++ b/packages/jest-cli/package.json @@ -12,6 +12,7 @@ "istanbul-api": "^1.1.0-alpha.1", "istanbul-lib-coverage": "^1.0.0", "istanbul-lib-instrument": "^1.1.1", + "istanbul-lib-source-maps": "^1.1.0", "jest-changed-files": "^17.0.2", "jest-config": "^18.1.0", "jest-environment-jsdom": "^18.1.0", diff --git a/packages/jest-cli/src/reporters/CoverageReporter.js b/packages/jest-cli/src/reporters/CoverageReporter.js index b1487b3dc4da..5ad4e12f39e7 100644 --- a/packages/jest-cli/src/reporters/CoverageReporter.js +++ b/packages/jest-cli/src/reporters/CoverageReporter.js @@ -9,7 +9,7 @@ */ 'use strict'; -import type {AggregatedResult, CoverageMap, TestResult} from 'types/TestResult'; +import type {AggregatedResult, CoverageMap, SourceMapStore, TestResult} from 'types/TestResult'; import type {Config} from 'types/Config'; import type {RunnerContext} from 'types/Reporters'; @@ -22,6 +22,7 @@ const fs = require('fs'); const generateEmptyCoverage = require('../generateEmptyCoverage'); const isCI = require('is-ci'); const istanbulCoverage = require('istanbul-lib-coverage'); +const libSourceMaps = require('istanbul-lib-source-maps'); const FAIL_COLOR = chalk.bold.red; const RUNNING_TEST_COLOR = chalk.bold.dim; @@ -30,10 +31,12 @@ const isInteractive = process.stdout.isTTY && !isCI; class CoverageReporter extends BaseReporter { _coverageMap: CoverageMap; + _sourceMapStore: SourceMapStore; constructor() { super(); this._coverageMap = istanbulCoverage.createCoverageMap({}); + this._sourceMapStore = libSourceMaps.createSourceMapStore(); } onTestResult( @@ -54,6 +57,10 @@ class CoverageReporter extends BaseReporter { runnerContext: RunnerContext, ) { this._addUntestedFiles(config, runnerContext); + const {map, sourceFinder} = config.mapCoverage + ? this._sourceMapStore.transformCoverage(this._coverageMap) + : {map: this._coverageMap, sourceFinder: undefined}; + const reporter = createReporter(); try { if (config.coverageDirectory) { @@ -70,8 +77,8 @@ class CoverageReporter extends BaseReporter { } reporter.addAll(coverageReporters); - reporter.write(this._coverageMap); - aggregatedResults.coverageMap = this._coverageMap; + reporter.write(map, sourceFinder && {sourceFinder}); + aggregatedResults.coverageMap = map; } catch (e) { console.error(chalk.red(` Failed to write coverage reports: @@ -80,7 +87,7 @@ class CoverageReporter extends BaseReporter { `)); } - this._checkThreshold(config); + this._checkThreshold(map, config); } _addUntestedFiles(config: Config, runnerContext: RunnerContext) { @@ -118,9 +125,9 @@ class CoverageReporter extends BaseReporter { } } - _checkThreshold(config: Config) { + _checkThreshold(map: CoverageMap, config: Config) { if (config.coverageThreshold) { - const results = this._coverageMap.getCoverageSummary().toJSON(); + const results = map.getCoverageSummary().toJSON(); function check(name, thresholds, actuals) { return [ diff --git a/packages/jest-cli/src/reporters/__tests__/CoverageReporter-test.js b/packages/jest-cli/src/reporters/__tests__/CoverageReporter-test.js index 9c6eaf68a473..2bcd9db1c393 100644 --- a/packages/jest-cli/src/reporters/__tests__/CoverageReporter-test.js +++ b/packages/jest-cli/src/reporters/__tests__/CoverageReporter-test.js @@ -12,9 +12,11 @@ jest .mock('fs') .mock('istanbul-lib-coverage') + .mock('istanbul-lib-source-maps') .mock('istanbul-api'); let libCoverage; +let libSourceMaps; let CoverageReporter; let istanbulApi; @@ -27,6 +29,7 @@ beforeEach(() => { CoverageReporter = require('../CoverageReporter'); libCoverage = require('istanbul-lib-coverage'); + libSourceMaps = require('istanbul-lib-source-maps'); }); describe('onRunComplete', () => { @@ -66,6 +69,14 @@ describe('onRunComplete', () => { }; }); + libSourceMaps.createSourceMapStore = jest.fn(() => { + return { + transformCoverage(map) { + return {map}; + }, + }; + }); + testReporter = new CoverageReporter(); testReporter.log = jest.fn(); }); diff --git a/packages/jest-config/src/defaults.js b/packages/jest-config/src/defaults.js index 5d8a7b655d89..c92e55ca30b1 100644 --- a/packages/jest-config/src/defaults.js +++ b/packages/jest-config/src/defaults.js @@ -32,6 +32,7 @@ module.exports = ({ haste: { providesModuleNodeModules: [], }, + mapCoverage: true, moduleDirectories: ['node_modules'], moduleFileExtensions: [ 'js', diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index 2ca2218618d6..5c0fb26c3876 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -335,6 +335,7 @@ function normalize(config: InitialConfig, argv: Object = {}) { case 'haste': case 'logHeapUsage': case 'logTransformErrors': + case 'mapCoverage': case 'moduleDirectories': case 'moduleFileExtensions': case 'moduleLoader': diff --git a/packages/jest-config/src/validConfig.js b/packages/jest-config/src/validConfig.js index 27632e2c9205..307451558104 100644 --- a/packages/jest-config/src/validConfig.js +++ b/packages/jest-config/src/validConfig.js @@ -45,6 +45,7 @@ module.exports = ({ }, logHeapUsage: true, logTransformErrors: true, + mapCoverage: true, moduleDirectories: ['node_modules'], moduleFileExtensions: ['js', 'json', 'jsx', 'node'], moduleLoader: '', diff --git a/packages/jest-runtime/package.json b/packages/jest-runtime/package.json index 58110d9c0edb..35939383a65f 100644 --- a/packages/jest-runtime/package.json +++ b/packages/jest-runtime/package.json @@ -10,9 +10,10 @@ "dependencies": { "babel-core": "^6.0.0", "babel-jest": "^18.0.0", - "babel-plugin-istanbul": "^3.0.0", + "babel-plugin-istanbul": "^3.1.2", "chalk": "^1.1.3", "graceful-fs": "^4.1.6", + "istanbul-lib-instrument": "^1.3.0", "jest-config": "^18.1.0", "jest-file-exists": "^17.0.0", "jest-haste-map": "^18.1.0", diff --git a/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap b/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap index c96d7f56ed11..767ceabd8ab4 100644 --- a/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap +++ b/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap @@ -1,3 +1,13 @@ +exports[`transform does not instrument with source map if mapCoverage config option is false 1`] = ` +"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"4a81f91764b586e5729481ba491440a08d658002\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 0 }, end: { line: 1, column: 7 } } }, fnMap: {}, branchMap: {}, s: { \\"0\\": 0 }, f: {}, b: {}, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();++cov_25u22311x4.s[0];content; +}});" +`; + +exports[`transform instruments with source map if preprocessor supplies it 1`] = ` +"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"2af15ff13eaa7e17fc0a001b1b75722b8fbeeb74\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 0 }, end: { line: 1, column: 7 } } }, fnMap: {}, branchMap: {}, s: { \\"0\\": 0 }, f: {}, b: {}, inputSourceMap: { mappings: \\";AAAA\\", version: 3 }, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();++cov_25u22311x4.s[0];content; +}});" +`; + exports[`transform transforms a file properly 1`] = ` "({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"04636d4ae73b4b3e24bf6fba39e08c946fd0afb5\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 0 }, end: { line: 1, column: 26 } } }, fnMap: {}, branchMap: {}, s: { \\"0\\": 0 }, f: {}, b: {}, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();++cov_25u22311x4.s[0];module.exports = \\"banana\\"; }});" diff --git a/packages/jest-runtime/src/__tests__/transform-test.js b/packages/jest-runtime/src/__tests__/transform-test.js index 3b8c94dd065f..0f991535c4ec 100644 --- a/packages/jest-runtime/src/__tests__/transform-test.js +++ b/packages/jest-runtime/src/__tests__/transform-test.js @@ -44,6 +44,25 @@ jest.mock( {virtual: true}, ); +jest.mock( + 'preprocessor-with-sourcemaps', + () => { + return { + getCacheKey: jest.fn((content, filename, configStr) => 'ab'), + process: (content, filename, config) => { + return { + content: 'content', + sourceMap: { + mappings: ';AAAA', + version: 3, + }, + }; + }, + }; + }, + {virtual: true}, +); + jest.mock( 'css-preprocessor', () => { @@ -212,6 +231,28 @@ describe('transform', () => { expect(vm.Script.mock.calls[2][0]).toMatchSnapshot(); }); + it('instruments with source map if preprocessor supplies it', () => { + config = Object.assign(config, { + collectCoverage: true, + mapCoverage: true, + transform: [['^.+\\.js$', 'preprocessor-with-sourcemaps']], + }); + + transform('/fruits/banana.js', config); + expect(vm.Script.mock.calls[0][0]).toMatchSnapshot(); + }); + + it('does not instrument with source map if mapCoverage config option is false', () => { + config = Object.assign(config, { + collectCoverage: true, + mapCoverage: false, + transform: [['^.+\\.js$', 'preprocessor-with-sourcemaps']], + }); + + transform('/fruits/banana.js', config); + expect(vm.Script.mock.calls[0][0]).toMatchSnapshot(); + }); + it('reads values from the cache', () => { if (skipOnWindows.test()) { return; diff --git a/packages/jest-runtime/src/transform.js b/packages/jest-runtime/src/transform.js index 7ecc0ae918ae..fcc91280aab8 100644 --- a/packages/jest-runtime/src/transform.js +++ b/packages/jest-runtime/src/transform.js @@ -10,7 +10,7 @@ 'use strict'; import type {Config, Path} from 'types/Config'; -import type {Transformer} from 'types/Transform'; +import type {Transformer, TransformedSource} from 'types/Transform'; const createDirectory = require('jest-util').createDirectory; const crypto = require('crypto'); @@ -235,7 +235,7 @@ const stripShebang = content => { }; const instrumentFile = ( - content: string, + transformedSource: TransformedSource, filename: Path, config: Config, ): string => { @@ -243,18 +243,24 @@ const instrumentFile = ( // time by 2sec if not running in `--coverage` mode const babel = require('babel-core'); const babelPluginIstanbul = require('babel-plugin-istanbul').default; + const pluginOptions: any = { + cwd: config.rootDir, // files outside `cwd` will not be instrumented + exclude: [], + useInlineSourceMaps: config.mapCoverage, + }; + + if (transformedSource.sourceMap && config.mapCoverage) { + pluginOptions.inputSourceMap = transformedSource.sourceMap; + } - return babel.transform(content, { + return babel.transform(transformedSource.content, { auxiliaryCommentBefore: ' istanbul ignore next ', babelrc: false, filename, plugins: [ [ babelPluginIstanbul, - { - cwd: config.rootDir, // files outside `cwd` will not be instrumented - exclude: [], - }, + pluginOptions, ], ], retainLines: true, @@ -276,21 +282,36 @@ const transformSource = ( return result; } - result = content; + let transformed: TransformedSource = { + content, + sourceMap: null, + }; if (transform && shouldTransform(filename, config)) { - result = transform.process(result, filename, config, { + const processed = transform.process(content, filename, config, { instrument, watch: config.watch, }); + + if (typeof processed === 'string') { + transformed.content = processed; + } else { + transformed = processed; + } + } + + if (config.mapCoverage === false) { + transformed.sourceMap = null; } // That means that the transform has a custom instrumentation // logic and will handle it based on `config.collectCoverage` option - const transformWillInstrument = transform && transform.canInstrument; + const transformDidInstrument = transform && transform.canInstrument; - if (!transformWillInstrument && instrument) { - result = instrumentFile(result, filename, config); + if (!transformDidInstrument && instrument) { + result = instrumentFile(transformed, filename, config); + } else { + result = transformed.content; } writeCacheFile(cacheFilePath, result); diff --git a/scripts/build.js b/scripts/build.js index 9014528a968b..dee6b9c52412 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -25,7 +25,7 @@ const fs = require('fs'); const getPackages = require('./_getPackages'); const glob = require('glob'); const micromatch = require('micromatch'); -const mkdirp = require('mkdirp'); +const mkdir = require('mkdirp'); const path = require('path'); const SRC_DIR = 'src'; @@ -69,7 +69,7 @@ function buildFile(file, silent) { const relativeToSrcPath = path.relative(packageSrcPath, file); const destPath = path.resolve(packageBuildPath, relativeToSrcPath); - mkdirp.sync(path.dirname(destPath)); + mkdir.sync(path.dirname(destPath)); if (micromatch.isMatch(file, IGNORE_PATTERN)) { silent || process.stdout.write( chalk.dim(' \u2022 ') + @@ -88,6 +88,7 @@ function buildFile(file, silent) { ); } else { const transformed = babel.transformFileSync(file, babelOptions).code; + mkdir.sync(path.dirname(destPath)); fs.writeFileSync(destPath, transformed); silent || process.stdout.write( chalk.green(' \u2022 ') + diff --git a/types/Config.js b/types/Config.js index 0f5838d56f16..cfd17b0011ea 100644 --- a/types/Config.js +++ b/types/Config.js @@ -31,6 +31,7 @@ export type DefaultConfig = {| expand: boolean, globals: ConfigGlobals, haste: HasteConfig, + mapCoverage: boolean, moduleDirectories: Array, moduleFileExtensions: Array, moduleNameMapper: {[key: string]: string}, @@ -73,6 +74,7 @@ export type Config = {| forceExit: boolean, globals: ConfigGlobals, haste: HasteConfig, + mapCoverage: boolean, logHeapUsage: boolean, logTransformErrors: ?boolean, moduleDirectories: Array, @@ -133,6 +135,7 @@ export type InitialConfig = {| haste?: HasteConfig, logHeapUsage?: boolean, logTransformErrors?: ?boolean, + mapCoverage?: boolean, moduleDirectories?: Array, moduleFileExtensions?: Array, moduleLoader?: Path, diff --git a/types/TestResult.js b/types/TestResult.js index 6d54fdd384fa..27ff6e0631a4 100644 --- a/types/TestResult.js +++ b/types/TestResult.js @@ -10,13 +10,24 @@ 'use strict'; import type {ConsoleBuffer} from './Console'; - -export type Coverage = {| - coveredSpans: Array, - uncoveredSpans: Array, - sourceText: string, +import type {SourceMap} from './Transform'; + +export type RawFileCoverage = {| + path: string, + s: { [statementId: number]: number }, + b: { [branchId: number]: number }, + f: { [functionId: number]: number }, + l: { [lineId: number]: number }, + fnMap: { [functionId: number]: any }, + statementMap: { [statementId: number]: any }, + branchMap: { [branchId: number]: any }, + inputSourceMap?: SourceMap, |}; +export type RawCoverage = { + [filePath: string]: RawFileCoverage, +}; + type FileCoverageTotal = {| total: number, covered: number, @@ -46,12 +57,19 @@ export type FileCoverage = {| export type CoverageMap = {| merge: (data: Object) => void, getCoverageSummary: () => FileCoverage, - data: Object, - addFileCoverage: (fileCoverage: Object) => void, + data: RawCoverage, + addFileCoverage: (fileCoverage: RawFileCoverage) => void, files: () => Array, fileCoverageFor: (file: string) => FileCoverage, |}; +export class SourceMapStore { + transformCoverage: (coverageMap: CoverageMap) => { + map: CoverageMap, + sourceFinder: any + } +} + export type Error = {| message: string, stack: ?string, @@ -115,7 +133,7 @@ export type Suite = {| export type TestResult = {| console: ?ConsoleBuffer, - coverage?: Coverage, + coverage?: RawCoverage, memoryUsage?: Bytes, failureMessage: ?string, numFailingTests: number, @@ -171,7 +189,7 @@ export type FormattedTestResults = { export type CodeCoverageReporter = any; export type CodeCoverageFormatter = ( - coverage: ?Coverage, + coverage: ?RawCoverage, reporter?: CodeCoverageReporter, ) => ?Object; diff --git a/types/Transform.js b/types/Transform.js index 9fa6ffdc9c45..e7e6ee44e384 100644 --- a/types/Transform.js +++ b/types/Transform.js @@ -11,6 +11,21 @@ import type {Config, Path} from 'types/Config'; +export type TransformedSource = {| + content: string, + sourceMap: ?SourceMap, +|}; + +export type SourceMap = {| + file: string, + mappings: string, + names: string[], + sourceRoot: string, + sources: string[], + sourcesContent: string[], + version: number, +|}; + export type TransformOptions = {| instrument: boolean, watch: boolean, @@ -31,5 +46,5 @@ export type Transformer = {| sourcePath: Path, config: Config, options?: TransformOptions, - ) => string, + ) => string | TransformedSource, |}; From 6ad632dbb849a09366a03d85f6575682a0fdfd35 Mon Sep 17 00:00:00 2001 From: Justin Bay Date: Wed, 1 Feb 2017 22:38:22 -0500 Subject: [PATCH 2/8] be less dangerous only enforce files in src/ having @flow since everything else may not be run through babel --- dangerfile.js | 5 ++--- .../coverage-remapping/typescript-preprocessor.js | 10 +++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/dangerfile.js b/dangerfile.js index e6f169cbb479..243b66e55122 100644 --- a/dangerfile.js +++ b/dangerfile.js @@ -51,8 +51,7 @@ const raiseIssueAboutPaths = ( }; const newJsFiles = danger.git.created_files.filter(path => path.endsWith('js')); -const isNotInTestFiles = path => !(includes(path, '__tests__') - || includes(path, '__mocks__')); +const isSourceFile = path => includes(path, '/src/'); // New JS files should have the FB copyright header + flow const facebookLicenseHeaderComponents = [ @@ -80,7 +79,7 @@ if (noFBCopyrightFiles.length > 0) { // Ensure the majority of all files use Flow // Does not run for test files, and also offers a warning not an error. const noFlowFiles = newJsFiles - .filter(isNotInTestFiles) + .filter(isSourceFile) .filter(filepath => { const content = fs.readFileSync(filepath).toString(); return !includes(content, '@flow'); diff --git a/integration_tests/coverage-remapping/typescript-preprocessor.js b/integration_tests/coverage-remapping/typescript-preprocessor.js index 2cdebdc9b88d..248fba6cfeac 100644 --- a/integration_tests/coverage-remapping/typescript-preprocessor.js +++ b/integration_tests/coverage-remapping/typescript-preprocessor.js @@ -1,4 +1,12 @@ -// Copyright 2004-present Facebook. All Rights Reserved. +/** + * 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. + */ + +'use strict'; const tsc = require('typescript'); From 04cda593c1e2c54e9a681e4d0553181a0bff909e Mon Sep 17 00:00:00 2001 From: Justin Bay Date: Mon, 6 Feb 2017 19:10:40 -0500 Subject: [PATCH 3/8] lazy require for istanbul-lib-source-maps --- .../jest-cli/src/reporters/CoverageReporter.js | 15 ++++++++------- packages/jest-runtime/src/transform.js | 2 +- scripts/build.js | 5 ++--- types/TestResult.js | 7 ------- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/jest-cli/src/reporters/CoverageReporter.js b/packages/jest-cli/src/reporters/CoverageReporter.js index 5ad4e12f39e7..2073965c3158 100644 --- a/packages/jest-cli/src/reporters/CoverageReporter.js +++ b/packages/jest-cli/src/reporters/CoverageReporter.js @@ -9,7 +9,7 @@ */ 'use strict'; -import type {AggregatedResult, CoverageMap, SourceMapStore, TestResult} from 'types/TestResult'; +import type {AggregatedResult, CoverageMap, TestResult} from 'types/TestResult'; import type {Config} from 'types/Config'; import type {RunnerContext} from 'types/Reporters'; @@ -22,7 +22,6 @@ const fs = require('fs'); const generateEmptyCoverage = require('../generateEmptyCoverage'); const isCI = require('is-ci'); const istanbulCoverage = require('istanbul-lib-coverage'); -const libSourceMaps = require('istanbul-lib-source-maps'); const FAIL_COLOR = chalk.bold.red; const RUNNING_TEST_COLOR = chalk.bold.dim; @@ -31,12 +30,10 @@ const isInteractive = process.stdout.isTTY && !isCI; class CoverageReporter extends BaseReporter { _coverageMap: CoverageMap; - _sourceMapStore: SourceMapStore; constructor() { super(); this._coverageMap = istanbulCoverage.createCoverageMap({}); - this._sourceMapStore = libSourceMaps.createSourceMapStore(); } onTestResult( @@ -57,9 +54,13 @@ class CoverageReporter extends BaseReporter { runnerContext: RunnerContext, ) { this._addUntestedFiles(config, runnerContext); - const {map, sourceFinder} = config.mapCoverage - ? this._sourceMapStore.transformCoverage(this._coverageMap) - : {map: this._coverageMap, sourceFinder: undefined}; + let map = this._coverageMap; + let sourceFinder: Object; + if (config.mapCoverage) { + const libSourceMaps = require('istanbul-lib-source-maps'); + const sourceMapStore = libSourceMaps.createSourceMapStore(); + ({map, sourceFinder} = sourceMapStore.transformCoverage(map)); + } const reporter = createReporter(); try { diff --git a/packages/jest-runtime/src/transform.js b/packages/jest-runtime/src/transform.js index fcc91280aab8..4fcfa91e2254 100644 --- a/packages/jest-runtime/src/transform.js +++ b/packages/jest-runtime/src/transform.js @@ -300,7 +300,7 @@ const transformSource = ( } } - if (config.mapCoverage === false) { + if (!config.mapCoverage) { transformed.sourceMap = null; } diff --git a/scripts/build.js b/scripts/build.js index dee6b9c52412..9014528a968b 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -25,7 +25,7 @@ const fs = require('fs'); const getPackages = require('./_getPackages'); const glob = require('glob'); const micromatch = require('micromatch'); -const mkdir = require('mkdirp'); +const mkdirp = require('mkdirp'); const path = require('path'); const SRC_DIR = 'src'; @@ -69,7 +69,7 @@ function buildFile(file, silent) { const relativeToSrcPath = path.relative(packageSrcPath, file); const destPath = path.resolve(packageBuildPath, relativeToSrcPath); - mkdir.sync(path.dirname(destPath)); + mkdirp.sync(path.dirname(destPath)); if (micromatch.isMatch(file, IGNORE_PATTERN)) { silent || process.stdout.write( chalk.dim(' \u2022 ') + @@ -88,7 +88,6 @@ function buildFile(file, silent) { ); } else { const transformed = babel.transformFileSync(file, babelOptions).code; - mkdir.sync(path.dirname(destPath)); fs.writeFileSync(destPath, transformed); silent || process.stdout.write( chalk.green(' \u2022 ') + diff --git a/types/TestResult.js b/types/TestResult.js index 27ff6e0631a4..96ebcbd444aa 100644 --- a/types/TestResult.js +++ b/types/TestResult.js @@ -63,13 +63,6 @@ export type CoverageMap = {| fileCoverageFor: (file: string) => FileCoverage, |}; -export class SourceMapStore { - transformCoverage: (coverageMap: CoverageMap) => { - map: CoverageMap, - sourceFinder: any - } -} - export type Error = {| message: string, stack: ?string, From 4b6011f508c9eed83ac2241cb2c444eb2381698d Mon Sep 17 00:00:00 2001 From: Justin Bay Date: Wed, 8 Feb 2017 19:51:34 -0500 Subject: [PATCH 4/8] add documentation for mapCoverage config option --- docs/Configuration.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/Configuration.md b/docs/Configuration.md index 6755ecd34690..23d14067cd7b 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -150,6 +150,19 @@ For example, the following would create a global `__DEV__` variable set to `true Note that, if you specify a global reference value (like an object or array) here, and some code mutates that value in the midst of running a test, that mutation will *not* be persisted across test runs for other test files. +### `mapCoverage` [boolean] + +##### available in Jest **19.0.0+** + +Default: `true` + +If you have [transformers](#transform-object-string-string) configured that emit +source maps, Jest will use them to map code coverage against the original source +code when writing [reports](#coveragereporters-array-string) and checking +[thresholds](#coveragethreshold-object). This can be resource-intensive. If Jest +consumes large amounts of memory while calculating coverage, try setting this +option to `false`. + ### `moduleFileExtensions` [array] Default: `["js", "json", "jsx", "node"]` From ff8941a80fd8a122732507c2ca87e68139971258 Mon Sep 17 00:00:00 2001 From: Justin Bay Date: Sun, 12 Feb 2017 19:35:44 -0500 Subject: [PATCH 5/8] store source maps on disk instead of in worker process memory --- .eslintignore | 1 + .../src/reporters/CoverageReporter.js | 16 ++++- packages/jest-runtime/package.json | 1 + .../__snapshots__/transform-test.js.snap | 9 ++- .../__tests__/test_root/hasInlineSourceMap.js | 2 + .../src/__tests__/transform-test.js | 66 ++++++++++++++++--- packages/jest-runtime/src/transform.js | 45 +++++++++---- types/TestResult.js | 4 +- types/Transform.js | 12 +--- 9 files changed, 116 insertions(+), 40 deletions(-) create mode 100644 packages/jest-runtime/src/__tests__/test_root/hasInlineSourceMap.js diff --git a/.eslintignore b/.eslintignore index d52f9a2d336b..8403368d0441 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,6 +4,7 @@ bin/ docs/ examples/react-native +packages/jest-runtime/src/__tests__/test_root/hasInlineSourceMap.js flow-typed/** packages/*/build/** types/** diff --git a/packages/jest-cli/src/reporters/CoverageReporter.js b/packages/jest-cli/src/reporters/CoverageReporter.js index 2073965c3158..d48539a039e3 100644 --- a/packages/jest-cli/src/reporters/CoverageReporter.js +++ b/packages/jest-cli/src/reporters/CoverageReporter.js @@ -22,6 +22,7 @@ const fs = require('fs'); const generateEmptyCoverage = require('../generateEmptyCoverage'); const isCI = require('is-ci'); const istanbulCoverage = require('istanbul-lib-coverage'); +const libSourceMaps = require('istanbul-lib-source-maps'); const FAIL_COLOR = chalk.bold.red; const RUNNING_TEST_COLOR = chalk.bold.dim; @@ -30,10 +31,12 @@ const isInteractive = process.stdout.isTTY && !isCI; class CoverageReporter extends BaseReporter { _coverageMap: CoverageMap; + _sourceMapStore: any; constructor() { super(); this._coverageMap = istanbulCoverage.createCoverageMap({}); + this._sourceMapStore = libSourceMaps.createSourceMapStore(); } onTestResult( @@ -42,6 +45,15 @@ class CoverageReporter extends BaseReporter { aggregatedResults: AggregatedResult, ) { if (testResult.coverage) { + if (config.mapCoverage) { + Object.keys(testResult.coverage).map(path => { + // $FlowFixMe - ignores null check above + const {inputSourceMapPath} = testResult.coverage[path]; + if (inputSourceMapPath) { + this._sourceMapStore.registerURL(path, inputSourceMapPath); + } + }); + } this._coverageMap.merge(testResult.coverage); // Remove coverage data to free up some memory. delete testResult.coverage; @@ -57,9 +69,7 @@ class CoverageReporter extends BaseReporter { let map = this._coverageMap; let sourceFinder: Object; if (config.mapCoverage) { - const libSourceMaps = require('istanbul-lib-source-maps'); - const sourceMapStore = libSourceMaps.createSourceMapStore(); - ({map, sourceFinder} = sourceMapStore.transformCoverage(map)); + ({map, sourceFinder} = this._sourceMapStore.transformCoverage(map)); } const reporter = createReporter(); diff --git a/packages/jest-runtime/package.json b/packages/jest-runtime/package.json index 35939383a65f..91157de5c175 100644 --- a/packages/jest-runtime/package.json +++ b/packages/jest-runtime/package.json @@ -12,6 +12,7 @@ "babel-jest": "^18.0.0", "babel-plugin-istanbul": "^3.1.2", "chalk": "^1.1.3", + "convert-source-map": "^1.3.0", "graceful-fs": "^4.1.6", "istanbul-lib-instrument": "^1.3.0", "jest-config": "^18.1.0", diff --git a/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap b/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap index 767ceabd8ab4..b1e6779361df 100644 --- a/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap +++ b/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap @@ -3,8 +3,15 @@ exports[`transform does not instrument with source map if mapCoverage config opt }});" `; +exports[`transform instruments with source map if preprocessor inlines it 1`] = ` +"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"5dc8e485e00fed337d9101d8fac0e5dd5a88b834\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 12 }, end: { line: 1, column: 24 } } }, fnMap: {}, branchMap: { \\"0\\": { loc: { start: { line: 1, column: 12 }, end: { line: 1, column: 24 } }, type: \\"cond-expr\\", locations: [{ start: { line: 1, column: 19 }, end: { line: 1, column: 20 } }, { start: { line: 1, column: 23 }, end: { line: 1, column: 24 } }] } }, s: { \\"0\\": 0 }, f: {}, b: { \\"0\\": [0, 0] }, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();var value = /* istanbul ignore next */(++cov_25u22311x4.s[0], true ? /* istanbul ignore next */(++cov_25u22311x4.b[0][0], 1) : /* istanbul ignore next */(++cov_25u22311x4.b[0][1], 0)); +;global.__coverage__['/fruits/banana.js'].inputSourceMapPath = '/cache/jest-transform-cache-test/ab/banana_ab.map'; +}});" +`; + exports[`transform instruments with source map if preprocessor supplies it 1`] = ` -"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"2af15ff13eaa7e17fc0a001b1b75722b8fbeeb74\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 0 }, end: { line: 1, column: 7 } } }, fnMap: {}, branchMap: {}, s: { \\"0\\": 0 }, f: {}, b: {}, inputSourceMap: { mappings: \\";AAAA\\", version: 3 }, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();++cov_25u22311x4.s[0];content; +"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"4a81f91764b586e5729481ba491440a08d658002\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 0 }, end: { line: 1, column: 7 } } }, fnMap: {}, branchMap: {}, s: { \\"0\\": 0 }, f: {}, b: {}, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();++cov_25u22311x4.s[0];content; +;global.__coverage__['/fruits/banana.js'].inputSourceMapPath = '/cache/jest-transform-cache-test/ab/banana_ab.map'; }});" `; diff --git a/packages/jest-runtime/src/__tests__/test_root/hasInlineSourceMap.js b/packages/jest-runtime/src/__tests__/test_root/hasInlineSourceMap.js new file mode 100644 index 000000000000..b27d518faa2f --- /dev/null +++ b/packages/jest-runtime/src/__tests__/test_root/hasInlineSourceMap.js @@ -0,0 +1,2 @@ +var value = true ? 1 : 0; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInRlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsSUFBTSxLQUFLLEdBQVcsSUFBSSxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUMifQ== diff --git a/packages/jest-runtime/src/__tests__/transform-test.js b/packages/jest-runtime/src/__tests__/transform-test.js index 0f991535c4ec..bc5ed914e7b7 100644 --- a/packages/jest-runtime/src/__tests__/transform-test.js +++ b/packages/jest-runtime/src/__tests__/transform-test.js @@ -10,10 +10,14 @@ 'use strict'; const skipOnWindows = require('skipOnWindows'); +const path = require('path'); jest .mock('graceful-fs') .mock('jest-file-exists') + .mock('jest-haste-map', () => ({ + getCacheFilePath: (cacheDir, baseDir, version) => cacheDir + baseDir, + })) .mock('jest-util', () => { const util = require.requireActual('jest-util'); util.createDirectory = jest.fn(); @@ -47,17 +51,11 @@ jest.mock( jest.mock( 'preprocessor-with-sourcemaps', () => { + const getProcessResult = jest.fn(); return { getCacheKey: jest.fn((content, filename, configStr) => 'ab'), - process: (content, filename, config) => { - return { - content: 'content', - sourceMap: { - mappings: ';AAAA', - version: 3, - }, - }; - }, + getProcessResult, + process: (content, filename, config) => getProcessResult(), }; }, {virtual: true}, @@ -238,8 +236,48 @@ describe('transform', () => { transform: [['^.+\\.js$', 'preprocessor-with-sourcemaps']], }); + const sourceMap = { + mappings: ';AAAA', + version: 3, + }; + + require('preprocessor-with-sourcemaps').getProcessResult.mockReturnValue({ + content: 'content', + sourceMap, + }); + transform('/fruits/banana.js', config); expect(vm.Script.mock.calls[0][0]).toMatchSnapshot(); + expect(fs.writeFileSync).toBeCalledWith( + '/cache/jest-transform-cache-test/ab/banana_ab.map', + JSON.stringify(sourceMap), + 'utf8', + ); + }); + + it('instruments with source map if preprocessor inlines it', () => { + const realFS = require('fs'); + + config = Object.assign(config, { + collectCoverage: true, + mapCoverage: true, + transform: [['^.+\\.js$', 'preprocessor-with-sourcemaps']], + }); + + require('preprocessor-with-sourcemaps').getProcessResult.mockReturnValue({ + content: realFS.readFileSync( + path.resolve(__dirname, './test_root/hasInlineSourceMap.js'), + 'utf8' + ), + }); + + transform('/fruits/banana.js', config); + expect(vm.Script.mock.calls[0][0]).toMatchSnapshot(); + expect(fs.writeFileSync).toBeCalledWith( + '/cache/jest-transform-cache-test/ab/banana_ab.map', + expect.stringMatching(/mappings/), + 'utf8', + ); }); it('does not instrument with source map if mapCoverage config option is false', () => { @@ -249,6 +287,16 @@ describe('transform', () => { transform: [['^.+\\.js$', 'preprocessor-with-sourcemaps']], }); + const sourceMap = { + mappings: ';AAAA', + version: 3, + }; + + require('preprocessor-with-sourcemaps').getProcessResult.mockReturnValue({ + content: 'content', + sourceMap, + }); + transform('/fruits/banana.js', config); expect(vm.Script.mock.calls[0][0]).toMatchSnapshot(); }); diff --git a/packages/jest-runtime/src/transform.js b/packages/jest-runtime/src/transform.js index 4fcfa91e2254..1c86871e6e87 100644 --- a/packages/jest-runtime/src/transform.js +++ b/packages/jest-runtime/src/transform.js @@ -235,7 +235,7 @@ const stripShebang = content => { }; const instrumentFile = ( - transformedSource: TransformedSource, + content: string, filename: Path, config: Config, ): string => { @@ -243,30 +243,28 @@ const instrumentFile = ( // time by 2sec if not running in `--coverage` mode const babel = require('babel-core'); const babelPluginIstanbul = require('babel-plugin-istanbul').default; - const pluginOptions: any = { - cwd: config.rootDir, // files outside `cwd` will not be instrumented - exclude: [], - useInlineSourceMaps: config.mapCoverage, - }; - if (transformedSource.sourceMap && config.mapCoverage) { - pluginOptions.inputSourceMap = transformedSource.sourceMap; - } - - return babel.transform(transformedSource.content, { + return babel.transform(content, { auxiliaryCommentBefore: ' istanbul ignore next ', babelrc: false, filename, plugins: [ [ babelPluginIstanbul, - pluginOptions, + { + cwd: config.rootDir, // files outside `cwd` will not be instrumented + exclude: [], + useInlineSourceMaps: false, + }, ], ], retainLines: true, }).code; }; +const escapePathForJavaScript = (filePath: string) => + filePath.replace(/\\/g, '\\\\'); + const transformSource = ( filename: Path, config: Config, @@ -300,7 +298,15 @@ const transformSource = ( } } - if (!config.mapCoverage) { + if (config.mapCoverage) { + if (!transformed.sourceMap) { + const convert = require('convert-source-map'); + const inlineSourceMap = convert.fromSource(transformed.content); + if (inlineSourceMap) { + transformed.sourceMap = inlineSourceMap.toJSON(); + } + } + } else { transformed.sourceMap = null; } @@ -309,11 +315,22 @@ const transformSource = ( const transformDidInstrument = transform && transform.canInstrument; if (!transformDidInstrument && instrument) { - result = instrumentFile(transformed, filename, config); + result = instrumentFile(transformed.content, filename, config); } else { result = transformed.content; } + if (instrument && transformed.sourceMap && config.mapCoverage) { + const sourceMapContent = typeof transformed.sourceMap === 'string' + ? transformed.sourceMap + : JSON.stringify(transformed.sourceMap); + const sourceMapFilePath = cacheFilePath + '.map'; + writeCacheFile(sourceMapFilePath, sourceMapContent); + result += + `\n;global.__coverage__['${escapePathForJavaScript(filename)}']` + + `.inputSourceMapPath = '${escapePathForJavaScript(sourceMapFilePath)}';`; + } + writeCacheFile(cacheFilePath, result); return result; }; diff --git a/types/TestResult.js b/types/TestResult.js index 96ebcbd444aa..00eacf01a77a 100644 --- a/types/TestResult.js +++ b/types/TestResult.js @@ -10,7 +10,6 @@ 'use strict'; import type {ConsoleBuffer} from './Console'; -import type {SourceMap} from './Transform'; export type RawFileCoverage = {| path: string, @@ -21,7 +20,8 @@ export type RawFileCoverage = {| fnMap: { [functionId: number]: any }, statementMap: { [statementId: number]: any }, branchMap: { [branchId: number]: any }, - inputSourceMap?: SourceMap, + inputSourceMap?: Object, + inputSourceMapPath?: string, |}; export type RawCoverage = { diff --git a/types/Transform.js b/types/Transform.js index e7e6ee44e384..c6b97e86bc7c 100644 --- a/types/Transform.js +++ b/types/Transform.js @@ -13,17 +13,7 @@ import type {Config, Path} from 'types/Config'; export type TransformedSource = {| content: string, - sourceMap: ?SourceMap, -|}; - -export type SourceMap = {| - file: string, - mappings: string, - names: string[], - sourceRoot: string, - sources: string[], - sourcesContent: string[], - version: number, + sourceMap: ?Object | string, |}; export type TransformOptions = {| From ad11e77f411c5801295fb294e9920b267a22fc69 Mon Sep 17 00:00:00 2001 From: Justin Bay Date: Sun, 12 Feb 2017 23:02:48 -0500 Subject: [PATCH 6/8] add mapCoverage to CLI, switch off by default --- .eslintignore | 1 - dangerfile.js | 4 ++- docs/Configuration.md | 20 ++++++----- .../coverage-remapping-test.js.snap | 2 +- .../__tests__/coverage-remapping-test.js | 2 +- packages/jest-cli/src/cli/args.js | 6 ++++ packages/jest-config/src/defaults.js | 2 +- packages/jest-config/src/setFromArgv.js | 4 +++ packages/jest-config/src/validConfig.js | 2 +- .../__snapshots__/transform-test.js.snap | 2 +- .../__tests__/test_root/hasInlineSourceMap.js | 2 -- .../src/__tests__/transform-test.js | 33 +++++++++++-------- 12 files changed, 50 insertions(+), 30 deletions(-) delete mode 100644 packages/jest-runtime/src/__tests__/test_root/hasInlineSourceMap.js diff --git a/.eslintignore b/.eslintignore index 8403368d0441..d52f9a2d336b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,7 +4,6 @@ bin/ docs/ examples/react-native -packages/jest-runtime/src/__tests__/test_root/hasInlineSourceMap.js flow-typed/** packages/*/build/** types/** diff --git a/dangerfile.js b/dangerfile.js index 243b66e55122..49d9a673e478 100644 --- a/dangerfile.js +++ b/dangerfile.js @@ -51,7 +51,9 @@ const raiseIssueAboutPaths = ( }; const newJsFiles = danger.git.created_files.filter(path => path.endsWith('js')); -const isSourceFile = path => includes(path, '/src/'); +const isSourceFile = path => + includes(path, '/src/') && + !includes(path, '__tests__'); // New JS files should have the FB copyright header + flow const facebookLicenseHeaderComponents = [ diff --git a/docs/Configuration.md b/docs/Configuration.md index 23d14067cd7b..06c5e104c130 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -154,14 +154,18 @@ Note that, if you specify a global reference value (like an object or array) her ##### available in Jest **19.0.0+** -Default: `true` - -If you have [transformers](#transform-object-string-string) configured that emit -source maps, Jest will use them to map code coverage against the original source -code when writing [reports](#coveragereporters-array-string) and checking -[thresholds](#coveragethreshold-object). This can be resource-intensive. If Jest -consumes large amounts of memory while calculating coverage, try setting this -option to `false`. +Default: `false` + +If you have [transformers](#transform-object-string-string) configured that emit source maps, Jest will use them to map code coverage against the original source code when writing [reports](#coveragereporters-array-string) and checking [thresholds](#coveragethreshold-object). This can be resource-intensive. If Jest is taking a long time to calculate coverage at the end of a test run, try setting this option to `false`. + +Both inline source maps and source maps returned directly from a transformer are supported. Source map URLs are not supported because Jest may not be able to locate them. To return source maps from a transformer, the `process` function can return an object like the following. The sourceMap property may either be an object, or a string of JSON. + +```js +return { + content: 'the code', + sourceMap: 'the source map', +}; +``` ### `moduleFileExtensions` [array] Default: `["js", "json", "jsx", "node"]` diff --git a/integration_tests/__tests__/__snapshots__/coverage-remapping-test.js.snap b/integration_tests/__tests__/__snapshots__/coverage-remapping-test.js.snap index 9d2d3815470e..15b1454457f2 100644 --- a/integration_tests/__tests__/__snapshots__/coverage-remapping-test.js.snap +++ b/integration_tests/__tests__/__snapshots__/coverage-remapping-test.js.snap @@ -1,4 +1,4 @@ -exports[`test maps code coverage against original source 1`] = ` +exports[`maps code coverage against original source 1`] = ` Object { "covered.ts": Object { "b": Object { diff --git a/integration_tests/__tests__/coverage-remapping-test.js b/integration_tests/__tests__/coverage-remapping-test.js index a4c912f989bb..cce153fe3618 100644 --- a/integration_tests/__tests__/coverage-remapping-test.js +++ b/integration_tests/__tests__/coverage-remapping-test.js @@ -19,7 +19,7 @@ skipOnWindows.suite(); it('maps code coverage against original source', () => { const dir = path.resolve(__dirname, '../coverage-remapping'); run('npm install', dir); - runJest(dir, ['--coverage', '--no-cache']); + runJest(dir, ['--coverage', '--mapCoverage', '--no-cache']); const coverageMapFile = path.join( __dirname, diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index e2823d267254..14fdd1f6cc35 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -147,6 +147,12 @@ const options = { 'leaks. Use together with `--runInBand` and `--expose-gc` in node.', type: 'boolean', }, + mapCoverage: { + description: + 'Maps code coverage reports against original source code when ' + + 'transformers supply source maps.', + type: 'boolean', + }, maxWorkers: { alias: 'w', description: diff --git a/packages/jest-config/src/defaults.js b/packages/jest-config/src/defaults.js index c92e55ca30b1..68854d18f8fa 100644 --- a/packages/jest-config/src/defaults.js +++ b/packages/jest-config/src/defaults.js @@ -32,7 +32,7 @@ module.exports = ({ haste: { providesModuleNodeModules: [], }, - mapCoverage: true, + mapCoverage: false, moduleDirectories: ['node_modules'], moduleFileExtensions: [ 'js', diff --git a/packages/jest-config/src/setFromArgv.js b/packages/jest-config/src/setFromArgv.js index ce29233e35a0..2c6bb994e892 100644 --- a/packages/jest-config/src/setFromArgv.js +++ b/packages/jest-config/src/setFromArgv.js @@ -13,6 +13,10 @@ function setFromArgv(config, argv) { config.collectCoverage = true; } + if (argv.mapCoverage) { + config.mapCoverage = true; + } + if (argv.verbose) { config.verbose = argv.verbose; } diff --git a/packages/jest-config/src/validConfig.js b/packages/jest-config/src/validConfig.js index 307451558104..e6b984c9512e 100644 --- a/packages/jest-config/src/validConfig.js +++ b/packages/jest-config/src/validConfig.js @@ -45,7 +45,7 @@ module.exports = ({ }, logHeapUsage: true, logTransformErrors: true, - mapCoverage: true, + mapCoverage: false, moduleDirectories: ['node_modules'], moduleFileExtensions: ['js', 'json', 'jsx', 'node'], moduleLoader: '', diff --git a/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap b/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap index b1e6779361df..aa5094246548 100644 --- a/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap +++ b/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap @@ -4,7 +4,7 @@ exports[`transform does not instrument with source map if mapCoverage config opt `; exports[`transform instruments with source map if preprocessor inlines it 1`] = ` -"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"5dc8e485e00fed337d9101d8fac0e5dd5a88b834\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 12 }, end: { line: 1, column: 24 } } }, fnMap: {}, branchMap: { \\"0\\": { loc: { start: { line: 1, column: 12 }, end: { line: 1, column: 24 } }, type: \\"cond-expr\\", locations: [{ start: { line: 1, column: 19 }, end: { line: 1, column: 20 } }, { start: { line: 1, column: 23 }, end: { line: 1, column: 24 } }] } }, s: { \\"0\\": 0 }, f: {}, b: { \\"0\\": [0, 0] }, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();var value = /* istanbul ignore next */(++cov_25u22311x4.s[0], true ? /* istanbul ignore next */(++cov_25u22311x4.b[0][0], 1) : /* istanbul ignore next */(++cov_25u22311x4.b[0][1], 0)); +"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"051b2f7208d7f64ba1cda9a8fec8ebb925b6a32a\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 8 }, end: { line: 1, column: 9 } } }, fnMap: {}, branchMap: {}, s: { \\"0\\": 0 }, f: {}, b: {}, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();var x = /* istanbul ignore next */(++cov_25u22311x4.s[0], 1); ;global.__coverage__['/fruits/banana.js'].inputSourceMapPath = '/cache/jest-transform-cache-test/ab/banana_ab.map'; }});" `; diff --git a/packages/jest-runtime/src/__tests__/test_root/hasInlineSourceMap.js b/packages/jest-runtime/src/__tests__/test_root/hasInlineSourceMap.js deleted file mode 100644 index b27d518faa2f..000000000000 --- a/packages/jest-runtime/src/__tests__/test_root/hasInlineSourceMap.js +++ /dev/null @@ -1,2 +0,0 @@ -var value = true ? 1 : 0; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInRlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsSUFBTSxLQUFLLEdBQVcsSUFBSSxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUMifQ== diff --git a/packages/jest-runtime/src/__tests__/transform-test.js b/packages/jest-runtime/src/__tests__/transform-test.js index bc5ed914e7b7..9a363b5cc941 100644 --- a/packages/jest-runtime/src/__tests__/transform-test.js +++ b/packages/jest-runtime/src/__tests__/transform-test.js @@ -10,7 +10,6 @@ 'use strict'; const skipOnWindows = require('skipOnWindows'); -const path = require('path'); jest .mock('graceful-fs') @@ -51,11 +50,9 @@ jest.mock( jest.mock( 'preprocessor-with-sourcemaps', () => { - const getProcessResult = jest.fn(); return { getCacheKey: jest.fn((content, filename, configStr) => 'ab'), - getProcessResult, - process: (content, filename, config) => getProcessResult(), + process: jest.fn(), }; }, {virtual: true}, @@ -230,6 +227,10 @@ describe('transform', () => { }); it('instruments with source map if preprocessor supplies it', () => { + if (skipOnWindows.test()) { //snapshot has os-dependent path separators + return; + } + config = Object.assign(config, { collectCoverage: true, mapCoverage: true, @@ -241,7 +242,7 @@ describe('transform', () => { version: 3, }; - require('preprocessor-with-sourcemaps').getProcessResult.mockReturnValue({ + require('preprocessor-with-sourcemaps').process.mockReturnValue({ content: 'content', sourceMap, }); @@ -256,7 +257,9 @@ describe('transform', () => { }); it('instruments with source map if preprocessor inlines it', () => { - const realFS = require('fs'); + if (skipOnWindows.test()) { //snapshot has os-dependent path separators + return; + } config = Object.assign(config, { collectCoverage: true, @@ -264,18 +267,22 @@ describe('transform', () => { transform: [['^.+\\.js$', 'preprocessor-with-sourcemaps']], }); - require('preprocessor-with-sourcemaps').getProcessResult.mockReturnValue({ - content: realFS.readFileSync( - path.resolve(__dirname, './test_root/hasInlineSourceMap.js'), - 'utf8' - ), + const sourceMap = JSON.stringify({ + mappings: 'AAAA,IAAM,CAAC,GAAW,CAAC,CAAC', + version: 3, }); + const content = 'var x = 1;\n' + + '//# sourceMappingURL=data:application/json;base64,' + + new Buffer(sourceMap).toString('base64'); + + require('preprocessor-with-sourcemaps').process.mockReturnValue(content); + transform('/fruits/banana.js', config); expect(vm.Script.mock.calls[0][0]).toMatchSnapshot(); expect(fs.writeFileSync).toBeCalledWith( '/cache/jest-transform-cache-test/ab/banana_ab.map', - expect.stringMatching(/mappings/), + sourceMap, 'utf8', ); }); @@ -292,7 +299,7 @@ describe('transform', () => { version: 3, }; - require('preprocessor-with-sourcemaps').getProcessResult.mockReturnValue({ + require('preprocessor-with-sourcemaps').process.mockReturnValue({ content: 'content', sourceMap, }); From ecbde2083e384c1956f300a3d2dbebbb161f7fc3 Mon Sep 17 00:00:00 2001 From: Justin Bay Date: Thu, 16 Feb 2017 00:12:48 -0500 Subject: [PATCH 7/8] move source map info from coverage object to jest-runtime --- .../typescript-preprocessor.js | 4 +- packages/jest-cli/src/TestRunner.js | 1 + .../__tests__/generateEmptyCoverage-test.js | 2 +- .../jest-cli/src/generateEmptyCoverage.js | 9 +- .../src/reporters/CoverageReporter.js | 28 +++-- packages/jest-cli/src/runTest.js | 1 + .../__snapshots__/transform-test.js.snap | 17 --- .../src/__tests__/instrumentation-test.js | 2 +- .../src/__tests__/transform-test.js | 37 +++--- packages/jest-runtime/src/index.js | 23 +++- packages/jest-runtime/src/transform.js | 105 ++++++++++-------- types/TestResult.js | 1 + types/Transform.js | 10 +- 13 files changed, 134 insertions(+), 106 deletions(-) diff --git a/integration_tests/coverage-remapping/typescript-preprocessor.js b/integration_tests/coverage-remapping/typescript-preprocessor.js index 248fba6cfeac..019e8f7a1e1b 100644 --- a/integration_tests/coverage-remapping/typescript-preprocessor.js +++ b/integration_tests/coverage-remapping/typescript-preprocessor.js @@ -24,8 +24,8 @@ module.exports = { } ); return { - content: result.outputText, - sourceMap: JSON.parse(result.sourceMapText), + code: result.outputText, + map: JSON.parse(result.sourceMapText), }; } return src; diff --git a/packages/jest-cli/src/TestRunner.js b/packages/jest-cli/src/TestRunner.js index 88dd1258980b..c3cf566c6998 100644 --- a/packages/jest-cli/src/TestRunner.js +++ b/packages/jest-cli/src/TestRunner.js @@ -523,6 +523,7 @@ const buildFailureTestResult = ( unmatched: 0, updated: 0, }, + sourceMaps: {}, testExecError: err, testFilePath: testPath, testResults: [], diff --git a/packages/jest-cli/src/__tests__/generateEmptyCoverage-test.js b/packages/jest-cli/src/__tests__/generateEmptyCoverage-test.js index 6b5b7a22b8df..c5f24186100c 100644 --- a/packages/jest-cli/src/__tests__/generateEmptyCoverage-test.js +++ b/packages/jest-cli/src/__tests__/generateEmptyCoverage-test.js @@ -39,5 +39,5 @@ it('generates an empty coverage object for a file without running it', () => { baseCacheDir: os.tmpdir(), cacheDirectory: os.tmpdir(), rootDir: os.tmpdir(), - })).toMatchSnapshot(); + }).coverage).toMatchSnapshot(); }); diff --git a/packages/jest-cli/src/generateEmptyCoverage.js b/packages/jest-cli/src/generateEmptyCoverage.js index 6eade08c24a3..5ea23983e670 100644 --- a/packages/jest-cli/src/generateEmptyCoverage.js +++ b/packages/jest-cli/src/generateEmptyCoverage.js @@ -20,10 +20,13 @@ module.exports = function(source: string, filename: Path, config: Config) { if (shouldInstrument(filename, config)) { // Transform file without instrumentation first, to make sure produced // source code is ES6 (no flowtypes etc.) and can be instrumented - source = transformSource(filename, config, source, false); + const transformResult = transformSource(filename, config, source, false); const instrumenter = IstanbulInstrument.createInstrumenter(); - instrumenter.instrumentSync(source, filename); - return instrumenter.fileCoverage; + instrumenter.instrumentSync(transformResult.code, filename); + return { + coverage: instrumenter.fileCoverage, + sourceMapPath: transformResult.sourceMapPath, + }; } else { return null; } diff --git a/packages/jest-cli/src/reporters/CoverageReporter.js b/packages/jest-cli/src/reporters/CoverageReporter.js index d48539a039e3..db61612b8bb0 100644 --- a/packages/jest-cli/src/reporters/CoverageReporter.js +++ b/packages/jest-cli/src/reporters/CoverageReporter.js @@ -45,18 +45,16 @@ class CoverageReporter extends BaseReporter { aggregatedResults: AggregatedResult, ) { if (testResult.coverage) { - if (config.mapCoverage) { - Object.keys(testResult.coverage).map(path => { - // $FlowFixMe - ignores null check above - const {inputSourceMapPath} = testResult.coverage[path]; - if (inputSourceMapPath) { - this._sourceMapStore.registerURL(path, inputSourceMapPath); - } - }); - } this._coverageMap.merge(testResult.coverage); // Remove coverage data to free up some memory. delete testResult.coverage; + + Object.keys(testResult.sourceMaps).forEach(sourcePath => { + this._sourceMapStore.registerURL( + sourcePath, + testResult.sourceMaps[sourcePath] + ); + }); } } @@ -117,9 +115,15 @@ class CoverageReporter extends BaseReporter { if (!this._coverageMap.data[filename]) { try { const source = fs.readFileSync(filename).toString(); - const coverage = generateEmptyCoverage(source, filename, config); - if (coverage) { - this._coverageMap.addFileCoverage(coverage); + const result = generateEmptyCoverage(source, filename, config); + if (result) { + this._coverageMap.addFileCoverage(result.coverage); + if (result.sourceMapPath) { + this._sourceMapStore.registerURL( + filename, + result.sourceMapPath + ); + } } } catch (e) { console.error(chalk.red(` diff --git a/packages/jest-cli/src/runTest.js b/packages/jest-cli/src/runTest.js index 19027e4f679a..2589cf4bd12e 100644 --- a/packages/jest-cli/src/runTest.js +++ b/packages/jest-cli/src/runTest.js @@ -59,6 +59,7 @@ function runTest(path: Path, config: Config, resolver: Resolver) { result.perfStats = {end: Date.now(), start}; result.testFilePath = path; result.coverage = runtime.getAllCoverageInfo(); + result.sourceMaps = runtime.getSourceMapInfo(); result.console = testConsole.getBuffer(); result.skipped = testCount === result.numPendingTests; return result; diff --git a/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap b/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap index aa5094246548..c96d7f56ed11 100644 --- a/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap +++ b/packages/jest-runtime/src/__tests__/__snapshots__/transform-test.js.snap @@ -1,20 +1,3 @@ -exports[`transform does not instrument with source map if mapCoverage config option is false 1`] = ` -"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"4a81f91764b586e5729481ba491440a08d658002\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 0 }, end: { line: 1, column: 7 } } }, fnMap: {}, branchMap: {}, s: { \\"0\\": 0 }, f: {}, b: {}, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();++cov_25u22311x4.s[0];content; -}});" -`; - -exports[`transform instruments with source map if preprocessor inlines it 1`] = ` -"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"051b2f7208d7f64ba1cda9a8fec8ebb925b6a32a\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 8 }, end: { line: 1, column: 9 } } }, fnMap: {}, branchMap: {}, s: { \\"0\\": 0 }, f: {}, b: {}, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();var x = /* istanbul ignore next */(++cov_25u22311x4.s[0], 1); -;global.__coverage__['/fruits/banana.js'].inputSourceMapPath = '/cache/jest-transform-cache-test/ab/banana_ab.map'; -}});" -`; - -exports[`transform instruments with source map if preprocessor supplies it 1`] = ` -"({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"4a81f91764b586e5729481ba491440a08d658002\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 0 }, end: { line: 1, column: 7 } } }, fnMap: {}, branchMap: {}, s: { \\"0\\": 0 }, f: {}, b: {}, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();++cov_25u22311x4.s[0];content; -;global.__coverage__['/fruits/banana.js'].inputSourceMapPath = '/cache/jest-transform-cache-test/ab/banana_ab.map'; -}});" -`; - exports[`transform transforms a file properly 1`] = ` "({\\"Object.\\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"04636d4ae73b4b3e24bf6fba39e08c946fd0afb5\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 0 }, end: { line: 1, column: 26 } } }, fnMap: {}, branchMap: {}, s: { \\"0\\": 0 }, f: {}, b: {}, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();++cov_25u22311x4.s[0];module.exports = \\"banana\\"; }});" diff --git a/packages/jest-runtime/src/__tests__/instrumentation-test.js b/packages/jest-runtime/src/__tests__/instrumentation-test.js index cb8f7b230c93..e7e59d21f6f3 100644 --- a/packages/jest-runtime/src/__tests__/instrumentation-test.js +++ b/packages/jest-runtime/src/__tests__/instrumentation-test.js @@ -28,7 +28,7 @@ it('instruments files', () => { collectCoverage: true, rootDir: '/', }; - const instrumented = transform(FILE_PATH_TO_INSTRUMENT, config); + const instrumented = transform(FILE_PATH_TO_INSTRUMENT, config).script; expect(instrumented instanceof vm.Script).toBe(true); // We can't really snapshot the resulting coverage, because it depends on // absolute path of the file, which will be different on different diff --git a/packages/jest-runtime/src/__tests__/transform-test.js b/packages/jest-runtime/src/__tests__/transform-test.js index 9a363b5cc941..1e005840e673 100644 --- a/packages/jest-runtime/src/__tests__/transform-test.js +++ b/packages/jest-runtime/src/__tests__/transform-test.js @@ -156,7 +156,7 @@ describe('transform', () => { it('transforms a file properly', () => { config.collectCoverage = true; - const response = transform('/fruits/banana.js', config); + const response = transform('/fruits/banana.js', config).script; expect(response instanceof vm.Script).toBe(true); expect(vm.Script.mock.calls[0][0]).toMatchSnapshot(); @@ -166,7 +166,7 @@ describe('transform', () => { expect(fs.readFileSync).toBeCalledWith('/fruits/banana.js', 'utf8'); // in-memory cache - const response2 = transform('/fruits/banana.js', config); + const response2 = transform('/fruits/banana.js', config).script; expect(response2).toBe(response); transform('/fruits/kiwi.js', config); @@ -227,40 +227,32 @@ describe('transform', () => { }); it('instruments with source map if preprocessor supplies it', () => { - if (skipOnWindows.test()) { //snapshot has os-dependent path separators - return; - } - config = Object.assign(config, { collectCoverage: true, mapCoverage: true, transform: [['^.+\\.js$', 'preprocessor-with-sourcemaps']], }); - const sourceMap = { + const map = { mappings: ';AAAA', version: 3, }; require('preprocessor-with-sourcemaps').process.mockReturnValue({ - content: 'content', - sourceMap, + code: 'content', + map, }); - transform('/fruits/banana.js', config); - expect(vm.Script.mock.calls[0][0]).toMatchSnapshot(); + const result = transform('/fruits/banana.js', config); + expect(result.sourceMapPath).toEqual(expect.any(String)); expect(fs.writeFileSync).toBeCalledWith( - '/cache/jest-transform-cache-test/ab/banana_ab.map', - JSON.stringify(sourceMap), + result.sourceMapPath, + JSON.stringify(map), 'utf8', ); }); it('instruments with source map if preprocessor inlines it', () => { - if (skipOnWindows.test()) { //snapshot has os-dependent path separators - return; - } - config = Object.assign(config, { collectCoverage: true, mapCoverage: true, @@ -278,10 +270,10 @@ describe('transform', () => { require('preprocessor-with-sourcemaps').process.mockReturnValue(content); - transform('/fruits/banana.js', config); - expect(vm.Script.mock.calls[0][0]).toMatchSnapshot(); + const result = transform('/fruits/banana.js', config); + expect(result.sourceMapPath).toEqual(expect.any(String)); expect(fs.writeFileSync).toBeCalledWith( - '/cache/jest-transform-cache-test/ab/banana_ab.map', + result.sourceMapPath, sourceMap, 'utf8', ); @@ -304,8 +296,9 @@ describe('transform', () => { sourceMap, }); - transform('/fruits/banana.js', config); - expect(vm.Script.mock.calls[0][0]).toMatchSnapshot(); + const result = transform('/fruits/banana.js', config); + expect(result.sourceMapPath).toBeFalsy(); + expect(fs.writeFileSync).toHaveBeenCalledTimes(1); }); it('reads values from the cache', () => { diff --git a/packages/jest-runtime/src/index.js b/packages/jest-runtime/src/index.js index 0ce8af3d40a0..728d8f831c47 100644 --- a/packages/jest-runtime/src/index.js +++ b/packages/jest-runtime/src/index.js @@ -87,6 +87,7 @@ class Runtime { _shouldAutoMock: boolean; _shouldMockModuleCache: BooleanObject; _shouldUnmockTransitiveDependenciesCache: BooleanObject; + _sourceMapRegistry: {[key: string]: string}; _transitiveShouldMock: BooleanObject; _unmockList: ?RegExp; _virtualMocks: BooleanObject; @@ -99,6 +100,7 @@ class Runtime { this._moduleRegistry = Object.create(null); this._internalModuleRegistry = Object.create(null); this._mockRegistry = Object.create(null); + this._sourceMapRegistry = Object.create(null); this._config = config; this._environment = environment; this._resolver = resolver; @@ -391,6 +393,15 @@ class Runtime { return this._environment.global.__coverage__; } + getSourceMapInfo() { + return Object.keys(this._sourceMapRegistry).reduce((result, sourcePath) => { + if (fs.existsSync(this._sourceMapRegistry[sourcePath])) { + result[sourcePath] = this._sourceMapRegistry[sourcePath]; + } + return result; + }, {}); + } + setMock( from: string, moduleName: string, @@ -438,9 +449,17 @@ class Runtime { localModule.paths = this._resolver.getModulePaths(dirname); localModule.require = this._createRequireImplementation(filename, options); - const script = transform(filename, this._config, {isInternalModule}); + const transformedFile = transform( + filename, + this._config, + {isInternalModule} + ); + + if (transformedFile.sourceMapPath) { + this._sourceMapRegistry[filename] = transformedFile.sourceMapPath; + } - const wrapper = this._environment.runScript(script)[ + const wrapper = this._environment.runScript(transformedFile.script)[ transform.EVAL_RESULT_VARIABLE ]; wrapper.call( diff --git a/packages/jest-runtime/src/transform.js b/packages/jest-runtime/src/transform.js index 1c86871e6e87..88cfacd196c5 100644 --- a/packages/jest-runtime/src/transform.js +++ b/packages/jest-runtime/src/transform.js @@ -10,8 +10,11 @@ 'use strict'; import type {Config, Path} from 'types/Config'; -import type {Transformer, TransformedSource} from 'types/Transform'; - +import type { + Transformer, + TransformedSource, + BuiltTransformResult, +} from 'types/Transform'; const createDirectory = require('jest-util').createDirectory; const crypto = require('crypto'); const fileExists = require('jest-file-exists'); @@ -30,7 +33,7 @@ type Options = {| const EVAL_RESULT_VARIABLE = 'Object.'; -const cache: Map = new Map(); +const cache: Map = new Map(); const configToJsonMap = new Map(); // Cache regular expressions to test whether the file needs to be preprocessed const ignoreCache: WeakMap = new WeakMap(); @@ -262,27 +265,28 @@ const instrumentFile = ( }).code; }; -const escapePathForJavaScript = (filePath: string) => - filePath.replace(/\\/g, '\\\\'); - const transformSource = ( filename: Path, config: Config, content: string, instrument: boolean, -): string => { +) => { const transform = getTransformer(filename, config); const cacheFilePath = getFileCachePath(filename, config, content, instrument); + let sourceMapPath = cacheFilePath + '.map'; // Ignore cache if `config.cache` is set (--no-cache) - let result = config.cache ? readCacheFile(filename, cacheFilePath) : null; + let code = config.cache ? readCacheFile(filename, cacheFilePath) : null; - if (result) { - return result; + if (code) { + return { + code, + sourceMapPath, + }; } let transformed: TransformedSource = { - content, - sourceMap: null, + code: content, + map: null, }; if (transform && shouldTransform(filename, config)) { @@ -292,22 +296,22 @@ const transformSource = ( }); if (typeof processed === 'string') { - transformed.content = processed; + transformed.code = processed; } else { transformed = processed; } } if (config.mapCoverage) { - if (!transformed.sourceMap) { + if (!transformed.map) { const convert = require('convert-source-map'); - const inlineSourceMap = convert.fromSource(transformed.content); + const inlineSourceMap = convert.fromSource(transformed.code); if (inlineSourceMap) { - transformed.sourceMap = inlineSourceMap.toJSON(); + transformed.map = inlineSourceMap.toJSON(); } } } else { - transformed.sourceMap = null; + transformed.map = null; } // That means that the transform has a custom instrumentation @@ -315,24 +319,26 @@ const transformSource = ( const transformDidInstrument = transform && transform.canInstrument; if (!transformDidInstrument && instrument) { - result = instrumentFile(transformed.content, filename, config); + code = instrumentFile(transformed.code, filename, config); } else { - result = transformed.content; + code = transformed.code; } - if (instrument && transformed.sourceMap && config.mapCoverage) { - const sourceMapContent = typeof transformed.sourceMap === 'string' - ? transformed.sourceMap - : JSON.stringify(transformed.sourceMap); - const sourceMapFilePath = cacheFilePath + '.map'; - writeCacheFile(sourceMapFilePath, sourceMapContent); - result += - `\n;global.__coverage__['${escapePathForJavaScript(filename)}']` + - `.inputSourceMapPath = '${escapePathForJavaScript(sourceMapFilePath)}';`; + if (instrument && transformed.map && config.mapCoverage) { + const sourceMapContent = typeof transformed.map === 'string' + ? transformed.map + : JSON.stringify(transformed.map); + writeCacheFile(sourceMapPath, sourceMapContent); + } else { + sourceMapPath = null; } - writeCacheFile(cacheFilePath, result); - return result; + writeCacheFile(cacheFilePath, code); + + return { + code, + sourceMapPath, + }; }; const transformAndBuildScript = ( @@ -340,22 +346,33 @@ const transformAndBuildScript = ( config: Config, options: ?Options, instrument: boolean, -): vm.Script => { +): BuiltTransformResult => { const isInternalModule = !!(options && options.isInternalModule); const content = stripShebang(fs.readFileSync(filename, 'utf8')); - let wrappedResult; + let wrappedCode: string; + let sourceMapPath: ?string = null; const willTransform = !isInternalModule && (shouldTransform(filename, config) || instrument); try { if (willTransform) { - wrappedResult = - wrap(transformSource(filename, config, content, instrument)); + const transformedSource = transformSource( + filename, + config, + content, + instrument + ); + + wrappedCode = wrap(transformedSource.code); + sourceMapPath = transformedSource.sourceMapPath; } else { - wrappedResult = wrap(content); + wrappedCode = wrap(content); } - return new vm.Script(wrappedResult, {displayErrors: true, filename}); + return { + script: new vm.Script(wrappedCode, {displayErrors: true, filename}), + sourceMapPath, + }; } catch (e) { if (e.codeFrame) { e.stack = e.codeFrame; @@ -367,7 +384,7 @@ const transformAndBuildScript = ( `TRANSFORM: ${willTransform.toString()}\n` + `INSTRUMENT: ${instrument.toString()}\n` + `SOURCE:\n` + - String(wrappedResult), + String(wrappedCode), ); } @@ -379,16 +396,16 @@ module.exports = ( filename: Path, config: Config, options: Options, -): vm.Script => { +): BuiltTransformResult => { const instrument = shouldInstrument(filename, config); const scriptCacheKey = getScriptCacheKey(filename, config, instrument); - let script = cache.get(scriptCacheKey); - if (script) { - return script; + let result = cache.get(scriptCacheKey); + if (result) { + return result; } else { - script = transformAndBuildScript(filename, config, options, instrument); - cache.set(scriptCacheKey, script); - return script; + result = transformAndBuildScript(filename, config, options, instrument); + cache.set(scriptCacheKey, result); + return result; } }; diff --git a/types/TestResult.js b/types/TestResult.js index 00eacf01a77a..31805fa4c610 100644 --- a/types/TestResult.js +++ b/types/TestResult.js @@ -145,6 +145,7 @@ export type TestResult = {| unmatched: number, updated: number, |}, + sourceMaps: {[sourcePath: string]: string}, testExecError?: Error, testFilePath: string, testResults: Array, diff --git a/types/Transform.js b/types/Transform.js index c6b97e86bc7c..219d313b12ea 100644 --- a/types/Transform.js +++ b/types/Transform.js @@ -10,10 +10,16 @@ 'use strict'; import type {Config, Path} from 'types/Config'; +import type {Script} from 'vm'; export type TransformedSource = {| - content: string, - sourceMap: ?Object | string, + code: string, + map: ?Object | string, +|}; + +export type BuiltTransformResult = {| + script: Script, + sourceMapPath: ?string, |}; export type TransformOptions = {| From 254c619c5ea8da9157bd9141fbf3564581840133 Mon Sep 17 00:00:00 2001 From: Justin Bay Date: Thu, 23 Feb 2017 20:00:01 -0500 Subject: [PATCH 8/8] tweak docs, fix a test --- docs/Configuration.md | 10 +++++----- .../jest-runtime/src/__tests__/transform-test.js | 12 ++++++------ types/TestResult.js | 1 - 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 06c5e104c130..b79928832c94 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -152,18 +152,18 @@ Note that, if you specify a global reference value (like an object or array) her ### `mapCoverage` [boolean] -##### available in Jest **19.0.0+** +##### available in Jest **20.0.0+** Default: `false` -If you have [transformers](#transform-object-string-string) configured that emit source maps, Jest will use them to map code coverage against the original source code when writing [reports](#coveragereporters-array-string) and checking [thresholds](#coveragethreshold-object). This can be resource-intensive. If Jest is taking a long time to calculate coverage at the end of a test run, try setting this option to `false`. +If you have [transformers](#transform-object-string-string) configured that emit source maps, Jest will use them to try and map code coverage against the original source code when writing [reports](#coveragereporters-array-string) and checking [thresholds](#coveragethreshold-object). This is done on a best-effort basis as some compile-to-JavaScript languages may provide more accurate source maps than others. This can also be resource-intensive. If Jest is taking a long time to calculate coverage at the end of a test run, try setting this option to `false`. -Both inline source maps and source maps returned directly from a transformer are supported. Source map URLs are not supported because Jest may not be able to locate them. To return source maps from a transformer, the `process` function can return an object like the following. The sourceMap property may either be an object, or a string of JSON. +Both inline source maps and source maps returned directly from a transformer are supported. Source map URLs are not supported because Jest may not be able to locate them. To return source maps from a transformer, the `process` function can return an object like the following. The `map` property may either be the source map object, or the source map object as a JSON string. ```js return { - content: 'the code', - sourceMap: 'the source map', + code: 'the code', + map: 'the source map', }; ``` diff --git a/packages/jest-runtime/src/__tests__/transform-test.js b/packages/jest-runtime/src/__tests__/transform-test.js index 1e005840e673..23663b9f5eda 100644 --- a/packages/jest-runtime/src/__tests__/transform-test.js +++ b/packages/jest-runtime/src/__tests__/transform-test.js @@ -226,7 +226,7 @@ describe('transform', () => { expect(vm.Script.mock.calls[2][0]).toMatchSnapshot(); }); - it('instruments with source map if preprocessor supplies it', () => { + it('writes source map if preprocessor supplies it', () => { config = Object.assign(config, { collectCoverage: true, mapCoverage: true, @@ -252,7 +252,7 @@ describe('transform', () => { ); }); - it('instruments with source map if preprocessor inlines it', () => { + it('writes source map if preprocessor inlines it', () => { config = Object.assign(config, { collectCoverage: true, mapCoverage: true, @@ -279,21 +279,21 @@ describe('transform', () => { ); }); - it('does not instrument with source map if mapCoverage config option is false', () => { + it('does not write source map if mapCoverage config option is false', () => { config = Object.assign(config, { collectCoverage: true, mapCoverage: false, transform: [['^.+\\.js$', 'preprocessor-with-sourcemaps']], }); - const sourceMap = { + const map = { mappings: ';AAAA', version: 3, }; require('preprocessor-with-sourcemaps').process.mockReturnValue({ - content: 'content', - sourceMap, + code: 'content', + map, }); const result = transform('/fruits/banana.js', config); diff --git a/types/TestResult.js b/types/TestResult.js index d78ae4cf1f46..34fbfad28e45 100644 --- a/types/TestResult.js +++ b/types/TestResult.js @@ -21,7 +21,6 @@ export type RawFileCoverage = {| statementMap: { [statementId: number]: any }, branchMap: { [branchId: number]: any }, inputSourceMap?: Object, - inputSourceMapPath?: string, |}; export type RawCoverage = {