UNPKG

14.3 kBJavaScriptView Raw
1/*
2 Copyright (c) 2012, Yahoo! Inc. All rights reserved.
3 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
4 */
5
6/**
7 * utility methods to process coverage objects. A coverage object has the following
8 * format.
9 *
10 * {
11 * "/path/to/file1.js": { file1 coverage },
12 * "/path/to/file2.js": { file2 coverage }
13 * }
14 *
15 * The internals of the file coverage object are intentionally not documented since
16 * it is not a public interface.
17 *
18 * *Note:* When a method of this module has the word `File` in it, it will accept
19 * one of the sub-objects of the main coverage object as an argument. Other
20 * methods accept the higher level coverage object with multiple keys.
21 *
22 * Works on `node` as well as the browser.
23 *
24 * Usage on nodejs
25 * ---------------
26 *
27 * var objectUtils = require('istanbul').utils;
28 *
29 * Usage in a browser
30 * ------------------
31 *
32 * Load this file using a `script` tag or other means. This will set `window.coverageUtils`
33 * to this module's exports.
34 *
35 * @class ObjectUtils
36 * @module main
37 * @static
38 */
39(function (isNode) {
40 /**
41 * adds line coverage information to a file coverage object, reverse-engineering
42 * it from statement coverage. The object passed in is updated in place.
43 *
44 * Note that if line coverage information is already present in the object,
45 * it is not recomputed.
46 *
47 * @method addDerivedInfoForFile
48 * @static
49 * @param {Object} fileCoverage the coverage object for a single file
50 */
51 function addDerivedInfoForFile(fileCoverage) {
52 var statementMap = fileCoverage.statementMap,
53 statements = fileCoverage.s,
54 lineMap;
55
56 if (!fileCoverage.l) {
57 fileCoverage.l = lineMap = {};
58 Object.keys(statements).forEach(function (st) {
59 var line = statementMap[st].start.line,
60 count = statements[st],
61 prevVal = lineMap[line];
62 if (count === 0 && statementMap[st].skip) { count = 1; }
63 if (typeof prevVal === 'undefined' || prevVal < count) {
64 lineMap[line] = count;
65 }
66 });
67 }
68 }
69 /**
70 * adds line coverage information to all file coverage objects.
71 *
72 * @method addDerivedInfo
73 * @static
74 * @param {Object} coverage the coverage object
75 */
76 function addDerivedInfo(coverage) {
77 Object.keys(coverage).forEach(function (k) {
78 addDerivedInfoForFile(coverage[k]);
79 });
80 }
81 /**
82 * removes line coverage information from all file coverage objects
83 * @method removeDerivedInfo
84 * @static
85 * @param {Object} coverage the coverage object
86 */
87 function removeDerivedInfo(coverage) {
88 Object.keys(coverage).forEach(function (k) {
89 delete coverage[k].l;
90 });
91 }
92
93 function percent(covered, total) {
94 var tmp;
95 if (total > 0) {
96 tmp = 1000 * 100 * covered / total + 5;
97 return Math.floor(tmp / 10) / 100;
98 } else {
99 return 100.00;
100 }
101 }
102
103 function computeSimpleTotals(fileCoverage, property, mapProperty) {
104 var stats = fileCoverage[property],
105 map = mapProperty ? fileCoverage[mapProperty] : null,
106 ret = { total: 0, covered: 0, skipped: 0 };
107
108 Object.keys(stats).forEach(function (key) {
109 var covered = !!stats[key],
110 skipped = map && map[key].skip;
111 ret.total += 1;
112 if (covered || skipped) {
113 ret.covered += 1;
114 }
115 if (!covered && skipped) {
116 ret.skipped += 1;
117 }
118 });
119 ret.pct = percent(ret.covered, ret.total);
120 return ret;
121 }
122
123 function computeBranchTotals(fileCoverage) {
124 var stats = fileCoverage.b,
125 branchMap = fileCoverage.branchMap,
126 ret = { total: 0, covered: 0, skipped: 0 };
127
128 Object.keys(stats).forEach(function (key) {
129 var branches = stats[key],
130 map = branchMap[key],
131 covered,
132 skipped,
133 i;
134 for (i = 0; i < branches.length; i += 1) {
135 covered = branches[i] > 0;
136 skipped = map.locations && map.locations[i] && map.locations[i].skip;
137 if (covered || skipped) {
138 ret.covered += 1;
139 }
140 if (!covered && skipped) {
141 ret.skipped += 1;
142 }
143 }
144 ret.total += branches.length;
145 });
146 ret.pct = percent(ret.covered, ret.total);
147 return ret;
148 }
149 /**
150 * returns a blank summary metrics object. A metrics object has the following
151 * format.
152 *
153 * {
154 * lines: lineMetrics,
155 * statements: statementMetrics,
156 * functions: functionMetrics,
157 * branches: branchMetrics
158 * linesCovered: lineCoveredCount
159 * }
160 *
161 * Each individual metric object looks as follows:
162 *
163 * {
164 * total: n,
165 * covered: m,
166 * pct: percent
167 * }
168 *
169 * @method blankSummary
170 * @static
171 * @return {Object} a blank metrics object
172 */
173 function blankSummary() {
174 return {
175 lines: {
176 total: 0,
177 covered: 0,
178 skipped: 0,
179 pct: 'Unknown'
180 },
181 statements: {
182 total: 0,
183 covered: 0,
184 skipped: 0,
185 pct: 'Unknown'
186 },
187 functions: {
188 total: 0,
189 covered: 0,
190 skipped: 0,
191 pct: 'Unknown'
192 },
193 branches: {
194 total: 0,
195 covered: 0,
196 skipped: 0,
197 pct: 'Unknown'
198 },
199 linesCovered: {}
200 };
201 }
202 /**
203 * returns the summary metrics given the coverage object for a single file. See `blankSummary()`
204 * to understand the format of the returned object.
205 *
206 * @method summarizeFileCoverage
207 * @static
208 * @param {Object} fileCoverage the coverage object for a single file.
209 * @return {Object} the summary metrics for the file
210 */
211 function summarizeFileCoverage(fileCoverage) {
212 var ret = blankSummary();
213 addDerivedInfoForFile(fileCoverage);
214 ret.lines = computeSimpleTotals(fileCoverage, 'l');
215 ret.functions = computeSimpleTotals(fileCoverage, 'f', 'fnMap');
216 ret.statements = computeSimpleTotals(fileCoverage, 's', 'statementMap');
217 ret.branches = computeBranchTotals(fileCoverage);
218 ret.linesCovered = fileCoverage.l;
219 return ret;
220 }
221 /**
222 * merges two instances of file coverage objects *for the same file*
223 * such that the execution counts are correct.
224 *
225 * @method mergeFileCoverage
226 * @static
227 * @param {Object} first the first file coverage object for a given file
228 * @param {Object} second the second file coverage object for the same file
229 * @return {Object} an object that is a result of merging the two. Note that
230 * the input objects are not changed in any way.
231 */
232 function mergeFileCoverage(first, second) {
233 var ret = JSON.parse(JSON.stringify(first)),
234 i;
235
236 delete ret.l; //remove derived info
237
238 Object.keys(second.s).forEach(function (k) {
239 ret.s[k] += second.s[k];
240 });
241 Object.keys(second.f).forEach(function (k) {
242 ret.f[k] += second.f[k];
243 });
244 Object.keys(second.b).forEach(function (k) {
245 var retArray = ret.b[k],
246 secondArray = second.b[k];
247 for (i = 0; i < retArray.length; i += 1) {
248 retArray[i] += secondArray[i];
249 }
250 });
251
252 return ret;
253 }
254 /**
255 * merges multiple summary metrics objects by summing up the `totals` and
256 * `covered` fields and recomputing the percentages. This function is generic
257 * and can accept any number of arguments.
258 *
259 * @method mergeSummaryObjects
260 * @static
261 * @param {Object} summary... multiple summary metrics objects
262 * @return {Object} the merged summary metrics
263 */
264 function mergeSummaryObjects() {
265 var ret = blankSummary(),
266 args = Array.prototype.slice.call(arguments),
267 keys = ['lines', 'statements', 'branches', 'functions'],
268 increment = function (obj) {
269 if (obj) {
270 keys.forEach(function (key) {
271 ret[key].total += obj[key].total;
272 ret[key].covered += obj[key].covered;
273 ret[key].skipped += obj[key].skipped;
274 });
275
276 // keep track of all lines we have coverage for.
277 Object.keys(obj.linesCovered).forEach(function (key) {
278 if (!ret.linesCovered[key]) {
279 ret.linesCovered[key] = obj.linesCovered[key];
280 } else {
281 ret.linesCovered[key] += obj.linesCovered[key];
282 }
283 });
284 }
285 };
286 args.forEach(function (arg) {
287 increment(arg);
288 });
289 keys.forEach(function (key) {
290 ret[key].pct = percent(ret[key].covered, ret[key].total);
291 });
292
293 return ret;
294 }
295 /**
296 * returns the coverage summary for a single coverage object. This is
297 * wrapper over `summarizeFileCoverage` and `mergeSummaryObjects` for
298 * the common case of a single coverage object
299 * @method summarizeCoverage
300 * @static
301 * @param {Object} coverage the coverage object
302 * @return {Object} summary coverage metrics across all files in the coverage object
303 */
304 function summarizeCoverage(coverage) {
305 var fileSummary = [];
306 Object.keys(coverage).forEach(function (key) {
307 fileSummary.push(summarizeFileCoverage(coverage[key]));
308 });
309 return mergeSummaryObjects.apply(null, fileSummary);
310 }
311
312 /**
313 * makes the coverage object generated by this library yuitest_coverage compatible.
314 * Note that this transformation is lossy since the returned object will not have
315 * statement and branch coverage.
316 *
317 * @method toYUICoverage
318 * @static
319 * @param {Object} coverage The `istanbul` coverage object
320 * @return {Object} a coverage object in `yuitest_coverage` format.
321 */
322 function toYUICoverage(coverage) {
323 var ret = {};
324
325 addDerivedInfo(coverage);
326
327 Object.keys(coverage).forEach(function (k) {
328 var fileCoverage = coverage[k],
329 lines = fileCoverage.l,
330 functions = fileCoverage.f,
331 fnMap = fileCoverage.fnMap,
332 o;
333
334 o = ret[k] = {
335 lines: {},
336 calledLines: 0,
337 coveredLines: 0,
338 functions: {},
339 calledFunctions: 0,
340 coveredFunctions: 0
341 };
342 Object.keys(lines).forEach(function (k) {
343 o.lines[k] = lines[k];
344 o.coveredLines += 1;
345 if (lines[k] > 0) {
346 o.calledLines += 1;
347 }
348 });
349 Object.keys(functions).forEach(function (k) {
350 var name = fnMap[k].name + ':' + fnMap[k].line;
351 o.functions[name] = functions[k];
352 o.coveredFunctions += 1;
353 if (functions[k] > 0) {
354 o.calledFunctions += 1;
355 }
356 });
357 });
358 return ret;
359 }
360
361 /**
362 * Creates new file coverage object with incremented hits count
363 * on skipped statements, branches and functions
364 *
365 * @method incrementIgnoredTotals
366 * @static
367 * @param {Object} cov File coverage object
368 * @return {Object} New file coverage object
369 */
370 function incrementIgnoredTotals(cov) {
371 //TODO: This may be slow in the browser and may break in older browsers
372 // Look into using a library that works in Node and the browser
373 var fileCoverage = JSON.parse(JSON.stringify(cov));
374
375 [
376 {mapKey: 'statementMap', hitsKey: 's'},
377 {mapKey: 'branchMap', hitsKey: 'b'},
378 {mapKey: 'fnMap', hitsKey: 'f'}
379 ].forEach(function (keys) {
380 Object.keys(fileCoverage[keys.mapKey])
381 .forEach(function (key) {
382 var map = fileCoverage[keys.mapKey][key];
383 var hits = fileCoverage[keys.hitsKey];
384
385 if (keys.mapKey === 'branchMap') {
386 var locations = map.locations;
387
388 locations.forEach(function (location, index) {
389 if (hits[key][index] === 0 && location.skip) {
390 hits[key][index] = 1;
391 }
392 });
393
394 return;
395 }
396
397 if (hits[key] === 0 && map.skip) {
398 hits[key] = 1;
399 }
400 });
401 });
402
403 return fileCoverage;
404 }
405
406 var exportables = {
407 addDerivedInfo: addDerivedInfo,
408 addDerivedInfoForFile: addDerivedInfoForFile,
409 removeDerivedInfo: removeDerivedInfo,
410 blankSummary: blankSummary,
411 summarizeFileCoverage: summarizeFileCoverage,
412 summarizeCoverage: summarizeCoverage,
413 mergeFileCoverage: mergeFileCoverage,
414 mergeSummaryObjects: mergeSummaryObjects,
415 toYUICoverage: toYUICoverage,
416 incrementIgnoredTotals: incrementIgnoredTotals
417 };
418
419 /* istanbul ignore else: windows */
420 if (isNode) {
421 module.exports = exportables;
422 } else {
423 window.coverageUtils = exportables;
424 }
425}(typeof module !== 'undefined' && typeof module.exports !== 'undefined' && typeof exports !== 'undefined'));