/* 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'));