Skip to content

Commit

Permalink
Include input source map in file coverage
Browse files Browse the repository at this point in the history
Add an option to include an input source map. The input source map
is the source map that maps the code before its instumentation back it's
original form. This is needed to support more complex setups, e.g. when
bundlers are involved. If a bundler like webpack, browserify - or you
name it - is used, then there are several, intermediate, source maps
involved. The main issue is, that the instrumented code is bundled
into a single file, and therefore a new source map is created. But the
source map of the bundled file cannot be used to remap the istanbul coverage.
Therefore, these intermediate source maps need to be stored to be available
when creating the report or remaping.

This solution is kind of hacky and should start a discussion how to solve
it best. Anyway, it fixes variouis issues that, up to now, required hacky
solutions.

refs SitePen/remap-istanbul#2
  • Loading branch information
Micha Reiser committed Nov 7, 2016
1 parent 71134b6 commit 8d70de9
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 11 deletions.
10 changes: 9 additions & 1 deletion api.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ is supported. To instrument ES6 modules, make sure that you set the

- `code` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** the code to instrument
- `filename` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** the filename against which to track coverage.
- `inputSourceMap` **[object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** the source map that maps the not instrumented code back to it's original form.
Is assigned to the coverage object and therefore, is available in the json output and can be used to remap the
coverage to the untranspiled source.

Returns **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** the instrumented code.

Expand All @@ -53,6 +56,9 @@ the callback will be called in the same process tick and is not asynchronous.
- `code` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** the code to instrument
- `filename` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** the filename against which to track coverage.
- `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** the callback
- `inputSourceMap` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** the source map that maps the not instrumented code back to it's original form.
Is assigned to the coverage object and therefore, is available in the json output and can be used to remap the
coverage to the untranspiled source.

## lastFileCoverage

Expand Down Expand Up @@ -84,5 +90,7 @@ The exit function returns an object that currently has the following keys:

- `types` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** an instance of babel-types
- `sourceFilePath` **\[[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)](default 'unknown.js')** the path to source file
- `opts` **\[[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)](default {coverageVariable: '\_\_coverage\_\_'})** additional options
- `opts` **\[[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)](default {coverageVariable: '\_\_coverage\_\_', inputSourceMap: undefined })** additional options
- `opts.coverageVariable` **\[[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)]** the global coverage variable name. (optional, default `__coverage__`)
- `opts.inputSourceMap` **\[[object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)]** the input source map, that maps the uninstrumented code back to the
original code. (optional, default `undefined`)
15 changes: 11 additions & 4 deletions src/instrumenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,12 @@ class Instrumenter {
*
* @param {string} code - the code to instrument
* @param {string} filename - the filename against which to track coverage.
* @param {object} [inputSourceMap] - the source map that maps the not instrumented code back to it's original form.
* Is assigned to the coverage object and therefore, is available in the json output and can be used to remap the
* coverage to the untranspiled source.
* @returns {string} the instrumented code.
*/
instrumentSync(code, filename) {
instrumentSync(code, filename, inputSourceMap) {
if (typeof code !== 'string') {
throw new Error('Code must be a string');
}
Expand All @@ -80,7 +83,8 @@ class Instrumenter {
sourceType: opts.esModules ? "module" : "script"
});
const ee = programVisitor(t, filename, {
coverageVariable: opts.coverageVariable
coverageVariable: opts.coverageVariable,
inputSourceMap: inputSourceMap
});
let output = {};
const visitor = {
Expand Down Expand Up @@ -115,14 +119,17 @@ class Instrumenter {
* @param {string} code - the code to instrument
* @param {string} filename - the filename against which to track coverage.
* @param {Function} callback - the callback
* @param {Object} inputSourceMap - the source map that maps the not instrumented code back to it's original form.
* Is assigned to the coverage object and therefore, is available in the json output and can be used to remap the
* coverage to the untranspiled source.
*/
instrument(code, filename, callback) {
instrument(code, filename, callback, inputSourceMap) {
if (!callback && typeof filename === 'function') {
callback = filename;
filename = null;
}
try {
var out = this.instrumentSync(code, filename);
var out = this.instrumentSync(code, filename, inputSourceMap);
callback(null, out);
} catch (ex) {
callback(ex);
Expand Down
9 changes: 9 additions & 0 deletions src/source-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ class SourceCoverage extends classes.FileCoverage {
return counts.length - 1;
}

/**
* Assigns an input source map to the coverage that can be used
* to remap the coverage output to the original source
* @param sourceMap {object} the source map
*/
inputSourceMap(sourceMap) {
this.data.inputSourceMap = sourceMap;
}

freeze() {
// prune empty branches
var map = this.data.branchMap,
Expand Down
12 changes: 9 additions & 3 deletions src/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ function genVar(filename) {
// VisitState holds the state of the visitor, provides helper functions
// and is the `this` for the individual coverage visitors.
class VisitState {
constructor(types, sourceFilePath) {
constructor(types, sourceFilePath, inputSourceMap) {
this.varName = genVar(sourceFilePath);
this.attrs = {};
this.nextIgnore = null;
this.cov = new SourceCoverage(sourceFilePath);

if (typeof (inputSourceMap) !== "undefined") {
this.cov.inputSourceMap(inputSourceMap);
}
this.types = types;
this.sourceMappingURL = null;
}
Expand Down Expand Up @@ -460,10 +464,12 @@ const coverageTemplate = template(`
* @param {string} sourceFilePath - the path to source file
* @param {Object} opts - additional options
* @param {string} [opts.coverageVariable=__coverage__] the global coverage variable name.
* @param {object} [opts.inputSourceMap=undefined] the input source map, that maps the uninstrumented code back to the
* original code.
*/
function programVisitor(types, sourceFilePath = 'unknown.js', opts = {coverageVariable: '__coverage__'}) {
function programVisitor(types, sourceFilePath = 'unknown.js', opts = {coverageVariable: '__coverage__', inputSourceMap: undefined }) {
const T = types;
const visitState = new VisitState(types, sourceFilePath);
const visitState = new VisitState(types, sourceFilePath, opts.inputSourceMap);
return {
enter(path) {
path.traverse(codeVisitor, visitState);
Expand Down
2 changes: 1 addition & 1 deletion test/specs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function generateTests(docs) {
(doc.tests || []).forEach(function (t) {
var fn = function () {
var genOnly = (doc.opts || {}).generateOnly,
v = verifier.create(doc.code, doc.opts || {}, doc.instrumentOpts),
v = verifier.create(doc.code, doc.opts || {}, doc.instrumentOpts, doc.inputSourceMap),
test = clone(t),
args = test.args,
out = test.out;
Expand Down
24 changes: 24 additions & 0 deletions test/specs/input-source-map.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
name: defined input source map
code: |
output = "test"
inputSourceMap: { file: "test.js", mappings: "", names: [], sourceRoot: undefined, sources: [ "test.js" ], sourcesContent: [ 'output = "test"' ], version: 3 }
tests:
- name: sets the input source map
args: []
out: "test"
lines: { '1': 1 }
statements: { '1': 1 }
inputSourceMap: { file: "test.js", mappings: "", names: [], sourceRoot: undefined, sources: [ "test.js" ], sourcesContent: [ 'output = "test"' ], version: 3 }
---
name: without input source map
code: |
output = "test"
inputSourceMap: undefined
tests:
- name: is not set on the coverage object
args: []
out: "test"
lines: { '1': 1 }
statements: { '1': 1 }
inputSourceMap: undefined
5 changes: 3 additions & 2 deletions test/util/verifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Verifier {
assert.deepEqual(cov.f, expectedCoverage.functions || {}, 'Function coverage mismatch');
assert.deepEqual(cov.b, expectedCoverage.branches || {}, 'Branch coverage mismatch');
assert.deepEqual(cov.s, expectedCoverage.statements || {}, 'Statement coverage mismatch');
assert.deepEqual(cov.data.inputSourceMap, expectedCoverage.inputSourceMap || undefined, "Input source map mismatch");
}

getCoverage() {
Expand Down Expand Up @@ -75,7 +76,7 @@ function extractTestOption(opts, name, defaultValue) {
return v;
}

function create(code, opts, instrumenterOpts) {
function create(code, opts, instrumenterOpts, inputSourceMap) {

opts = opts || {};
instrumenterOpts = instrumenterOpts || {};
Expand All @@ -97,7 +98,7 @@ function create(code, opts, instrumenterOpts) {
}
instrumenter = new Instrumenter(instrumenterOpts);
try {
instrumenterOutput = instrumenter.instrumentSync(code, file);
instrumenterOutput = instrumenter.instrumentSync(code, file, inputSourceMap);
if (debug) {
console.log('================== Original ============================================');
console.log(annotatedCode(code));
Expand Down

0 comments on commit 8d70de9

Please sign in to comment.