From ecbde2083e384c1956f300a3d2dbebbb161f7fc3 Mon Sep 17 00:00:00 2001 From: Justin Bay Date: Thu, 16 Feb 2017 00:12:48 -0500 Subject: [PATCH] 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 = {|