"use strict";
var crypto = require("crypto"),
esprima = require("esprima"),
escodegen = require("escodegen"),
estraverse = require("estraverse"),
// Maps for instrumenting
noReplaceMap = require("./instrumentor-config").noReplaceMap,
allowedReplacements = require("./instrumentor-config").allowedReplacements;
/*
Public: Instrument a string of JS code using the SteamShovel instrumentor.
This function executes synchronously.
data - The raw JavaScript code to instrument
filename - The filename of the code in question, for inclusion in
the instrument map
incorporateMap - Whether to include the map (defaults to true, but useful
for testing output without the additional noise of the
map.)
Returns a string containing the instrumented code.
Examples
code = instrumentCode("function(a) { return a; }", "myfile.js");
*/
module.exports = function instrumentCode(data, filename, incorporateMap) {
filename = filename || "implicit-filename";
incorporateMap = incorporateMap === false ? false : true;
var ast = esprima.parse(data, {loc: true, range: true, raw: true}),
filetag = crypto.createHash("md5").update(filename).digest("hex"),
// State and storage
id = 0,
code = null,
sourceMap = {};
// Process AST
estraverse.replace(ast, {
// Enter is where we mark nodes as noReplace
// which prevents bugs around weird edge cases, which I'll probably
// discover as time goes on.
//
// We also record a stack path to be provided to the instrumentor
// that lets us know our place in the AST when the instrument recorder
// is called.
enter: preprocessNode,
// Leave is where we replace the actual nodes.
leave: function (node) {
if (node.noReplace)
return;
// If we're allowed to replace the node,
// replace it with a Sequence Expression.
if (~allowedReplacements.indexOf(node.type))
return (
id++,
sourceMap["id_" + filetag + "_" + id] = {
"loc": node.loc,
"range": node.range,
"results": [],
"stack": node.stackPath,
"type": node.type
},
sequenceExpression(
"id_" + filetag + "_" + id,
node
));
}
});
code = escodegen.generate(ast);
if (incorporateMap)
code = prependInstrumentorMap(data, filename, code, sourceMap);
return code;
};
/*
Public: Preprocess a node to save the AST stack/path into the node, and
to mark whether its children should be replaced or not.
data - The AST node as represented by Esprima.
Returns null.
Examples
preprocessNode(astNode);
*/
var preprocessNode = module.exports.preprocessNode =
function preprocessNode(node) {
if (!node.stackPath)
node.stackPath = [node.type];
// Now mark a path to the node.
Object.keys(node).forEach(function(nodekey) {
var prop = node[nodekey];
function saveStack(prop) {
// This property most likely isn't a node.
if (!prop || typeof prop !== "object" || !prop.type) return;
prop.stackPath = node.stackPath.concat(prop.type);
}
if (Array.isArray(prop))
prop.forEach(saveStack);
saveStack(prop);
});
var nodeRule = noReplaceMap[node.type];
if (!nodeRule) return;
// Convert the rule to an array so we can handle it using
// the same logic.
//
// Strings and arrays just wholesale exclude the child nodes
// of the current node where they match.
if (nodeRule instanceof String)
nodeRule = [nodeRule];
if (nodeRule instanceof Array) {
nodeRule.forEach(function(property) {
if (!node[property]) return;
if (node[property] instanceof Array)
return node[property].forEach(function(item) {
item.noReplace = true;
});
node[property].noReplace = true;
});
}
// Whereas this more verbose object style allows
// exclusion based on subproperty matches.
if (nodeRule instanceof Object) {
Object.keys(nodeRule).forEach(function(property) {
if (!node[property]) return;
var exclude =
Object
.keys(nodeRule[property])
.reduce(function(prev, cur) {
if (!prev) return prev;
return (
node[property][cur] ===
nodeRule[property][cur]);
}, true);
if (exclude) node[property].noReplace = true;
});
}
};
/*
Public: Generates a replacement SequenceExpression for a given node,
which includes a call to an instrument function as its first argument.
id - A unique ID string to mark the call
data - The AST node as represented by Esprima.
Returns an object representing a replacement node.
Examples
sequenceExpression(id, astNode);
*/
var sequenceExpression = module.exports.sequenceExpression =
function sequenceExpression(id, node) {
return {
"type": "SequenceExpression",
"expressions": [
{
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "instrumentor_record"
},
"arguments": [
{
"type": "Literal",
"value": id
}
]
},
node
]
}
};
/*
Public: Given a string representation of JavaScript sourcecode, a filename,
an AST representing the rewritten, instrumented code, and a map containing
metainformation about the instrument probes themselves, this function
prepends a string/source representation of the instrument probes, as well
as the sourcecode of the function that performs the instrumentation itself.
source - The original sourcecode of the file.
filename - A unique filename representing the path to the
uninstrumented code on disk.
code - The instrumented AST to be generated to source
sourceMap - A map of information about each instrument probe and
containing the sourcecode of the file.
Returns the instrumented JavaScript sourcecode to be executed or written
to disk.
Examples
prependInstrumentorMap(source, filename, code, sourceMap);
*/
var prependInstrumentorMap = module.exports.prependInstrumentorMap =
function prependInstrumentorMap(source, filename, code, sourceMap) {
var filetag = crypto.createHash("md5").update(filename).digest("hex"),
sourceObject = { "source": String(source), "tag": filetag },
originalSource = JSON.stringify(sourceObject),
sourceKey = JSON.stringify("source_" + filename);
var initialiser =
"if (typeof __instrumentor_map__ === 'undefined') {" +
"__instrumentor_map__ = {};" +
"}\n" +
"if (!__instrumentor_map__[" + sourceKey + "]) {" +
"__instrumentor_map__[" + sourceKey + "] = " + originalSource + ";"+
"}\n";
Object.keys(sourceMap).forEach(function(key) {
initialiser +=
"if (!__instrumentor_map__." + key + ") {" +
" __instrumentor_map__." + key + " = " +
JSON.stringify(sourceMap[key]) +
";}\n";
});
initialiser += "\n" + require("./instrumentor-record").toString() + "\n";
return initialiser + code;
};
"use strict";
var allowedReplacements = [
"AssignmentExpression",
"ArrayExpression",
"ArrayPattern",
"ArrowFunctionExpression",
"BinaryExpression",
"CallExpression",
"ConditionalExpression",
"FunctionExpression",
"Identifier",
"LogicalExpression",
"MemberExpression",
"NewExpression",
"ObjectExpression",
"ObjectPattern",
"UnaryExpression",
"UpdateExpression"
];
var noReplaceMap = {
// If we don't do this, our instrument code removes the context
// from member expression function calls and returns the value
// without the context required by some 'methods'.
//
// Without this, calls like array.indexOf() would break.
"CallExpression": {
"callee": {
"type": "MemberExpression"
}
},
// We can't put SequenceExpressions on the left side of
// AssignmentExpressions.
//
// (e.g. (abc, def) = value;)
"AssignmentExpression": ["left"],
// Nor can we replace the id component of VariableDeclarators.
// (e.g. var [id] = value)
"VariableDeclarator": ["id"],
// The components of MemberExpressions should not be touched.
// (e.g. (instrument, abc).(instrument, def) from `abc.def`.)
"MemberExpression": ["object", "property"],
// The id component of Functions should not be touched.
// (e.g. function (instrument, abc)() {} from `function abc() {}`.)
// The parameters of FunctionExpressions should not be touched.
// (e.g. function abc((instrument,a)) {} from `function abc(a) {}`.)
"FunctionExpression": ["id", "params"],
"FunctionDeclaration": ["id", "params"],
// The properties of Objects should not be touched.
// (e.g. {(instrument,a)): b} from `{a:b}`.)
"Property": ["key"],
// The parameter of a catch clause should not be touched.
// (e.g. catch (instrument,a) { ... } from `catch (a) { ... }`.)
"CatchClause": ["param"],
// The argument of UpdateExpressions should not be touched.
// (e.g. (instrument,a)++ from `a++`.)
"UpdateExpression": ["argument"],
// The argument of the UnaryExpression `typeof` should not be touched.
// (e.g. typeof (instrument,a) from `typeof a`.)
"UnaryExpression": ["argument"],
// The left side of a ForInStatement should not be touched.
// (e.g. for ((instrument,a) in (instrument,b)) from `for (a in b)`.)
"ForInStatement": ["left"]
};
module.exports = {
noReplaceMap: noReplaceMap,
allowedReplacements: allowedReplacements
};
"use strict";
module.exports = {
"basic": require("./basic")
};
"use strict";
// Basic statistics for generating lines, conditions, expressions
// and functions covered.
//
// All these functions return a map, containing three values:
//
// eg.
// { covered: 15, total: 25, percengage: 0.6 }
var util = require("./utils");
module.exports = function generateStatistics(inputData, key) {
var self = module.exports;
if (!inputData)
throw new Error("Input was undefined.");
return {
"lines": self.lines(inputData, key),
"conditions": self.conditions(inputData, key),
"expressions": self.expressions(inputData, key),
"functions": self.functions(inputData, key)
};
};
module.exports.lines = function(inputData, key) {
var covered = 0, total = 0,
getValue = util.calculateValue(inputData),
uniqueLines = {};
total =
Object.keys(inputData)
.filter(function(key) {
return key.indexOf("source_") === 0;
})
.filter(function(index) {
if (!key) return true;
return inputData[index].tag === key;
})
.reduce(function(acc, cur) {
return acc + util.sloc(inputData[cur].source);
}, 0);
Object.keys(inputData)
.filter(function(index) {
return index.indexOf("id_" + (key || "")) === 0;
})
.forEach(function(key) {
var item = inputData[key],
id = key.split("_")[1],
start = item.loc.start.line,
results = item.results;
if (results.length)
uniqueLines[id + "_" + start] =
Math.max(
getValue(item),
uniqueLines[id + "_" + start] || 0);
});
covered =
Object.keys(uniqueLines)
.reduce(function(acc, cur) {
return acc + uniqueLines[cur];
}, 0);
return {
covered: covered,
total: total,
percentage: util.percentage(covered, total)
};
};
module.exports.conditions = function(inputData, key) {
var covered = 0, total = 0,
getValue = util.calculateValue(inputData),
instruments =
util.getInstruments(inputData, key)
.filter(util.isOfType([
"IfStatement",
"LogicalExpression",
"ConditionalExpression",
]));
total = instruments.length;
covered =
instruments
.filter(util.hasResults)
.reduce(function(count, item) {
return count + getValue(item);
}, 0);
return {
covered: covered,
total: total,
percentage: util.percentage(covered, total)
};
};
module.exports.expressions = function(inputData, key) {
var covered = 0, total = 0,
getValue = util.calculateValue(inputData),
instruments = util.getInstruments(inputData, key);
total = instruments.length;
covered =
instruments
.filter(util.hasResults)
.reduce(function(count, item) {
return count + getValue(item);
}, 0);
return {
covered: covered,
total: total,
percentage: util.percentage(covered, total)
};
};
module.exports.functions = function(inputData, key) {
var covered = 0, total = 0,
getValue = util.calculateValue(inputData),
instruments =
util.getInstruments(inputData, key)
.filter(util.isOfType([
"FunctionDeclaration",
"FunctionExpression",
]));
total = instruments.length;
covered =
instruments
.filter(util.hasResults)
.reduce(function(count, item) {
return count + getValue(item);
}, 0);
return {
covered: covered,
total: total,
percentage: util.percentage(covered, total)
};
};
"use strict";
var util = module.exports = {};
util.sloc = function(sourceInput) {
sourceInput = String(sourceInput || "");
return (
sourceInput.split(/\r?\n/ig)
.filter(util.lineIsWhitespace)
.reduce(function(acc, cur) {
cur =
cur .split(/\/\//)
.shift()
.replace(/\/\*.*?\*\//g, "")
.trim();
if (~cur.indexOf("/*"))
acc.inComment = true,
cur = cur.split(/\/\*/).shift().trim();
if (~cur.indexOf("*/"))
acc.inComment = false,
cur = cur.split("*/").slice(1).join("*/").trim();
if (!cur.length)
return acc;
if (!acc.inComment)
acc.count ++;
return acc;
}, { count: 0, inComment: false})
.count
);
}
util.lineIsWhitespace = function(line) {
line = String(line || "");
return !line.match(/^\s*$/);
}
util.shallowestCall = function shallowestCall(inputData) {
if (inputData.__shallowestCall !== undefined)
return inputData.__shallowestCall;
return inputData.__shallowestCall = (
util.getInstruments(inputData)
.filter(util.hasResults)
.reduce(function(prev, cur) {
return cur.results.reduce(function(prev, cur) {
return cur.depth < prev ? cur.depth : prev;
}, prev);
}, Infinity));
};
util.getInstruments = function getInstruments(inputData, scope) {
return (
Object.keys(inputData)
.filter(function(key) {
return key.indexOf("id_" + (scope || "")) === 0;
})
.map(function(key) {
inputData[key].key = key;
return inputData[key];
}));
};
util.isOfType = function isOfType(types) {
types = types instanceof Array ? types : [types];
return function(item) {
return types.reduce(function(acc, cur) {
return acc || ~item.stack.indexOf(cur);
}, false);
};
};
util.hasResults = function hasResults(item) {
return (item && item.results && item.results.length);
};
util.calculateValue = function calculateValue(inputData) {
var shallowMark = util.shallowestCall(inputData),
graceThreshold = 5,
inverseDampingFactor = 1.25;
return function(item) {
return (
Math.max.apply(Math,
item.results.map(function(result) {
// Calculate inverse logarithm
var relativeDepth = (result.depth - shallowMark) + 1;
relativeDepth = relativeDepth - graceThreshold;
relativeDepth = relativeDepth <= 0 ? 1 : relativeDepth;
return 1 / (
Math.pow(inverseDampingFactor, relativeDepth) /
inverseDampingFactor);
})));
}
}
util.getMinimumDepth = function getMinimumDepth(item) {
return Math.min.apply(Math, item.results.map(function(result) {
return result.depth;
}));
};
util.percentage = function percentage(a, b) {
return (+((a/b)*10000)|0) / 100;
};
util.generateIterationMap = function generateIterationMap(inputData) {
return (
util.getInstruments(inputData)
.reduce(function(acc, cur) {
return acc.concat(cur.results.map(function(result) {
return {
invocation: result.invocation,
key: cur.key,
loc: cur.loc.start,
kind: cur.stack[cur.stack.length-1],
depth: result.depth,
time: result.time,
timeOffset: result.timeOffset,
memoryUsage: result.memoryUsage,
milestone: result.milestone
};
}));
}, [])
.sort(function(a, b) {
return a.invocation - b.invocation;
})
);
};
util.generateMemoryStats = function generateMemoryStats(inputData) {
var defaults = { rss: 0, heapTotal: 0, heapUsed: 0 },
instruments = util.getInstruments(inputData),
iterationMap = util.generateIterationMap(inputData);
function mapper(result) {
var prev = result.invocation > 0 ? result.invocation - 1 : 0;
prev = iterationMap[prev].memoryUsage;
return {
rss: result.memoryUsage.rss - prev.rss,
heapTotal: result.memoryUsage.heapTotal - prev.heapTotal,
heapUsed: result.memoryUsage.heapUsed - prev.heapUsed,
};
}
function reducer(acc, cur, idx, array) {
var factor = 1 / array.length;
acc.rss += cur.rss * factor;
acc.heapTotal += cur.heapTotal * factor;
acc.heapUsed += cur.heapUsed * factor;
return acc;
}
for (var i = 0; i < instruments.length; i++)
instruments[i].avgMemChanges =
instruments[i]
.results.map(mapper)
.reduce(reducer, defaults);
};