These are some notes I made while reviewing hypothesis/client#156 to understand how Istanbul works
Istanbul instruments code in order to generate code coverage metrics for tests by adding code to record lines, statements etc. that are executed.
It adds a global __coverage__
variable to the generated code which is a map from file path
to coverage information. The code for each module is then augmented with:
-
A header which creates an entry in the
__coverage__
map containing a set of hit counters for functions, branches and statements and mappings of functions/branches/statements back to their locations in the original source -
Statements added around each statement or expression from the original code which increment the appropriate hit counter just before a function, branch or statement is executed.
See the coverage.json docs for details on the format of the __coverage__
map.
When using Karma + Browserify, the generated code is written to a ".browserify" file in the "/tmp" directory. When running the tests in "watch" mode using "gulp test-watch", the name of this bundle is printed whenever a source file is changed after the first run of the tests.
As long as the test runner is running, you can open this file in a text editor to see what the generated code looks like with instrumentation added.
Example, foo.coffee:
class Foo
constructor: (arg) ->
console.log 'Foo Coffee', Foo.toString()
if arg
@arg = arg
else
throw new Error('arg missing')
module.exports = Foo
Instrumented code, using the "isparta" instrumenter:
[function(require,module,exports){
var __cov_LmQeoDnoeGOnxVJ9q82G5Q = (Function('return this'))();
if (!__cov_LmQeoDnoeGOnxVJ9q82G5Q.__coverage__) { __cov_LmQeoDnoeGOnxVJ9q82G5Q.__coverage__ = {}; }
__cov_LmQeoDnoeGOnxVJ9q82G5Q = __cov_LmQeoDnoeGOnxVJ9q82G5Q.__coverage__;
if (!(__cov_LmQeoDnoeGOnxVJ9q82G5Q['/home/robert/hypothesis/repos/client/h/static/scripts/annotator/foo.coffee'])) {
__cov_LmQeoDnoeGOnxVJ9q82G5Q['/home/robert/hypothesis/repos/client/h/static/scripts/annotator/foo.coffee'] = {"path":"/home/robert/hypothesis/repos/client/h/static/scripts/annotator/foo.coffee","s":{"1":0,"2":0,"3":1,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0},"b":{"1":[0,0]},"f":{"1":0,"2":0},"fnMap":{"1":{"name":"(anonymous_1)","line":3,"loc":{"start":{"line":1,"column":-9},"end":{"line":1,"column":-9}}},"2":{"name":"Foo","line":4,"loc":{"start":{"line":2,"column":15},"end":{"line":2,"column":15}}}},"statementMap":{"1":{"start":{"line":1,"column":-15},"end":{"line":1,"column":-15}},"2":{"start":{"line":0,"column":0},"end":{"line":0,"column":0},"skip":true},"3":{"start":{"line":0,"column":0},"end":{"line":0,"column":0},"skip":true},"4":{"start":{"line":3,"column":4},"end":{"line":3,"column":4}},"5":{"start":{"line":4,"column":4},"end":{"line":2,"column":15}},"6":{"start":{"line":5,"column":6},"end":{"line":4,"column":4}},"7":{"start":{"line":0,"column":0},"end":{"line":0,"column":0},"skip":true},"8":{"start":{"line":0,"column":0},"end":{"line":0,"column":0},"skip":true},"9":{"start":{"line":9,"column":0},"end":{"line":9,"column":17}}},"branchMap":{"1":{"line":6,"type":"if","locations":[{"start":{"line":4,"column":4},"end":{"line":4,"column":4}},{"start":{"line":4,"column":4},"end":{"line":4,"column":4}}]}}};
}
__cov_LmQeoDnoeGOnxVJ9q82G5Q = __cov_LmQeoDnoeGOnxVJ9q82G5Q['/home/robert/hypothesis/repos/client/h/static/scripts/annotator/foo.coffee'];
__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['1']++;var Foo;__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['2']++;Foo=function(){__cov_LmQeoDnoeGOnxVJ9q82G5Q.f['1']++;function Foo(arg){__cov_LmQeoDnoeGOnxVJ9q82G5Q.f['2']++;__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['4']++;console.log('Foo Coffee',Foo.toString());__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['5']++;if(arg){__cov_LmQeoDnoeGOnxVJ9q82G5Q.b['1'][0]++;__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['6']++;this.arg=arg;}else{__cov_LmQeoDnoeGOnxVJ9q82G5Q.b['1'][1]++;__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['7']++;throw new Error('arg missing');}}__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['8']++;return Foo;}();__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['9']++;module.exports=Foo;
},{}]
Prettifid version of the __coverage__
object:
{
"path": "/home/robert/hypothesis/repos/client/h/static/scripts/annotator/foo.coffee",
"s": {
"1": 0,
"2": 0,
"3": 1,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"8": 0,
"9": 0
},
"b": {
"1": [
0,
0
]
},
"f": {
"1": 0,
"2": 0
},
"fnMap": {
"1": {
"name": "(anonymous_1)",
"line": 3,
"loc": {
"start": {
"line": 1,
"column": -9
},
"end": {
"line": 1,
"column": -9
}
}
},
"2": {
"name": "Foo",
"line": 4,
"loc": {
"start": {
"line": 2,
"column": 15
},
"end": {
"line": 2,
"column": 15
}
}
}
},
"statementMap": {
"1": {
"start": {
"line": 1,
"column": -15
},
"end": {
"line": 1,
"column": -15
}
},
"2": {
"start": {
"line": 0,
"column": 0
},
"end": {
"line": 0,
"column": 0
},
"skip": true
},
"3": {
"start": {
"line": 0,
"column": 0
},
"end": {
"line": 0,
"column": 0
},
"skip": true
},
"4": {
"start": {
"line": 3,
"column": 4
},
"end": {
"line": 3,
"column": 4
}
},
"5": {
"start": {
"line": 4,
"column": 4
},
"end": {
"line": 2,
"column": 15
}
},
"6": {
"start": {
"line": 5,
"column": 6
},
"end": {
"line": 4,
"column": 4
}
},
"7": {
"start": {
"line": 0,
"column": 0
},
"end": {
"line": 0,
"column": 0
},
"skip": true
},
"8": {
"start": {
"line": 0,
"column": 0
},
"end": {
"line": 0,
"column": 0
},
"skip": true
},
"9": {
"start": {
"line": 9,
"column": 0
},
"end": {
"line": 9,
"column": 17
}
}
},
"branchMap": {
"1": {
"line": 6,
"type": "if",
"locations": [
{
"start": {
"line": 4,
"column": 4
},
"end": {
"line": 4,
"column": 4
}
},
{
"start": {
"line": 4,
"column": 4
},
"end": {
"line": 4,
"column": 4
}
}
]
}
}
}
Prettified version of the instrumented code:
__cov__.s['1']++;
var Foo;
__cov__.s['2']++;
Foo = function() {
__cov__.f['1']++;
function Foo(arg) {
__cov__.f['2']++;
__cov__.s['4']++;
console.log('Foo Coffee', Foo.toString());
__cov__.s['5']++;
if (arg) {
__cov__.b['1'][0]++;
__cov__.s['6']++;
this.arg = arg;
} else {
__cov__.b['1'][1]++;
__cov__.s['7']++;
throw new Error('arg missing');
}
}
__cov__.s['8']++;
return Foo;
}();
__cov__.s['9']++;
Istanbul allows overriding the instrumenter which gets fed the original code and outputs the instrumented code. The default instrumenter is replaced with isparta to generate correct function/source/statement location maps for CoffeeScript files, where the location in the generated code is different from the original code. Isparta was originally written for use with the Babel ES2015+ transpiler.
Without using "isparta", the generated maps from function/statement/branch number to original source file and line refer to the location in the JS code output by the CoffeeScript compiler. With "isparta", the generated maps refer to the location in the original CoffeeScript code.
Even though "isparta" is not a CoffeeScript processing tool, it works by subclassing the Istanbul instrumenter and overriding functions that transform the statement/function/branch location maps. The transformers convert locations in the generated code back to locations in the original code by reading the source maps. These source-maps are generated in the same format by all transpilers (Babel, CoffeeScript, TypeScript etc.) so it enables code coverage reporting for CoffeeScript even though it was designed for use with Babel.