Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80629 views
1
var instrument = require('./instrument')
2
var Module = require('module').Module;
3
var path = require('path');
4
var fs = require('fs');
5
var vm = require('vm');
6
var _ = require('underscore');
7
8
// Coverage tracker
9
function CoverageData (filename, instrumentor) {
10
this.instrumentor = instrumentor;
11
this.filename = filename;
12
this.nodes = {};
13
this.visitedBlocks = {};
14
this.source = instrumentor.source;
15
};
16
17
// Note that a node has been visited
18
CoverageData.prototype.visit = function(node) {
19
var node = this.nodes[node.id] = (this.nodes[node.id] || {node:node, count:0})
20
node.count++;
21
};
22
23
// Note that a node has been visited
24
CoverageData.prototype.visitBlock = function(blockIndex) {
25
var block = this.visitedBlocks[blockIndex] = (this.visitedBlocks[blockIndex] || {count:0})
26
block.count++;
27
};
28
29
// Get all the nodes we did not see
30
CoverageData.prototype.missing = function() {
31
// Find all the nodes which we haven't seen
32
var nodes = this.nodes;
33
var missing = this.instrumentor.filter(function(node) {
34
return !nodes[node.id];
35
});
36
37
return missing;
38
};
39
40
// Get all the nodes we did see
41
CoverageData.prototype.seen = function() {
42
// Find all the nodes we have seen
43
var nodes = this.nodes;
44
var seen = this.instrumentor.filter(function(node) {
45
return !!nodes[node.id];
46
});
47
48
return seen;
49
};
50
51
// Calculate node coverage statistics
52
CoverageData.prototype.blocks = function() {
53
var totalBlocks = this.instrumentor.blockCounter;
54
var numSeenBlocks = 0;
55
for(var index in this.visitedBlocks) {
56
numSeenBlocks++;
57
}
58
var numMissingBlocks = totalBlocks - numSeenBlocks;
59
60
var toReturn = {
61
total: totalBlocks,
62
seen: numSeenBlocks,
63
missing: numMissingBlocks,
64
percentage: totalBlocks ? numSeenBlocks / totalBlocks : 1
65
};
66
67
return toReturn;
68
};
69
70
// Explode all multi-line nodes into single-line ones.
71
var explodeNodes = function(coverageData, fileData) {
72
var missing = coverageData.missing();
73
var newNodes = [];
74
75
// Get only the multi-line nodes.
76
var multiLineNodes = missing.filter(function(node) {
77
return (node.loc.start.line < node.loc.end.line);
78
});
79
80
for(var i = 0; i < multiLineNodes.length; i++) {
81
// Get the current node and delta
82
var node = multiLineNodes[i];
83
var lineDelta = node.loc.end.line - node.loc.start.line + 1;
84
85
for(var j = 0; j < lineDelta; j++) {
86
// For each line in the multi-line node, we'll create a
87
// new node, and we set the start and end columns
88
// to the correct vlaues.
89
var curLine = node.loc.start.line + j;
90
var startCol = 0;
91
var endCol = fileData[curLine - 1].length;
92
93
if (curLine === node.loc.start.line) {
94
startCol = node.loc.start.column;
95
}
96
else if (curLine === node.loc.end.line) {
97
startCol = 0;
98
endCol = node.loc.end.column;
99
}
100
101
var newNode = {
102
loc:
103
{
104
start: {
105
line: curLine,
106
col: startCol
107
},
108
end: {
109
line: curLine,
110
col: endCol
111
}
112
}
113
};
114
115
newNodes.push(newNode);
116
}
117
}
118
119
return newNodes;
120
}
121
122
// Get per-line code coverage information
123
CoverageData.prototype.coverage = function() {
124
var missingLines = this.missing();
125
var fileData = this.instrumentor.source.split('\n');
126
127
// Get a dictionary of all the lines we did observe being at least
128
// partially covered
129
var seen = {};
130
131
this.seen().forEach(function(node) {
132
seen[node.loc.start.line] = true;
133
});
134
135
// Add all the new multi-line nodes.
136
missingLines = missingLines.concat(explodeNodes(this, fileData));
137
138
var seenNodes = {};
139
missingLines = missingLines.sort(
140
function(lhs, rhs) {
141
var lhsNode = lhs.loc;
142
var rhsNode = rhs.loc;
143
144
// First try to sort based on line
145
return lhsNode.start.line < rhsNode.start.line ? -1 : // first try line
146
lhsNode.start.line > rhsNode.start.line ? 1 :
147
lhsNode.start.column < rhsNode.start.column ? -1 : // then try start col
148
lhsNode.start.column > rhsNode.start.column ? 1 :
149
lhsNode.end.column < rhsNode.end.column ? -1 : // then try end col
150
lhsNode.end.column > rhsNode.end.column ? 1 :
151
0; // then just give up and say they are equal
152
}).filter(function(node) {
153
// If it is a multi-line node, we can just ignore it
154
if (node.loc.start.line < node.loc.end.line) {
155
return false;
156
}
157
158
// We allow multiple nodes per line, but only one node per
159
// start column (due to how instrumented works)
160
var okay = false;
161
if (seenNodes.hasOwnProperty(node.loc.start.line)) {
162
var isNew = (seenNodes[node.loc.start.line].indexOf(node.loc.start.column) < 0);
163
if (isNew) {
164
seenNodes[node.loc.start.line].push(node.loc.start.column);
165
okay = true;
166
}
167
}
168
else {
169
seenNodes[node.loc.start.line] = [node.loc.start.column];
170
okay = true;
171
}
172
173
return okay;
174
});
175
176
var coverage = {};
177
178
missingLines.forEach(function(node) {
179
// For each missing line, add some information for it
180
var line = node.loc.start.line;
181
var startCol = node.loc.start.column;
182
var endCol = node.loc.end.column;
183
var source = fileData[line - 1];
184
var partial = seen.hasOwnProperty(line) && seen[line];
185
186
if (coverage.hasOwnProperty(line)) {
187
coverage[line].missing.push({startCol: startCol, endCol: endCol});
188
}
189
else {
190
coverage[line] = {
191
partial: partial,
192
source: source,
193
missing: [{startCol: startCol, endCol: endCol}]
194
};
195
}
196
});
197
198
return coverage;
199
};
200
201
CoverageData.prototype.prepare = function() {
202
var store = require('./coverage_store').getStore(this.filename);
203
204
for(var index in store.nodes) {
205
if (store.nodes.hasOwnProperty(index)) {
206
this.nodes[index] = {node: this.instrumentor.nodes[index], count: store.nodes[index].count};
207
}
208
}
209
210
for(var index in store.blocks) {
211
if (store.blocks.hasOwnProperty(index)) {
212
this.visitedBlocks[index] = {count: store.blocks[index].count};
213
}
214
}
215
};
216
217
// Get statistics for the entire file, including per-line code coverage
218
// and block-level coverage
219
CoverageData.prototype.stats = function() {
220
this.prepare();
221
222
var missing = this.missing();
223
var filedata = this.instrumentor.source.split('\n');
224
225
var observedMissing = [];
226
var linesInfo = missing.sort(function(lhs, rhs) {
227
return lhs.loc.start.line < rhs.loc.start.line ? -1 :
228
lhs.loc.start.line > rhs.loc.start.line ? 1 :
229
0;
230
}).filter(function(node) {
231
// Make sure we don't double count missing lines due to multi-line
232
// issues
233
var okay = (observedMissing.indexOf(node.loc.start.line) < 0);
234
if(okay) {
235
observedMissing.push(node.loc.start.line);
236
}
237
238
return okay;
239
}).map(function(node, idx, all) {
240
// For each missing line, add info for it
241
return {
242
lineno: node.loc.start.line,
243
source: function() { return filedata[node.loc.start.line - 1]; }
244
};
245
});
246
247
var numLines = filedata.length;
248
var numMissingLines = observedMissing.length;
249
var numSeenLines = numLines - numMissingLines;
250
var percentageCovered = numSeenLines / numLines;
251
252
return {
253
percentage: percentageCovered,
254
lines: linesInfo,
255
missing: numMissingLines,
256
seen: numSeenLines,
257
total: numLines,
258
coverage: this.coverage(),
259
source: this.source,
260
blocks: this.blocks()
261
};
262
};
263
264
// Require the CLI module of cover and return it,
265
// in case anyone wants to use it programmatically
266
var cli = function() {
267
return require('./bin/cover');
268
};
269
270
var addInstrumentationHeader = function(template, filename, instrumented, coverageStorePath) {
271
var template = _.template(template);
272
var renderedSource = template({
273
instrumented: instrumented,
274
coverageStorePath: coverageStorePath,
275
filename: filename,
276
source: instrumented.instrumentedSource
277
});
278
279
return renderedSource
280
};
281
282
var stripBOM = function(content) {
283
// Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
284
// because the buffer-to-string conversion in `fs.readFileSync()`
285
// translates it to FEFF, the UTF-16 BOM.
286
if (content.charCodeAt(0) === 0xFEFF) {
287
content = content.slice(1);
288
}
289
return content;
290
}
291
292
var load = function(datas) {
293
var combinedCoverage = {};
294
295
_.each(datas, function(data) {
296
_.each(data.files, function(fileData, filename) {
297
var instrumentor = {
298
source: fileData.instrumentor.source,
299
nodes: fileData.instrumentor.nodes,
300
nodeCounter: fileData.instrumentor.nodeCounter,
301
blockCounter: fileData.instrumentor.blockCounter,
302
};
303
304
var coverage = new CoverageData(filename, new instrument.Instrumentor());
305
coverage.instrumentor.source = fileData.instrumentor.source;
306
coverage.instrumentor.nodes = fileData.instrumentor.nodes;
307
coverage.instrumentor.nodeCounter = fileData.instrumentor.nodeCounter;
308
coverage.instrumentor.blockCounter = fileData.instrumentor.blockCounter;
309
310
coverage.source = coverage.instrumentor.source;
311
coverage.hash = fileData.hash;
312
coverage.nodes = fileData.nodes;
313
coverage.visitedBlocks = fileData.blocks;
314
315
if (combinedCoverage.hasOwnProperty(filename)) {
316
var coverageSoFar = combinedCoverage[filename];
317
318
if (coverageSoFar.hash !== coverage.hash) {
319
throw new Error("Multiple versions of file '" + filename + "' in coverage data!");
320
}
321
322
// Merge nodes
323
_.each(coverage.nodes, function(nodeData, index) {
324
var nodeDataSoFar = coverageSoFar.nodes[index] = (coverageSoFar.nodes[index] || {node: nodeData.node, count: 0});
325
nodeDataSoFar.count += nodeData.count;
326
});
327
328
// Merge blocks
329
_.each(coverage.visitedBlocks, function(blockData, index) {
330
var blockDataSoFar = coverageSoFar.visitedBlocks[index] = (coverageSoFar.visitedBlocks[index] || {count: 0});
331
blockDataSoFar.count += blockData.count;
332
});
333
}
334
else {
335
combinedCoverage[filename] = coverage;
336
}
337
});
338
});
339
340
return combinedCoverage;
341
}
342
343
var cover = function(fileRegex, ignore, debugDirectory) {
344
var originalRequire = require.extensions['.js'];
345
var coverageData = {};
346
var match = null;
347
348
ignore = ignore || {};
349
350
if (fileRegex instanceof RegExp) {
351
match = regex;
352
}
353
else {
354
match = new RegExp(fileRegex ? (fileRegex.replace(/\//g, '\\/').replace(/\./g, '\\.')) : ".*", '');
355
}
356
357
var pathToCoverageStore = path.resolve(path.resolve(__dirname), "coverage_store.js").replace(/\\/g, "/");
358
var templatePath = path.resolve(path.resolve(__dirname), "templates", "instrumentation_header.js");
359
var template = fs.readFileSync(templatePath, 'utf-8');
360
361
require.extensions['.js'] = function(module, filename) {
362
363
filename = filename.replace(/\\/g, "/");
364
365
if(!match.test(filename)) return originalRequire(module, filename);
366
if(filename === pathToCoverageStore) return originalRequire(module, filename);
367
368
// If the specific file is to be ignored
369
var full = path.resolve(filename);
370
if(ignore[full]) {
371
return originalRequire(module, filename);
372
}
373
374
// If any of the parents of the file are to be ignored
375
do {
376
full = path.dirname(full);
377
if (ignore[full]) {
378
return originalRequire(module, filename);
379
}
380
} while(full !== path.dirname(full));
381
382
var data = stripBOM(fs.readFileSync(filename, 'utf8').trim());
383
data = data.replace(/^\#\!.*/, '');
384
385
var instrumented = instrument(data);
386
var coverage = coverageData[filename] = new CoverageData(filename, instrumented);
387
388
var newCode = addInstrumentationHeader(template, filename, instrumented, pathToCoverageStore);
389
390
if (debugDirectory) {
391
var outputPath = path.join(debugDirectory, filename.replace(/[\/|\:|\\]/g, "_") + ".js");
392
fs.writeFileSync(outputPath, newCode);
393
}
394
395
return module._compile(newCode, filename);
396
};
397
398
// Setup the data retrieval and release functions
399
var coverage = function(ready) {
400
ready(coverageData);
401
};
402
403
coverage.release = function() {
404
require.extensions['.js'] = originalRequire;
405
};
406
407
return coverage;
408
};
409
410
module.exports = {
411
cover: cover,
412
cli: cli,
413
load: load,
414
hook: function() {
415
var c = cli();
416
c.hook.apply(c, arguments);
417
},
418
combine: function() {
419
var c = cli();
420
c.combine.apply(c, arguments);
421
},
422
hookAndReport: function() {
423
var c = cli();
424
c.hookAndReport.apply(c, arguments);
425
},
426
hookAndCombine: function() {
427
var c = cli();
428
c.hookAndCombine.apply(c, arguments);
429
},
430
hookAndCombineAndReport: function() {
431
var c = cli();
432
c.hookAndCombineAndReport.apply(c, arguments);
433
},
434
reporters: {
435
html: require('./reporters/html'),
436
plain: require('./reporters/plain'),
437
cli: require('./reporters/cli'),
438
json: require('./reporters/json')
439
}
440
};
441