lab
Version:
Test utility
509 lines (385 loc) • 15.2 kB
JavaScript
// Adapted from:
// Blanket https://github.com/alex-seville/blanket, copyright (c) 2013 Alex Seville, MIT licensed
// Falafel https://github.com/substack/node-falafel, copyright (c) James Halliday, MIT licensed
// Load modules
var Fs = require('fs');
var Path = require('path');
var Espree = require('espree');
var SourceMapSupport = require('source-map-support');
var Transform = require('./transform');
// Declare internals
var internals = {
patterns: [],
sources: {}
};
internals.prime = function (extension) {
require.extensions[extension] = function (localModule, filename) {
for (var i = 0, il = internals.patterns.length; i < il; ++i) {
if (internals.patterns[i].test(filename.replace(/\\/g, '/'))) {
return localModule._compile(internals.instrument(filename), filename);
}
}
var src = Fs.readFileSync(filename, 'utf8');
return localModule._compile(Transform.transform(filename, src), filename);
};
};
exports.instrument = function (options) {
internals.patterns.unshift(internals.pattern(options));
Transform.install(options, internals.prime);
};
internals.pattern = function (options) {
var base = internals.escape(options.coveragePath || '');
var excludes = options.coverageExclude ? [].concat(options.coverageExclude).map(internals.escape).join('|') : '';
var regex = '^' + base + (excludes ? (base[base.length - 1] === '/' ? '' : '\\/') + '(?!' + excludes + ')' : '');
return new RegExp(regex);
};
internals.escape = function (string) {
return string.replace(/\\/g, '/').replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&');
};
internals.instrument = function (filename) {
filename = filename.replace(/\\/g, '/');
var file = Fs.readFileSync(filename, 'utf8');
var content = file.replace(/^\#\!.*/, '');
content = Transform.transform(filename, content);
var tracking = [];
var statements = [];
var chunks = content.split('');
var ids = 0;
var bypass = {};
var annotate = function (node, parent) {
// Decorate node
node.parent = parent;
node.source = function () {
return chunks.slice(node.range[0], node.range[1]).join('');
};
node.set = function (s) {
chunks[node.range[0]] = s;
for (var i = node.range[0] + 1, il = node.range[1]; i < il; i++) {
chunks[i] = '';
}
};
// Coverage status
if (bypass[node.range[0]]) {
return;
}
// Recursively annotate the tree from the inner-most out
Object.keys(node).forEach(function (name) {
if (name === 'parent') {
return;
}
var children = [].concat(node[name]);
children.forEach(function (child) {
if (child && typeof child.type === 'string') { // Identify node types
annotate(child, node);
}
});
});
// Annotate source code
var decoratedTypes = [
'IfStatement',
'WhileStatement',
'DoWhileStatement',
'ForStatement',
'ForInStatement',
'WithStatement'
];
var consequent;
var line;
if (decoratedTypes.indexOf(node.type) !== -1) {
if (node.alternate &&
node.alternate.type !== 'BlockStatement') {
node.alternate.set('{' + node.alternate.source() + '}');
}
consequent = node.consequent || node.body;
if (consequent.type !== 'BlockStatement') {
consequent.set('{' + consequent.source() + '}');
}
}
var trackedTypes = [
'ExpressionStatement',
'BreakStatement',
'ContinueStatement',
'VariableDeclaration',
'ReturnStatement',
'ThrowStatement',
'TryStatement',
'FunctionDeclaration',
'IfStatement',
'WhileStatement',
'DoWhileStatement',
'ForStatement',
'ForInStatement',
'SwitchStatement',
'WithStatement',
'LabeledStatement'
];
if (trackedTypes.indexOf(node.type) !== -1 &&
(node.type !== 'VariableDeclaration' || (node.parent.type !== 'ForStatement' && node.parent.type !== 'ForInStatement')) &&
(node.type !== 'ExpressionStatement' || node.expression.value !== 'use strict') &&
node.parent.type !== 'LabeledStatement') {
tracking.push(node.loc.start.line);
node.set('global.__$$labCov._line(\'' + filename + '\',' + node.loc.start.line + ');' + node.source());
}
else if (node.type === 'ConditionalExpression') {
line = node.loc.start.line;
consequent = addStatement(line, node.consequent, false);
var alternate = addStatement(line, node.alternate, false);
node.set('(' + node.test.source() + '? global.__$$labCov._statement(\'' + filename + '\',' + consequent + ',' + line + ',' + node.consequent.source() + ') : global.__$$labCov._statement(\'' + filename + '\',' + alternate + ',' + line + ',' + node.alternate.source() + '))');
}
else if (node.type === 'LogicalExpression') {
line = node.loc.start.line;
var left = addStatement(line, node.left, true);
var right = addStatement(line, node.right, node.parent.type === 'LogicalExpression');
node.set('(global.__$$labCov._statement(\'' + filename + '\',' + left + ',' + line + ',' + node.left.source() + ')' + node.operator + 'global.__$$labCov._statement(\'' + filename + '\',' + right + ',' + line + ',' + node.right.source() + '))');
}
else if (node.parent &&
node.parent.test === node &&
node.parent.type !== 'SwitchCase') {
line = node.loc.start.line;
var test = addStatement(line, node, true);
node.set('global.__$$labCov._statement(\'' + filename + '\',' + test + ',' + line + ',' + node.source() + ')');
}
};
var addStatement = function (line, node, bool) {
var id = ++ids;
statements.push({
id: id,
loc: node.loc,
line: line,
bool: bool && node.type !== 'ConditionalExpression' && node.type !== 'LogicalExpression'
});
return id;
};
// Parse tree
var tree = Espree.parse(content, {
loc: true,
comment: true,
range: true,
ecmaFeatures: {
blockBindings: true,
arrowFunctions: true,
templateStrings: true,
generators: true,
forOf: true,
binaryLiterals: true,
octalLiterals: true,
classes: true,
objectLiteralShorthandProperties: true,
objectLiteralShorthandMethods: true
}
});
// Process comments
var skipStart = 0;
var segmentSkip = false;
tree.comments.forEach(function (comment) {
var directive = comment.value.match(/^\s*\$lab\:coverage\:(off|on)\$\s*$/);
if (directive) {
var skip = directive[1] !== 'on';
if (skip !== segmentSkip) {
segmentSkip = skip;
if (skip) {
skipStart = comment.range[1];
}
else {
for (var s = skipStart; s < comment.range[0]; ++s) {
bypass[s] = true;
}
}
}
}
});
// Begin code annotation
annotate(tree);
// Store original source
var transformedFile = content.replace(/\/\/\#(.*)$/, '');
internals.sources[filename] = transformedFile.replace(/(\r\n|\n|\r)/gm, '\n').split('\n');
// Setup global report container
// $lab:coverage:off$
if (typeof global.__$$labCov === 'undefined') {
global.__$$labCov = {
files: {},
_line: function (name, line) {
global.__$$labCov.files[name].lines[line]++;
},
_statement: function (name, id, line, source) {
var statement = global.__$$labCov.files[name].statements[line][id];
if (!statement.bool) {
statement.hit[!source] = true;
}
statement.hit[!!source] = true;
return source;
}
};
} // $lab:coverage:on$
global.__$$labCov.files[filename] = {
statements: {},
lines: {}
};
var record = global.__$$labCov.files[filename];
tracking.forEach(function (item) {
record.lines[item] = 0;
});
statements.forEach(function (item) {
record.statements[item.line] = record.statements[item.line] || {};
record.statements[item.line][item.id] = { hit: {}, bool: item.bool, loc: item.loc };
});
return chunks.join('');
};
exports.analyze = function (options) {
// Process coverage (global.__$$labCov needed when labCov isn't defined)
/* $lab:coverage:off$ */ var report = global.__$$labCov || { files: {} }; /* $lab:coverage:on$ */
var pattern = internals.pattern(options);
var cov = {
sloc: 0,
hits: 0,
misses: 0,
percent: 0,
files: []
};
// Filter files
var files = Object.keys(report.files);
for (var i = 0, il = files.length; i < il; ++i) {
var filename = files[i];
if (pattern.test(filename)) {
report.files[filename].source = internals.sources[filename] || [];
var data = internals.file(filename, report.files[filename], options);
cov.files.push(data);
cov.hits += data.hits;
cov.misses += data.misses;
cov.sloc += data.sloc;
}
}
// Sort files based on directory structure
cov.files.sort(function (a, b) {
var segmentsA = a.filename.split('/');
var segmentsB = b.filename.split('/');
var al = segmentsA.length;
var bl = segmentsB.length;
for (var si = 0; si < al && si < bl; ++si) {
if (segmentsA[si] === segmentsB[si]) {
continue;
}
var lastA = si + 1 === al;
var lastB = si + 1 === bl;
if (lastA !== lastB) {
return lastA ? -1 : 1;
}
return segmentsA[si] < segmentsB[si] ? -1 : 1;
}
return segmentsA.length < segmentsB.length ? -1 : 1;
});
// Calculate coverage percentage
if (cov.sloc > 0) {
cov.percent = (cov.hits / cov.sloc) * 100;
}
return cov;
};
internals.addSourceMapsInformation = function (ret, num) {
var position = {
source: ret.filename,
line: num,
column: 0
};
var originalPosition = SourceMapSupport.mapSourcePosition(position);
var source = ret.source[num];
if (position !== originalPosition) {
source.originalFilename = originalPosition.source.replace(Path.join(process.cwd(), '/').replace(/\\/g, '/'), '');
source.originalLine = originalPosition.line;
if (!ret.sourcemaps) {
ret.sourcemaps = true;
}
}
else {
source.originalFilename = ret.filename;
source.originalLine = num;
}
};
internals.file = function (filename, data, options) {
var ret = {
filename: filename.replace(Path.join(process.cwd(), '/').replace(/\\/g, '/'), ''),
percent: 0,
hits: 0,
misses: 0,
sloc: 0,
source: {}
};
// Process each line of code
data.source.forEach(function (line, num) {
num++;
var isMiss = false;
ret.source[num] = {
source: line
};
if (options.sourcemaps) {
internals.addSourceMapsInformation(ret, +num);
}
if (data.lines[num] === 0) {
isMiss = true;
ret.misses++;
ret.sloc++;
}
else if (line) {
ret.sloc++;
if (data.statements[num]) {
var mask = new Array(line.length);
Object.keys(data.statements[num]).forEach(function (id) {
var statement = data.statements[num][id];
if (statement.hit.true &&
statement.hit.false) {
return;
}
if (statement.loc.start.line !== num) {
data.statements[statement.loc.start.line] = data.statements[statement.loc.start.line] || {};
data.statements[statement.loc.start.line][id] = statement;
return;
}
if (statement.loc.end.line !== num) {
data.statements[statement.loc.end.line] = data.statements[statement.loc.end.line] || {};
data.statements[statement.loc.end.line][id] = {
hit: statement.hit,
loc: {
start: {
line: statement.loc.end.line,
column: 0
},
end: {
line: statement.loc.end.line,
column: statement.loc.end.column
}
}
};
statement.loc.end.column = line.length;
}
isMiss = true;
var issue = statement.hit.true ? 'true' : (statement.hit.false ? 'false' : 'never');
for (var ci = statement.loc.start.column; ci < statement.loc.end.column; ++ci) {
mask[ci] = issue;
}
});
var chunks = [];
var from = 0;
for (var a = 1, al = mask.length; a < al; ++a) {
if (mask[a] !== mask[a - 1]) {
chunks.push({ source: line.slice(from, a), miss: mask[a - 1] });
from = a;
}
}
chunks.push({ source: line.slice(from), miss: mask[from] });
if (isMiss) {
ret.source[num].chunks = chunks;
ret.misses++;
}
else {
ret.hits++;
}
}
else {
ret.hits++;
}
}
ret.source[num].hits = data.lines[num];
ret.source[num].miss = isMiss;
});
ret.percent = ret.hits / ret.sloc * 100;
return ret;
};