blanket
Version:
seamless js code coverage
349 lines (336 loc) • 15.3 kB
JavaScript
var inBrowser = typeof window !== 'undefined' && this === window;
var parseAndModify = (inBrowser ? window.falafel : require("falafel"));
(inBrowser ? window : exports).blanket = (function(){
var linesToAddTracking = [
"ExpressionStatement",
"BreakStatement" ,
"ContinueStatement" ,
"VariableDeclaration",
"ReturnStatement" ,
"ThrowStatement" ,
"TryStatement" ,
"FunctionDeclaration" ,
"IfStatement" ,
"WhileStatement" ,
"DoWhileStatement" ,
"ForStatement" ,
"ForInStatement" ,
"SwitchStatement" ,
"WithStatement"
],
linesToAddBrackets = [
"IfStatement" ,
"WhileStatement" ,
"DoWhileStatement" ,
"ForStatement" ,
"ForInStatement" ,
"WithStatement"
],
__blanket,
copynumber = Math.floor(Math.random()*1000),
coverageInfo = {},options = {
reporter: null,
adapter:null,
filter: null,
customVariable: null,
loader: null,
ignoreScriptError: false,
existingRequireJS:false,
autoStart: false,
timeout: 180,
ignoreCors: false,
branchTracking: false,
sourceURL: false,
debug:false,
engineOnly:false,
testReadyCallback:null,
commonJS:false,
instrumentCache:false,
modulePattern: null,
ecmaVersion: 5
};
if (inBrowser && typeof window.blanket !== 'undefined'){
__blanket = window.blanket.noConflict();
}
_blanket = {
noConflict: function(){
if (__blanket){
return __blanket;
}
return _blanket;
},
_getCopyNumber: function(){
//internal method
//for differentiating between instances
return copynumber;
},
extend: function(obj) {
//borrowed from underscore
_blanket._extend(_blanket,obj);
},
_extend: function(dest,source){
if (source) {
for (var prop in source) {
if ( dest[prop] instanceof Object && typeof dest[prop] !== "function"){
_blanket._extend(dest[prop],source[prop]);
}else{
dest[prop] = source[prop];
}
}
}
},
getCovVar: function(){
var opt = _blanket.options("customVariable");
if (opt){
if (_blanket.options("debug")) {console.log("BLANKET-Using custom tracking variable:",opt);}
return inBrowser ? "window."+opt : opt;
}
return inBrowser ? "window._$blanket" : "_$jscoverage";
},
options: function(key,value){
if (typeof key !== "string"){
_blanket._extend(options,key);
}else if (typeof value === 'undefined'){
return options[key];
}else{
options[key]=value;
}
},
// instrument the file synchronously
// `next` is optional callback which will be called
// with instrumented code when present
instrumentSync: function(config, next){
//check instrumented hash table,
//return instrumented code if available.
var inFile = config.inputFile,
inFileName = config.inputFileName;
//check instrument cache
if (_blanket.options("instrumentCache") && sessionStorage && sessionStorage.getItem("blanket_instrument_store-"+inFileName)){
if (_blanket.options("debug")) {console.log("BLANKET-Reading instrumentation from cache: ",inFileName);}
if (next) {
next(sessionStorage.getItem("blanket_instrument_store-"+inFileName));
} else {
return(sessionStorage.getItem("blanket_instrument_store-"+inFileName));
}
}else{
var sourceArray = _blanket._prepareSource(inFile);
_blanket._trackingArraySetup=[];
//remove shebang
inFile = inFile.replace(/^\#\!.*/, "");
var instrumented = parseAndModify(inFile,{locations:true,comment:true,ecmaVersion:_blanket.options("ecmaVersion")}, _blanket._addTracking(inFileName));
instrumented = _blanket._trackingSetup(inFileName,sourceArray)+instrumented;
if (_blanket.options("sourceURL")){
instrumented += "\n//@ sourceURL="+inFileName.replace("http://","");
}
if (_blanket.options("debug")) {console.log("BLANKET-Instrumented file: ",inFileName);}
if (_blanket.options("instrumentCache") && sessionStorage){
if (_blanket.options("debug")) {console.log("BLANKET-Saving instrumentation to cache: ",inFileName);}
sessionStorage.setItem("blanket_instrument_store-"+inFileName,instrumented);
}
if (next) {
next(instrumented);
} else {
return(instrumented);
}
}
},
instrument: function(config, next){
_blanket.instrumentSync(config, next);
},
_trackingArraySetup: [],
_branchingArraySetup: [],
_useStrictMode: false,
_prepareSource: function(source){
return source.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/(\r\n|\n|\r)/gm,"\n").split('\n');
},
_trackingSetup: function(filename,sourceArray){
var branches = _blanket.options("branchTracking");
var sourceString = sourceArray.join("',\n'");
var intro = "";
var covVar = _blanket.getCovVar();
if(_blanket._useStrictMode) {
intro += "'use strict';\n";
}
intro += "if (typeof "+covVar+" === 'undefined') "+covVar+" = {};\n";
if (branches){
intro += "var _$branchFcn=function(f,l,c,r){ ";
intro += "if (!!r) { ";
intro += covVar+"[f].branchData[l][c][0] = "+covVar+"[f].branchData[l][c][0] || [];";
intro += covVar+"[f].branchData[l][c][0].push(r); }";
intro += "else { ";
intro += covVar+"[f].branchData[l][c][1] = "+covVar+"[f].branchData[l][c][1] || [];";
intro += covVar+"[f].branchData[l][c][1].push(r); }";
intro += "return r;};\n";
}
intro += "if (typeof "+covVar+"['"+filename+"'] === 'undefined'){";
intro += covVar+"['"+filename+"']=[];\n";
if (branches){
intro += covVar+"['"+filename+"'].branchData=[];\n";
}
intro += covVar+"['"+filename+"'].source=['"+sourceString+"'];\n";
//initialize array values
_blanket._trackingArraySetup.sort(function(a,b){
return parseInt(a,10) > parseInt(b,10);
}).forEach(function(item){
intro += covVar+"['"+filename+"']["+item+"]=0;\n";
});
if (branches){
_blanket._branchingArraySetup.sort(function(a,b){
return a.line > b.line;
}).sort(function(a,b){
return a.column > b.column;
}).forEach(function(item){
if (item.file === filename){
intro += "if (typeof "+ covVar+"['"+filename+"'].branchData["+item.line+"] === 'undefined'){\n";
intro += covVar+"['"+filename+"'].branchData["+item.line+"]=[];\n";
intro += "}";
intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"] = [];\n";
intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"].consequent = "+JSON.stringify(item.consequent)+";\n";
intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"].alternate = "+JSON.stringify(item.alternate)+";\n";
}
});
}
intro += "}";
return intro;
},
_blockifyIf: function(node){
if (linesToAddBrackets.indexOf(node.type) > -1){
var bracketsExistObject = node.consequent || node.body;
var bracketsExistAlt = node.alternate;
if( bracketsExistAlt && bracketsExistAlt.type !== "BlockStatement") {
bracketsExistAlt.update("{\n"+bracketsExistAlt.source()+"}\n");
}
if( bracketsExistObject && bracketsExistObject.type !== "BlockStatement") {
bracketsExistObject.update("{\n"+bracketsExistObject.source()+"}\n");
}
}
},
_trackBranch: function(node,filename){
//recursive on consequent and alternative
var line = node.loc.start.line;
var col = node.loc.start.column;
_blanket._branchingArraySetup.push({
line: line,
column: col,
file:filename,
consequent: node.consequent.loc,
alternate: node.alternate.loc
});
var updated = "_$branchFcn"+
"('"+filename+"',"+line+","+col+","+node.test.source()+
")?"+node.consequent.source()+":"+node.alternate.source();
node.update(updated);
},
_addTracking: function (filename) {
//falafel doesn't take a file name
//so we include the filename in a closure
//and return the function to falafel
var covVar = _blanket.getCovVar();
return function(node){
_blanket._blockifyIf(node);
if (linesToAddTracking.indexOf(node.type) > -1 && node.parent.type !== "LabeledStatement") {
_blanket._checkDefs(node,filename);
if (node.type === "VariableDeclaration" &&
(node.parent.type === "ForStatement" || node.parent.type === "ForInStatement")){
return;
}
if (node.loc && node.loc.start){
node.update(covVar+"['"+filename+"']["+node.loc.start.line+"]++;\n"+node.source());
_blanket._trackingArraySetup.push(node.loc.start.line);
}else{
//I don't think we can handle a node with no location
throw new Error("The instrumenter encountered a node with no location: "+Object.keys(node));
}
}else if (_blanket.options("branchTracking") && node.type === "ConditionalExpression"){
_blanket._trackBranch(node,filename);
}else if (node.type === "Literal" && node.value === "use strict" && node.parent && node.parent.type === "ExpressionStatement" && node.parent.parent && node.parent.parent.type === "Program"){
_blanket._useStrictMode = true;
}
};
},
_checkDefs: function(node,filename){
// Make sure developers don't redefine window. if they do, inform them it is wrong.
if (inBrowser){
if (node.type === "VariableDeclaration" && node.declarations) {
node.declarations.forEach(function(declaration) {
if (declaration.id.name === "window") {
throw new Error("Instrumentation error, you cannot redefine the 'window' variable in " + filename + ":" + node.loc.start.line);
}
});
}
if (node.type === "FunctionDeclaration" && node.params) {
node.params.forEach(function(param) {
if (param.name === "window") {
throw new Error("Instrumentation error, you cannot redefine the 'window' variable in " + filename + ":" + node.loc.start.line);
}
});
}
//Make sure developers don't redefine the coverage variable
if (node.type === "ExpressionStatement" &&
node.expression && node.expression.left &&
node.expression.left.object && node.expression.left.property &&
node.expression.left.object.name +
"." + node.expression.left.property.name === _blanket.getCovVar()) {
throw new Error("Instrumentation error, you cannot redefine the coverage variable in " + filename + ":" + node.loc.start.line);
}
}else{
//Make sure developers don't redefine the coverage variable in node
if (node.type === "ExpressionStatement" &&
node.expression && node.expression.left &&
!node.expression.left.object && !node.expression.left.property &&
node.expression.left.name === _blanket.getCovVar()) {
throw new Error("Instrumentation error, you cannot redefine the coverage variable in " + filename + ":" + node.loc.start.line);
}
}
},
setupCoverage: function(){
coverageInfo.instrumentation = "blanket";
coverageInfo.stats = {
"suites": 0,
"tests": 0,
"passes": 0,
"pending": 0,
"failures": 0,
"start": new Date()
};
},
_checkIfSetup: function(){
if (!coverageInfo.stats){
throw new Error("You must call blanket.setupCoverage() first.");
}
},
onTestStart: function(){
if (_blanket.options("debug")) {console.log("BLANKET-Test event started");}
this._checkIfSetup();
coverageInfo.stats.tests++;
coverageInfo.stats.pending++;
},
onTestDone: function(total,passed){
this._checkIfSetup();
if(passed === total){
coverageInfo.stats.passes++;
}else{
coverageInfo.stats.failures++;
}
coverageInfo.stats.pending--;
},
onModuleStart: function(){
this._checkIfSetup();
coverageInfo.stats.suites++;
},
onTestsDone: function(){
if (_blanket.options("debug")) {console.log("BLANKET-Test event done");}
this._checkIfSetup();
coverageInfo.stats.end = new Date();
if (inBrowser){
this.report(coverageInfo);
}else{
if (!_blanket.options("branchTracking")){
delete (inBrowser ? window : global)[_blanket.getCovVar()].branchFcn;
}
this.options("reporter").call(this,coverageInfo);
}
}
};
return _blanket;
})();