Skip to content

Commit

Permalink
feat: hook vm.runInContext (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
pangrr authored and bcoe committed Oct 15, 2017
1 parent 9c465f4 commit 9659936
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 9 deletions.
10 changes: 8 additions & 2 deletions packages/istanbul-api/lib/config.js
Expand Up @@ -39,6 +39,7 @@ function defaultConfig() {
},
hooks: {
'hook-run-in-context': false,
'hook-run-in-this-context': false,
'post-require-hook': null,
'handle-sigint': false
},
Expand Down Expand Up @@ -335,11 +336,16 @@ function HookOptions(config) {
this.config = config;
}

/**
* returns if `vm.runInContext` needs to be hooked. Used by the `cover` command.
* @method hookRunInContext
* @return {Boolean} true if `vm.runInContext` needs to be hooked for coverage
*/
/**
* returns if `vm.runInThisContext` needs to be hooked, in addition to the standard
* `require` hooks added by istanbul. This should be true for code that uses
* RequireJS for example. Used by the `cover` command.
* @method hookRunInContext
* @method hookRunInThisContext
* @return {Boolean} true if `vm.runInThisContext` needs to be hooked for coverage
*/
/**
Expand All @@ -360,7 +366,7 @@ function HookOptions(config) {
* @return {Boolean} true if SIGINT needs to be hooked to write coverage information
*/

addMethods(HookOptions, 'hookRunInContext', 'postRequireHook', 'handleSigint');
addMethods(HookOptions, 'hookRunInContext', 'hookRunInThisContext', 'postRequireHook', 'handleSigint');

/**
* represents the istanbul configuration and provides sub-objects that can
Expand Down
14 changes: 12 additions & 2 deletions packages/istanbul-api/lib/run-cover.js
Expand Up @@ -23,7 +23,12 @@ function getCoverFunctions(config, includes, callback) {
reportingDir = path.resolve(config.reporting.dir()),
reporter = new Reporter(config),
excludes = config.instrumentation.excludes(true),
coverageVar = '$$cov_' + new Date().getTime() + '$$',
// The coverage variable below should have different value than
// that of the coverage variable actually used by the instrumenter (in this case: __coverage__).
// Otherwise if you run nyc to provide coverage on these files,
// both the actual instrumenter and this file will write to the global coverage variable,
// and provide unexpected coverage result.
coverageVar = '$$coverage$$',
instOpts = config.instrumentation.getInstrumenterOpts(),
sourceMapStore = libSourceMaps.createSourceMapStore({}),
instrumenter,
Expand Down Expand Up @@ -88,14 +93,18 @@ function getCoverFunctions(config, includes, callback) {
hookFn = function (matchFn) {
var hookOpts = {
verbose: config.verbose,
extensions: config.instrumentation.extensions()
extensions: config.instrumentation.extensions(),
coverageVariable: coverageVar
};

//initialize the global variable
coverageSetterFn({});
reportInitFn();

if (config.hooks.hookRunInContext()) {
hook.hookRunInContext(matchFn, transformer, hookOpts);
}
if (config.hooks.hookRunInThisContext()) {
hook.hookRunInThisContext(matchFn, transformer, hookOpts);
}
disabler = hook.hookRequire(matchFn, requireTransformer, hookOpts);
Expand All @@ -106,6 +115,7 @@ function getCoverFunctions(config, includes, callback) {
disabler();
}
hook.unhookRunInThisContext();
hook.unhookRunInContext();
hook.unloadRequireCache(matchFn);
};

Expand Down
1 change: 1 addition & 0 deletions packages/istanbul-api/test/config.test.js
Expand Up @@ -47,6 +47,7 @@ describe('config', function () {
it('sets correct hook options', function () {
var hOpts = config.hooks;
assert.equal(hOpts.hookRunInContext(), false);
assert.equal(hOpts.hookRunInThisContext(), false);
assert.equal(hOpts.postRequireHook(), null);
reset();
});
Expand Down
2 changes: 1 addition & 1 deletion packages/istanbul-api/test/run-check-coverage.test.js
Expand Up @@ -17,7 +17,7 @@ describe('run check-coverage', function () {
var cfg = configuration.loadObject({
verbose: false,
hooks: {
'hook-run-in-context': true
'hook-run-in-this-context': true
},
instrumentation: {
root: codeRoot,
Expand Down
35 changes: 33 additions & 2 deletions packages/istanbul-api/test/run-cover.test.js
Expand Up @@ -76,12 +76,43 @@ describe('run cover', function () {
});
});

it('hooks runInThisContext and provides coverage', function (cb) {
it('hooks runInContext and provides coverage', function (cb) {
cb = wrap(cb);
var config = getConfig({
hooks: { 'hook-run-in-context': true },
instrumentation: { 'include-all-sources': false }
});
cover.getCoverFunctions(config, function(err, data) {
assert.ok(!err);
var fn = data.coverageFn,
exitFn = data.exitFn,
hookFn = data.hookFn,
coverage,
coverageMap,
otherMap;
unhookFn = data.unhookFn;
hookFn();
require('./sample-code/runInContext');
coverageMap = fn();
assert.ok(coverageMap);
coverage = coverageMap[path.resolve(codeRoot, 'foo.js')];
assert.ok(coverage);
exitFn();
assert.ok(fs.existsSync(path.resolve(outputDir, 'coverage.raw.json')));
assert.ok(fs.existsSync(path.resolve(outputDir, 'lcov.info')));
assert.ok(fs.existsSync(path.resolve(outputDir, 'lcov-report')));
otherMap = JSON.parse(fs.readFileSync(path.resolve(outputDir, 'coverage.raw.json')));
assert.deepEqual(otherMap, coverageMap);
cb();
});
});

it('hooks runInThisContext and provides coverage', function (cb) {
cb = wrap(cb);
var config = getConfig({
hooks: { 'hook-run-in-this-context': true },
instrumentation: { 'include-all-sources': false }
});
cover.getCoverFunctions(config, function(err, data) {
assert.ok(!err);
var fn = data.coverageFn,
Expand Down Expand Up @@ -146,7 +177,7 @@ describe('run cover', function () {
it('accepts specific includes', function (cb) {
cb = wrap(cb);
var config = getConfig({
hooks: { 'hook-run-in-context': true },
hooks: { 'hook-run-in-this-context': true },
instrumentation: { 'include-all-sources': false }
});
cover.getCoverFunctions(config, [ '**/foo.js' ], function (err, data) {
Expand Down
2 changes: 1 addition & 1 deletion packages/istanbul-api/test/run-reports.test.js
Expand Up @@ -19,7 +19,7 @@ describe('run reports', function () {
var cfg = configuration.loadObject({
verbose: false,
hooks: {
'hook-run-in-context': true
'hook-run-in-this-context': true
},
instrumentation: {
root: codeRoot,
Expand Down
7 changes: 7 additions & 0 deletions packages/istanbul-api/test/sample-code/runInContext.js
@@ -0,0 +1,7 @@
var vm = require('vm'),
path = require('path'),
fs = require('fs'),
file = path.resolve(__dirname, 'foo.js'),
code = fs.readFileSync(file, 'utf8');

vm.runInContext(code, vm.createContext({}), file);
42 changes: 41 additions & 1 deletion packages/istanbul-lib-hook/lib/hook.js
Expand Up @@ -6,7 +6,8 @@ var path = require('path'),
vm = require('vm'),
appendTransform = require('append-transform'),
originalCreateScript = vm.createScript,
originalRunInThisContext = vm.runInThisContext;
originalRunInThisContext = vm.runInThisContext,
originalRunInContext = vm.runInContext;

function transformFn(matcher, transformer, verbose) {

Expand Down Expand Up @@ -149,6 +150,43 @@ function hookRunInThisContext(matcher, transformer, opts) {
function unhookRunInThisContext() {
vm.runInThisContext = originalRunInThisContext;
}
/**
* hooks `vm.runInContext` to return transformed code.
* @method hookRunInContext
* @static
* @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`
* Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
* @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to
* `vm.createScript`. Should return the transformed code.
* @param opts {Object} [opts={}] options
* @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
*/
function hookRunInContext(matcher, transformer, opts) {
opts = opts || {};
var fn = transformFn(matcher, transformer, opts.verbose);
vm.runInContext = function (code, context, file) {
var ret = fn(code, file);
var coverageVariable = opts.coverageVariable || '__coverage__';
// Refer coverage variable in context to global coverage variable.
// So that coverage data will be written in global coverage variable for unit tests run in vm.runInContext.
// If all unit tests are run in vm.runInContext, no global coverage variable will be generated.
// Thus initialize a global coverage variable here.
if (!global[coverageVariable]) {
global[coverageVariable] = {};
}
context[coverageVariable] = global[coverageVariable];
return originalRunInContext(ret.code, context, file);
};

}
/**
* unhooks vm.runInContext, restoring it to its original state.
* @method unhookRunInContext
* @static
*/
function unhookRunInContext() {
vm.runInContext = originalRunInContext;
}
/**
* istanbul-lib-hook provides mechanisms to transform code in the scope of `require`,
* `vm.createScript`, `vm.runInThisContext` etc.
Expand Down Expand Up @@ -177,6 +215,8 @@ module.exports = {
unhookCreateScript: unhookCreateScript,
hookRunInThisContext : hookRunInThisContext,
unhookRunInThisContext : unhookRunInThisContext,
hookRunInContext : hookRunInContext,
unhookRunInContext : unhookRunInContext,
unloadRequireCache: unloadRequireCache
};

Expand Down
34 changes: 34 additions & 0 deletions packages/istanbul-lib-hook/test/hook.test.js
Expand Up @@ -165,4 +165,38 @@ describe('hooks', function () {
hook.unhookCreateScript();
});
});
describe('runInContext', function () {
beforeEach(function () {
currentHook = require('vm').runInContext;
});
afterEach(function () {
require('vm').runInContext = currentHook;
});
it('transforms foo', function () {
var s;
var vm = require('vm');
hook.hookRunInContext(matcher, scriptTransformer);
s = vm.runInContext('(function () { return 10; }());', vm.createContext({}), '/bar/foo.js');
assert.equal(s, 42);
hook.unhookRunInContext();
s = vm.runInContext('(function () { return 10; }());', vm.createContext({}), '/bar/foo.js');
assert.equal(s, 10);
});
it('does not transform code with no filename', function () {
var s;
var vm = require('vm');
hook.hookRunInContext(matcher, scriptTransformer);
s = vm.runInContext('(function () { return 10; }());', vm.createContext({}));
assert.equal(s, 10);
hook.unhookRunInContext();
});
it('does not transform code with non-string filename', function () {
var s;
var vm = require('vm');
hook.hookRunInContext(matcher, scriptTransformer);
s = vm.runInContext('(function () { return 10; }());', vm.createContext({}), {});
assert.equal(s, 10);
hook.unhookRunInContext();
});
});
});
1 change: 1 addition & 0 deletions packages/istanbul-lib-hook/test/index.test.js
Expand Up @@ -7,5 +7,6 @@ describe('external interface', function () {
it('exports the correct interface', function () {
assert.ok(index.hookRequire);
assert.ok(index.hookRunInThisContext);
assert.ok(index.hookRunInContext);
});
});

0 comments on commit 9659936

Please sign in to comment.