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