blob: ab88ae0b2f81e4cb1f41a0ddc63413f2c2f9006a [file] [log] [blame]
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
/**
* utility methods to process coverage objects. A coverage object has the following
* format.
*
* {
* "/path/to/file1.js": { file1 coverage },
* "/path/to/file2.js": { file2 coverage }
* }
*
* The internals of the file coverage object are intentionally not documented since
* it is not a public interface.
*
* *Note:* When a method of this module has the word `File` in it, it will accept
* one of the sub-objects of the main coverage object as an argument. Other
* methods accept the higher level coverage object with multiple keys.
*
* Works on `node` as well as the browser.
*
* Usage on nodejs
* ---------------
*
* var objectUtils = require('istanbul').utils;
*
* Usage in a browser
* ------------------
*
* Load this file using a `script` tag or other means. This will set `window.coverageUtils`
* to this module's exports.
*
* @class ObjectUtils
* @module main
* @static
*/
(function (isNode) {
/**
* adds line coverage information to a file coverage object, reverse-engineering
* it from statement coverage. The object passed in is updated in place.
*
* Note that if line coverage information is already present in the object,
* it is not recomputed.
*
* @method addDerivedInfoForFile
* @static
* @param {Object} fileCoverage the coverage object for a single file
*/
function addDerivedInfoForFile(fileCoverage) {
var statementMap = fileCoverage.statementMap,
statements = fileCoverage.s,
lineMap;
if (!fileCoverage.l) {
fileCoverage.l = lineMap = {};
Object.keys(statements).forEach(function (st) {
var line = statementMap[st].start.line,
count = statements[st],
prevVal = lineMap[line];
if (count === 0 && statementMap[st].skip) { count = 1; }
if (typeof prevVal === 'undefined' || prevVal < count) {
lineMap[line] = count;
}
});
}
}
/**
* adds line coverage information to all file coverage objects.
*
* @method addDerivedInfo
* @static
* @param {Object} coverage the coverage object
*/
function addDerivedInfo(coverage) {
Object.keys(coverage).forEach(function (k) {
addDerivedInfoForFile(coverage[k]);
});
}
/**
* removes line coverage information from all file coverage objects
* @method removeDerivedInfo
* @static
* @param {Object} coverage the coverage object
*/
function removeDerivedInfo(coverage) {
Object.keys(coverage).forEach(function (k) {
delete coverage[k].l;
});
}
function percent(covered, total) {
var tmp;
if (total > 0) {
tmp = 1000 * 100 * covered / total + 5;
return Math.floor(tmp / 10) / 100;
} else {
return 100.00;
}
}
function computeSimpleTotals(fileCoverage, property, mapProperty) {
var stats = fileCoverage[property],
map = mapProperty ? fileCoverage[mapProperty] : null,
ret = { total: 0, covered: 0, skipped: 0 };
Object.keys(stats).forEach(function (key) {
var covered = !!stats[key],
skipped = map && map[key].skip;
ret.total += 1;
if (covered || skipped) {
ret.covered += 1;
}
if (!covered && skipped) {
ret.skipped += 1;
}
});
ret.pct = percent(ret.covered, ret.total);
return ret;
}
function computeBranchTotals(fileCoverage) {
var stats = fileCoverage.b,
branchMap = fileCoverage.branchMap,
ret = { total: 0, covered: 0, skipped: 0 };
Object.keys(stats).forEach(function (key) {
var branches = stats[key],
map = branchMap[key],
covered,
skipped,
i;
for (i = 0; i < branches.length; i += 1) {
covered = branches[i] > 0;
skipped = map.locations && map.locations[i] && map.locations[i].skip;
if (covered || skipped) {
ret.covered += 1;
}
if (!covered && skipped) {
ret.skipped += 1;
}
}
ret.total += branches.length;
});
ret.pct = percent(ret.covered, ret.total);
return ret;
}
/**
* returns a blank summary metrics object. A metrics object has the following
* format.
*
* {
* lines: lineMetrics,
* statements: statementMetrics,
* functions: functionMetrics,
* branches: branchMetrics
* linesCovered: lineCoveredCount
* }
*
* Each individual metric object looks as follows:
*
* {
* total: n,
* covered: m,
* pct: percent
* }
*
* @method blankSummary
* @static
* @return {Object} a blank metrics object
*/
function blankSummary() {
return {
lines: {
total: 0,
covered: 0,
skipped: 0,
pct: 'Unknown'
},
statements: {
total: 0,
covered: 0,
skipped: 0,
pct: 'Unknown'
},
functions: {
total: 0,
covered: 0,
skipped: 0,
pct: 'Unknown'
},
branches: {
total: 0,
covered: 0,
skipped: 0,
pct: 'Unknown'
},
linesCovered: {}
};
}
/**
* returns the summary metrics given the coverage object for a single file. See `blankSummary()`
* to understand the format of the returned object.
*
* @method summarizeFileCoverage
* @static
* @param {Object} fileCoverage the coverage object for a single file.
* @return {Object} the summary metrics for the file
*/
function summarizeFileCoverage(fileCoverage) {
var ret = blankSummary();
addDerivedInfoForFile(fileCoverage);
ret.lines = computeSimpleTotals(fileCoverage, 'l');
ret.functions = computeSimpleTotals(fileCoverage, 'f', 'fnMap');
ret.statements = computeSimpleTotals(fileCoverage, 's', 'statementMap');
ret.branches = computeBranchTotals(fileCoverage);
ret.linesCovered = fileCoverage.l;
return ret;
}
/**
* merges two instances of file coverage objects *for the same file*
* such that the execution counts are correct.
*
* @method mergeFileCoverage
* @static
* @param {Object} first the first file coverage object for a given file
* @param {Object} second the second file coverage object for the same file
* @return {Object} an object that is a result of merging the two. Note that
* the input objects are not changed in any way.
*/
function mergeFileCoverage(first, second) {
var ret = JSON.parse(JSON.stringify(first)),
i;
delete ret.l; //remove derived info
Object.keys(second.s).forEach(function (k) {
ret.s[k] += second.s[k];
});
Object.keys(second.f).forEach(function (k) {
ret.f[k] += second.f[k];
});
Object.keys(second.b).forEach(function (k) {
var retArray = ret.b[k],
secondArray = second.b[k];
for (i = 0; i < retArray.length; i += 1) {
retArray[i] += secondArray[i];
}
});
return ret;
}
/**
* merges multiple summary metrics objects by summing up the `totals` and
* `covered` fields and recomputing the percentages. This function is generic
* and can accept any number of arguments.
*
* @method mergeSummaryObjects
* @static
* @param {Object} summary... multiple summary metrics objects
* @return {Object} the merged summary metrics
*/
function mergeSummaryObjects() {
var ret = blankSummary(),
args = Array.prototype.slice.call(arguments),
keys = ['lines', 'statements', 'branches', 'functions'],
increment = function (obj) {
if (obj) {
keys.forEach(function (key) {
ret[key].total += obj[key].total;
ret[key].covered += obj[key].covered;
ret[key].skipped += obj[key].skipped;
});
// keep track of all lines we have coverage for.
Object.keys(obj.linesCovered).forEach(function (key) {
if (!ret.linesCovered[key]) {
ret.linesCovered[key] = obj.linesCovered[key];
} else {
ret.linesCovered[key] += obj.linesCovered[key];
}
});
}
};
args.forEach(function (arg) {
increment(arg);
});
keys.forEach(function (key) {
ret[key].pct = percent(ret[key].covered, ret[key].total);
});
return ret;
}
/**
* returns the coverage summary for a single coverage object. This is
* wrapper over `summarizeFileCoverage` and `mergeSummaryObjects` for
* the common case of a single coverage object
* @method summarizeCoverage
* @static
* @param {Object} coverage the coverage object
* @return {Object} summary coverage metrics across all files in the coverage object
*/
function summarizeCoverage(coverage) {
var fileSummary = [];
Object.keys(coverage).forEach(function (key) {
fileSummary.push(summarizeFileCoverage(coverage[key]));
});
return mergeSummaryObjects.apply(null, fileSummary);
}
/**
* makes the coverage object generated by this library yuitest_coverage compatible.
* Note that this transformation is lossy since the returned object will not have
* statement and branch coverage.
*
* @method toYUICoverage
* @static
* @param {Object} coverage The `istanbul` coverage object
* @return {Object} a coverage object in `yuitest_coverage` format.
*/
function toYUICoverage(coverage) {
var ret = {};
addDerivedInfo(coverage);
Object.keys(coverage).forEach(function (k) {
var fileCoverage = coverage[k],
lines = fileCoverage.l,
functions = fileCoverage.f,
fnMap = fileCoverage.fnMap,
o;
o = ret[k] = {
lines: {},
calledLines: 0,
coveredLines: 0,
functions: {},
calledFunctions: 0,
coveredFunctions: 0
};
Object.keys(lines).forEach(function (k) {
o.lines[k] = lines[k];
o.coveredLines += 1;
if (lines[k] > 0) {
o.calledLines += 1;
}
});
Object.keys(functions).forEach(function (k) {
var name = fnMap[k].name + ':' + fnMap[k].line;
o.functions[name] = functions[k];
o.coveredFunctions += 1;
if (functions[k] > 0) {
o.calledFunctions += 1;
}
});
});
return ret;
}
/**
* Creates new file coverage object with incremented hits count
* on skipped statements, branches and functions
*
* @method incrementIgnoredTotals
* @static
* @param {Object} cov File coverage object
* @return {Object} New file coverage object
*/
function incrementIgnoredTotals(cov) {
//TODO: This may be slow in the browser and may break in older browsers
// Look into using a library that works in Node and the browser
var fileCoverage = JSON.parse(JSON.stringify(cov));
[
{mapKey: 'statementMap', hitsKey: 's'},
{mapKey: 'branchMap', hitsKey: 'b'},
{mapKey: 'fnMap', hitsKey: 'f'}
].forEach(function (keys) {
Object.keys(fileCoverage[keys.mapKey])
.forEach(function (key) {
var map = fileCoverage[keys.mapKey][key];
var hits = fileCoverage[keys.hitsKey];
if (keys.mapKey === 'branchMap') {
var locations = map.locations;
locations.forEach(function (location, index) {
if (hits[key][index] === 0 && location.skip) {
hits[key][index] = 1;
}
});
return;
}
if (hits[key] === 0 && map.skip) {
hits[key] = 1;
}
});
});
return fileCoverage;
}
var exportables = {
addDerivedInfo: addDerivedInfo,
addDerivedInfoForFile: addDerivedInfoForFile,
removeDerivedInfo: removeDerivedInfo,
blankSummary: blankSummary,
summarizeFileCoverage: summarizeFileCoverage,
summarizeCoverage: summarizeCoverage,
mergeFileCoverage: mergeFileCoverage,
mergeSummaryObjects: mergeSummaryObjects,
toYUICoverage: toYUICoverage,
incrementIgnoredTotals: incrementIgnoredTotals
};
/* istanbul ignore else: windows */
if (isNode) {
module.exports = exportables;
} else {
window.coverageUtils = exportables;
}
}(typeof module !== 'undefined' && typeof module.exports !== 'undefined' && typeof exports !== 'undefined'));