diff --git a/packages/istanbul-api/lib/config.js b/packages/istanbul-api/lib/config.js index ec69c72b..d0cc833b 100644 --- a/packages/istanbul-api/lib/config.js +++ b/packages/istanbul-api/lib/config.js @@ -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 }, @@ -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 */ /** @@ -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 diff --git a/packages/istanbul-api/lib/run-cover.js b/packages/istanbul-api/lib/run-cover.js index 53f21947..f61e18e3 100644 --- a/packages/istanbul-api/lib/run-cover.js +++ b/packages/istanbul-api/lib/run-cover.js @@ -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, @@ -88,7 +93,8 @@ 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 @@ -96,6 +102,9 @@ function getCoverFunctions(config, includes, callback) { 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); @@ -106,6 +115,7 @@ function getCoverFunctions(config, includes, callback) { disabler(); } hook.unhookRunInThisContext(); + hook.unhookRunInContext(); hook.unloadRequireCache(matchFn); }; diff --git a/packages/istanbul-api/test/config.test.js b/packages/istanbul-api/test/config.test.js index f8bfeaf5..b5ccdd86 100644 --- a/packages/istanbul-api/test/config.test.js +++ b/packages/istanbul-api/test/config.test.js @@ -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(); }); diff --git a/packages/istanbul-api/test/run-check-coverage.test.js b/packages/istanbul-api/test/run-check-coverage.test.js index bb3f7085..a9aa4130 100644 --- a/packages/istanbul-api/test/run-check-coverage.test.js +++ b/packages/istanbul-api/test/run-check-coverage.test.js @@ -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, diff --git a/packages/istanbul-api/test/run-cover.test.js b/packages/istanbul-api/test/run-cover.test.js index a6a12586..4c4fbf30 100644 --- a/packages/istanbul-api/test/run-cover.test.js +++ b/packages/istanbul-api/test/run-cover.test.js @@ -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, @@ -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) { diff --git a/packages/istanbul-api/test/run-reports.test.js b/packages/istanbul-api/test/run-reports.test.js index 3d56592a..29151030 100644 --- a/packages/istanbul-api/test/run-reports.test.js +++ b/packages/istanbul-api/test/run-reports.test.js @@ -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, diff --git a/packages/istanbul-api/test/sample-code/runInContext.js b/packages/istanbul-api/test/sample-code/runInContext.js new file mode 100644 index 00000000..d930e3ab --- /dev/null +++ b/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); diff --git a/packages/istanbul-lib-hook/lib/hook.js b/packages/istanbul-lib-hook/lib/hook.js index ff1119d8..df3162fa 100644 --- a/packages/istanbul-lib-hook/lib/hook.js +++ b/packages/istanbul-lib-hook/lib/hook.js @@ -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) { @@ -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. @@ -177,6 +215,8 @@ module.exports = { unhookCreateScript: unhookCreateScript, hookRunInThisContext : hookRunInThisContext, unhookRunInThisContext : unhookRunInThisContext, + hookRunInContext : hookRunInContext, + unhookRunInContext : unhookRunInContext, unloadRequireCache: unloadRequireCache }; diff --git a/packages/istanbul-lib-hook/test/hook.test.js b/packages/istanbul-lib-hook/test/hook.test.js index 9d326606..e8d5e75c 100644 --- a/packages/istanbul-lib-hook/test/hook.test.js +++ b/packages/istanbul-lib-hook/test/hook.test.js @@ -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(); + }); + }); }); \ No newline at end of file diff --git a/packages/istanbul-lib-hook/test/index.test.js b/packages/istanbul-lib-hook/test/index.test.js index 0065bd9b..b1aa4458 100644 --- a/packages/istanbul-lib-hook/test/index.test.js +++ b/packages/istanbul-lib-hook/test/index.test.js @@ -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); }); });