Skip to content

Commit

Permalink
Instrumenting preprocessors (jestjs#1336)
Browse files Browse the repository at this point in the history
* Instrumenting preprocessors

* Address Comments
  • Loading branch information
aaronabramov authored and cpojer committed Aug 2, 2016
1 parent a77f88c commit 6760c6c
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 30 deletions.
12 changes: 12 additions & 0 deletions integration_tests/__tests__/__snapshots__/transform-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ All files | 83.33 | 100 | 50 | 83.33 |
"
`;
exports[`custom preprocessor instruments files 1`] = `
"Using <<REPLACED>>, jasmine2
----------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
----------|----------|----------|----------|----------|----------------|
All files | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|----------------|
"
`;
exports[`no babel-jest instrumentation with no babel-jest 1`] = `
"Using <<REPLACED>>, jasmine2
----------------------------|----------|----------|----------|----------|----------------|
Expand Down
24 changes: 24 additions & 0 deletions integration_tests/__tests__/transform-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,27 @@ describe('no babel-jest', () => {
expect(stripJestVersion(stdout)).toMatchSnapshot();
});
});

describe('custom preprocessor', () => {
const dir = path.resolve(
__dirname,
'..',
'transform/custom-instrumenting-preprocessor'
);

it('proprocesses files', () => {
const {json, stderr} = runJest.json(dir, ['--no-cache']);
expect(stderr).toMatch(/FAIL/);
expect(stderr).toMatch(/instruments by setting.*global\.__INSTRUMENTED__/);
expect(json.numTotalTests).toBe(2);
expect(json.numPassedTests).toBe(1);
expect(json.numFailedTests).toBe(1);
});

it('instruments files', () => {
const {stdout, status} = runJest(dir, ['--no-cache', '--coverage']);
// coverage should be empty (100%) because there's no real instrumentation
expect(stripJestVersion(stdout)).toMatchSnapshot();
expect(status).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* 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';


require('../src');

it('instruments by setting global.__INSTRUMENTED__', () => {
expect(global.__INSTRUMENTED__).toBe(true);
});

it('preprocesses by setting global.__PREPROCESSED__', () => {
expect(global.__PREPROCESSED__).toBe(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"jest": {
"scriptPreprocessor": "<rootDir>/preprocessor.js"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* 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';

module.exports = {
INSTRUMENTS: true,
process(src, filename, config, options) {
src = `${src};\nglobal.__PREPROCESSED__ = true;`;

if (options.instrument) {
src = `${src};\nglobal.__INSTRUMENTED__ = true;`;
}

return src;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* 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';

module.exports = {a: 1};
25 changes: 21 additions & 4 deletions packages/babel-jest/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/

'use strict';

import type {Config, Path} from 'types/Config';
import type {PreprocessorOptions} from 'types/Preprocessor';

const babel = require('babel-core');
const jestPreset = require('babel-preset-jest');

const createTransformer = options => {
const createTransformer = (options: any) => {
options = Object.assign({}, options, {
auxiliaryCommentBefore: ' istanbul ignore next ',
presets: ((options && options.presets) || []).concat([jestPreset]),
Expand All @@ -20,11 +25,23 @@ const createTransformer = options => {
delete options.cacheDirectory;

return {
process(src, filename) {
INSTRUMENTS: true,
process(
src: string,
filename: Path,
config: Config,
preprocessorOptions: PreprocessorOptions,
): string {
let plugins = options.plugins || [];

if (preprocessorOptions.instrument) {
plugins = plugins.concat(require('babel-plugin-istanbul').default);
}

if (babel.util.canCompile(filename)) {
return babel.transform(
src,
Object.assign({}, options, {filename}),
Object.assign({}, options, {filename, plugins}),
).code;
}
return src;
Expand All @@ -33,4 +50,4 @@ const createTransformer = options => {
};

module.exports = createTransformer();
module.exports.createTransformer = createTransformer;
(module.exports: any).createTransformer = createTransformer;
4 changes: 3 additions & 1 deletion packages/jest-config/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/

'use strict';
Expand All @@ -14,7 +16,7 @@ const normalize = require('./normalize');
const path = require('path');
const setFromArgv = require('./setFromArgv');

function readConfig(argv, packageRoot) {
function readConfig(argv: any, packageRoot: string) {
return readRawConfig(argv, packageRoot)
.then(config => Object.freeze(setFromArgv(config, argv)));
}
Expand Down
58 changes: 33 additions & 25 deletions packages/jest-runtime/src/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
'use strict';

import type {Config, Path} from 'types/Config';
import type {Preprocessor} from 'types/Preprocessor';

const createDirectory = require('jest-util').createDirectory;
const crypto = require('crypto');
Expand All @@ -22,12 +23,8 @@ const vm = require('vm');

const VERSION = require('../package.json').version;

export type Processor = {
process: (sourceText: string, sourcePath: Path) => string,
};

type Options = {
isInternalModule: boolean
isInternalModule: boolean,
};

const EVAL_RESULT_VARIABLE = 'Object.<anonymous>';
Expand All @@ -47,6 +44,7 @@ const getCacheKey = (
fileData: string,
filePath: Path,
config: Config,
instrument: boolean,
): string => {
if (!configToJsonMap.has(config)) {
// We only need this set of config options that can likely influence
Expand All @@ -65,15 +63,15 @@ const getCacheKey = (
testRegex: config.testRegex,
}));
}
const configStr = configToJsonMap.get(config) || '';
const confStr = configToJsonMap.get(config) || '';
const preprocessor = getPreprocessor(config);

if (preprocessor && typeof preprocessor.getCacheKey === 'function') {
return preprocessor.getCacheKey(fileData, filePath, configStr);
return preprocessor.getCacheKey(fileData, filePath, confStr, {instrument});
} else {
return crypto.createHash('md5')
.update(fileData)
.update(configStr)
.update(confStr)
.digest('hex');
}
};
Expand Down Expand Up @@ -116,10 +114,10 @@ const readCacheFile = (filePath: Path, cachePath: Path): ?string => {
return fileData;
};

const getScriptCacheKey = (filename, config) => {
const getScriptCacheKey = (filename, config, instrument: boolean) => {
const mtime = fs.statSync(filename).mtime;
return filename + '_' + mtime.getTime() +
(shouldInstrument(filename, config) ? '_instrumented' : '');
(instrument ? '_instrumented' : '');
};

const shouldPreprocess = (filename: Path, config: Config): boolean => {
Expand Down Expand Up @@ -178,13 +176,14 @@ const getFileCachePath = (
filename: Path,
config: Config,
content: string,
instrument: boolean,
): Path => {
const baseCacheDir = getCacheFilePath(
config.cacheDirectory,
'jest-transform-cache-' + config.name,
VERSION,
);
const cacheKey = getCacheKey(content, filename, config);
const cacheKey = getCacheKey(content, filename, config, instrument);
// Create sub folders based on the cacheKey to avoid creating one
// directory with many files.
const cacheDir = path.join(baseCacheDir, cacheKey[0] + cacheKey[1]);
Expand All @@ -199,7 +198,7 @@ const getFileCachePath = (

const preprocessorCache = new WeakMap();

const getPreprocessor = (config: Config): ?Processor => {
const getPreprocessor = (config: Config): ?Preprocessor => {
if (preprocessorCache.has(config)) {
return preprocessorCache.get(config);
} else {
Expand Down Expand Up @@ -230,7 +229,7 @@ const stripShebang = content => {
}
};

const instrument = (content: string, filename: Path): string => {
const instrumentFile = (content: string, filename: Path): string => {
// NOTE: Keeping these requires inside this function reduces a single run
// time by 2sec if not running in `--coverage` mode
const babel = require('babel-core');
Expand Down Expand Up @@ -260,10 +259,11 @@ const instrument = (content: string, filename: Path): string => {
const cachedTransformAndWrap = (
filename: Path,
config: Config,
content: string
) => {
content: string,
instrument: boolean,
): string => {
const preprocessor = getPreprocessor(config);
const cacheFilePath = getFileCachePath(filename, config, content);
const cacheFilePath = getFileCachePath(filename, config, content, instrument);
// Ignore cache if `config.cache` is set (--no-cache)
let result = config.cache ? readCacheFile(filename, cacheFilePath) : null;

Expand All @@ -274,10 +274,15 @@ const cachedTransformAndWrap = (
result = content;

if (preprocessor && shouldPreprocess(filename, config)) {
result = preprocessor.process(result, filename, config);
result = preprocessor.process(result, filename, config, {instrument});
}
if (shouldInstrument(filename, config)) {
result = instrument(result, filename, config);

// That means that the preprocessor has a custom instrumentation
// logic and will handle it based on `config.collectCoverage` option
const preprocessorWillInstrument = preprocessor && preprocessor.INSTRUMENTS;

if (!preprocessorWillInstrument && instrument) {
result = instrumentFile(result, filename, config);
}

result = wrap(result);
Expand All @@ -288,17 +293,19 @@ const cachedTransformAndWrap = (
const transformAndBuildScript = (
filename: Path,
config: Config,
options: ?Options,
options?: Options,
instrument: boolean,
): vm.Script => {
const isInternalModule = !!(options && options.isInternalModule);
const content = stripShebang(fs.readFileSync(filename, 'utf8'));
let wrappedResult;

if (
!isInternalModule &&
(shouldPreprocess(filename, config) || shouldInstrument(filename, config))
(shouldPreprocess(filename, config) || instrument)
) {
wrappedResult = cachedTransformAndWrap(filename, config, content);
wrappedResult =
cachedTransformAndWrap(filename, config, content, instrument);
} else {
wrappedResult = wrap(content);
}
Expand All @@ -309,14 +316,15 @@ const transformAndBuildScript = (
module.exports = (
filename: Path,
config: Config,
options: ?Options,
options?: Options,
): vm.Script => {
const scriptCacheKey = getScriptCacheKey(filename, config);
const instrument = shouldInstrument(filename, config);
const scriptCacheKey = getScriptCacheKey(filename, config, instrument);
let script = cache.get(scriptCacheKey);
if (script) {
return script;
} else {
script = transformAndBuildScript(filename, config, options);
script = transformAndBuildScript(filename, config, options, instrument);
cache.set(scriptCacheKey, script);
return script;
}
Expand Down
34 changes: 34 additions & 0 deletions types/Preprocessor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';

import type {Config, Path} from 'types/Config';

export type PreprocessorOptions = {
instrument: boolean,
};

export type Preprocessor = {
INSTRUMENTS?: boolean,

getCacheKey: (
fileData: string,
filePath: Path,
configStr: string,
options: PreprocessorOptions,
) => string,

process: (
sourceText: string,
sourcePath: Path,
config: Config,
options?: PreprocessorOptions,
) => string,
};

0 comments on commit 6760c6c

Please sign in to comment.