426 lines
14 KiB
JavaScript
426 lines
14 KiB
JavaScript
/*
|
|
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'));
|