Coverage Report

KindCoveredTotalPercentage
lines101.4611644522.8%
conditions25.9034210823.98%
expressions451.806181324.92%
functions34.0892311729.13%

Key

SteamShovel uses the stack depth to determine how directly code was tested. Code which was not tested directly is not considered highly in the coverage output.

You-should-be-ashamedAbysmalWorsePoorAverageGoodGreatPerfectUnevaluated

lib/instrumentor.js

KindCoveredTotalPercentage
lines50.381846676.33%
conditions20.315032581.26%
expressions205.4543127973.63%
functions14.322141784.24%
/*global require:true, module:true */

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,
	noPrependMap		= require("./instrumentor-config").noPrependMap,
	allowedPrepend		= require("./instrumentor-config").allowedPrepend;

// Local definitions for functions in this file
var callExpression,
	expressionStatement,
	fileID,
	prependInstrumentorMap,
	preprocessNode;

/*
	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.)
	returnAsObject	-	Return the data as an object containing the instrumented
						code and the source map.

	Returns a string containing the instrumented code.

	Examples

		code = instrumentCode("function(a) { return a; }", "myfile.js");

*/

module.exports =
	function instrumentCode(data, filename, incorporateMap, returnAsObject) {

	filename = filename || "implicit-filename";
	incorporateMap = incorporateMap === false ? false : true;

	var esprimaOptions = {loc: true, range: true, raw: true, comment: true},
		ast = esprima.parse(data, esprimaOptions),
		filetag = fileID(filename),
		comments = ast.comments,

		// State and storage
		id = 0,
		code = null,
		sourceMap = {};

	// Add metadata
	sourceMap.filetag = filetag;
	sourceMap.filename = filename;

	// Add raw sourcecode
	sourceMap.source = String(data);

	// Bucket for instruments
	sourceMap.instruments = {};

	// Add comment ranges to sourceMap
	sourceMap.comments = comments.map(function(comment) {
		return comment.range;
	});

	function sourceMapAdd(id, node, line) {
		if (sourceMap.instruments[id])
			throw new Error("Instrument error: Instrument already exists!");

		sourceMap.instruments[id] = {
			"loc": node.loc,
			"range": node.range,
			"results": [],
			"stack": node.stackPath,
			"type": node.type,
			"line": !!line
		};
	}

	// 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) {

			// Does this node have a body? If so we can replace its contents.
			if (node.body && node.body.length) {
				node.body =
					[].slice.call(node.body, 0)
					.reduce(function(body, node) {

						if (!~allowedPrepend.indexOf(node.type))
							return body.concat(node);

						id++;
						sourceMapAdd(id, node, true);

						return body.concat(
							expressionStatement(
								callExpression(filetag, id)
							),
							node
						);

					}, []);
			}

			if (node.noReplace)
				return;

			// If we're allowed to replace the node,
			// replace it with a Call Expression.

			if (~allowedReplacements.indexOf(node.type))
				return (
					id ++,
					sourceMapAdd(id, node, false),
					callExpression(filetag, id, node)
				);
		}
	});

	code = escodegen.generate(ast);

	if (incorporateMap)
		code = prependInstrumentorMap(code, sourceMap);

	if (returnAsObject)
		return { "code": code, "map": sourceMap };

	return code;
};

module.exports.withmap = function(data, filename, incorporateMap) {
	return module.exports(data, filename, incorporateMap, true);
};

/*
	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);

*/

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 CallExpression for a given node,
	which invokes the instrument function, passing an id and the resolved
	value of the original expression as the first and second arguments.

	id				-	A unique ID string to mark the call
	node			-	The AST node as represented by Esprima.

	Returns an object representing a replacement node.

	Examples

		callExpression(id, astNode);

*/

callExpression = module.exports.callExpression =
	function callExpression(filetag, id, node) {

	var callArgs = [
		{
			"type": "Literal",
			"value": filetag
		},
		{
			"type": "Literal",
			"value": id
		}
	];

	if (!!node) callArgs.push(node);

	return {
		"type": "CallExpression",
		"callee": {
			"type": "Identifier",
			"name": "instrumentor_record"
		},
		"arguments": callArgs
	};
};

/*
	Public: Generates an ExpressionStatement AST node, containing an input
	expression.

	node	-	The expression's AST node as represented by Esprima.

	Returns an object representing a replacement node.

	Examples

		expressionStatement(astNode);

*/

expressionStatement = module.exports.expressionStatement =
	function expressionStatement(node) {
	return {
		"type": "ExpressionStatement",
		"expression": node
	};
};

/*
	Public: Given a string representation of JavaScript sourcecode 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.

	code			-	The source to which the instrument map should be
						appended
	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(code, sourceMap);

*/
prependInstrumentorMap = module.exports.prependInstrumentorMap =
	function prependInstrumentorMap(code, sourceMap) {

	var filetag = sourceMap.filetag,
		map = JSON.stringify(sourceMap);

	return [

		// Map initialisation
		"if (typeof __instrumentor_map__ === 'undefined') {",
			"__instrumentor_map__ = {};",
		"}",

		// Map for key
		"if (typeof __instrumentor_map__." + filetag + " === 'undefined') {",
			"__instrumentor_map__." + filetag + " = " + map + ";",
		"}",

		// Instrumentor
		require("./instrumentor-record").toString(),

		// Include the code
		code

	].join("\n");
};

/*
	Public: Given a string, this function will generate a string md5 digest
	which can be safely used barely in JS literal context.

	filename		-	The filename to digest

	Returns the digest string.

	Examples

		fileID("myfile.js");

*/

fileID = module.exports.fileID =
	function fileID(id) {

	return "i" + crypto.createHash("md5").update(id).digest("hex");
};

lib/instrumentor-config.js

KindCoveredTotalPercentage
lines0.0864661.44%
conditions000%
expressions1.16733811.44%
functions000%
"use strict";

// These are expression-context items which can be completely wrapped.
var allowedReplacements = [
	"AssignmentExpression",
	"ArrayExpression",
	"ArrayPattern",
	"ArrowFunctionExpression",
	"BinaryExpression",
	"CallExpression",
	"ConditionalExpression",
	"FunctionExpression",
	"Identifier",
	"Literal",
	"LogicalExpression",
	"MemberExpression",
	"NewExpression",
	"ObjectExpression",
	"ObjectPattern",
	"UnaryExpression",
	"UpdateExpression"
];

// These are block/statement level constructs which cannot be wrapped and must
// be prepended.
var allowedPrepend = [
	"BlockStatement",
	"BreakStatement",
	"CatchClause",
	"ContinueStatement",
	"DirectiveStatement",
	"DoWhileStatement",
	"DebuggerStatement",
	"EmptyStatement",
	"ExportDeclaration",
	"ExpressionStatement",
	"ForStatement",
	"ForInStatement",
	"ForOfStatement",
	"FunctionDeclaration",
	"IfStatement",
	"LabeledStatement",
	"ReturnStatement",
	"SwitchStatement",
	"SwitchCase",
	"ThisExpression",
	"ThrowStatement",
	"TryStatement",
	"VariableDeclaration",
	"VariableDeclarator",
	"WhileStatement",
	"WithStatement",
	"YieldExpression"
];

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"]
};

var noPrependMap = {

};

module.exports = {
	noReplaceMap: noReplaceMap,
	allowedReplacements: allowedReplacements,
	allowedPrepend: allowedPrepend,
	noPrependMap: noPrependMap
};

lib/stats/index.js

KindCoveredTotalPercentage
lines0.1374326.87%
conditions000%
expressions0.4123166.87%
functions000%
"use strict";

module.exports = {
	"basic": require("./basic")
};

lib/stats/basic.js

KindCoveredTotalPercentage
lines27.580883774.54%
conditions0.4140%
expressions140.4017616883.57%
functions10.650921288.75%
"use strict";

var util = require("./utils");


/*
	Public: Given a map of instrument data, this function returns a map of
	generated statistics regarding coverage, split into four groups:
	lines, conditions, expressions, and functions. This is a convenience
	function which simply calls the other four functions in this file.

	For more detail on how each statistic is calculated, refer to the notes
	against each function.

	instrumentData	-	The map of instrument data.
	filetag			-	Specify the tag associated with a specific file,
						allowing statistics to be scoped a single file.

	Returns a map of maps containing statistics grouped into the categories
	lines, conditions, expressions, and functions.

	Examples

		stats = stats.basic(instrumentData);

*/
module.exports = function generateStatistics(instrumentData, key) {
	var self = module.exports;

	if (!instrumentData)
		throw new Error("Input was undefined.");

	return {
		"lines":		self.lines(instrumentData, key),
		"conditions":	self.conditions(instrumentData, key),
		"expressions":	self.expressions(instrumentData, key),
		"functions":	self.functions(instrumentData, key)
	};
};

/*
	Public: Given a map of instrument data, this function returns a map of
	generated statistics regarding code coverage on a per-line level.

	This is ascertained by filtering the probes which were identified at
	the time of instrumentation as 'line' probes — that is, they don't execute
	in expression context, and are tied to statements, rather than expressions.

	instrumentData	-	The map of instrument data.
	filetag			-	Specify the tag associated with a specific file,
						allowing statistics to be scoped a single file.

	Returns a map of the coverage statistics associated with these probes. The
	map contains a count of the number of probes covered, a total number of
	probes, and a percentage of probes covered.

	For example:

		{ covered: 15, total: 25, percentage: 0.6 }

	Examples

		stats = stats.basic.lines(instrumentData);

*/
module.exports.lines = function(instrumentData, key) {
	var totalInstruments, coveredInstruments, covered, total = 0,
		getValue = util.calculateValue(instrumentData);

	totalInstruments =
		util.getInstruments(instrumentData, key)
			.filter(util.withkey("line"));

	total = totalInstruments.length;

	coveredInstruments =
		totalInstruments.filter(util.hasResults);

	covered =
		coveredInstruments
			.reduce(function(count, instrument) {
				return count + getValue(instrument);
			}, 0);

	return {
		covered: covered,
		total: total,
		percentage: util.percentage(covered, total)
	};
};

/*
	Public: Given a map of instrument data, this function returns a map of
	generated statistics regarding code coverage on a per-expression level.

	This is ascertained by filtering the probes which were identified at
	the time of instrumentation as 'expression' probes — that is, they execute
	in expression context, and are tied to expressions, rather than statements.

	instrumentData	-	The map of instrument data.
	filetag			-	Specify the tag associated with a specific file,
						allowing statistics to be scoped a single file.

	Returns a map of the coverage statistics associated with these probes. The
	map contains a count of the number of probes covered, a total number of
	probes, and a percentage of probes covered.

	For example:

		{ covered: 15, total: 25, percentage: 0.6 }

	Examples

		stats = stats.basic.expressions(instrumentData);

*/
module.exports.expressions = function(inputData, key) {
	var covered = 0, total = 0,
		getValue = util.calculateValue(inputData),
		instruments =
			util.getInstruments(inputData, key)
				.filter(function(item) {
					return !item.line;
				});

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

/*
	Public: Given a map of instrument data, this function returns a map of
	generated statistics regarding code coverage within conditional statements
	in the instrumented code.

	This is ascertained by filtering the probes which were identified at
	the time of instrumentation as being conditional expressions or statements,
	or exist within conditional expressions or statements.

	In this instance, the 'total' value returned is the number of probes which
	correspond *explicitly* to conditionals in the document. However, the
	coverage value returned is calculated based on the coverage of the probes
	directly associated with these conditionals *as well as* the probes for the
	expressions within them.

	In order for a conditional to be considered completely tested, all the
	codepaths within it must have been evaluated at appropriate depth.

	instrumentData	-	The map of instrument data.
	filetag			-	Specify the tag associated with a specific file,
						allowing statistics to be scoped a single file.

	Returns a map of the coverage statistics associated with these probes. The
	map contains a count of the number of probes covered, a total number of
	probes, and a percentage of probes covered.

	For example:

		{ covered: 15, total: 25, percentage: 0.6 }

	Examples

		stats = stats.basic.conditions(instrumentData);

*/
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"
					]));

	var strictTotal =
		instruments.filter(function(instrument) {
			return 	~[
						"IfStatement",
						"LogicalExpression",
						"ConditionalExpression"
					]
					.indexOf(instrument.type);
		})
		.length;

	total = instruments.length;
	covered =
		instruments
			.filter(util.hasResults)
			.reduce(function(count, instrument) {
				return count + getValue(instrument);
			}, 0);

	return {
		covered: covered * (strictTotal / total),
		total: strictTotal,
		percentage: util.percentage(covered, total)
	};
};

/*
	Public: Given a map of instrument data, this function returns a map of
	generated statistics regarding code coverage within function declarations
	and expressions in the instrumented code.

	This is ascertained by filtering the probes which were identified at
	the time of instrumentation as being function expressions or declarations,
	or exist within function expressions or declarations.

	In this instance, the 'total' value returned is the number of probes which
	correspond *explicitly* to functions in the document. However, the coverage
	value returned is calculated based on the coverage of the probes directly
	associated with these declarations and expressions *as well as* the probes
	for the expressions within them.

	In order for a function to be considered completely tested, all the
	codepaths within it must have been evaluated at appropriate depth.

	instrumentData	-	The map of instrument data.
	filetag			-	Specify the tag associated with a specific file,
						allowing statistics to be scoped a single file.

	Returns a map of the coverage statistics associated with these probes. The
	map contains a count of the number of probes covered, a total number of
	probes, and a percentage of probes covered.

	For example:

		{ covered: 15, total: 25, percentage: 0.6 }

	Examples

		stats = stats.basic.conditions(instrumentData);

*/
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"
					]));

	var strictTotal =
		instruments.filter(function(instrument) {
			return 	~[
						"FunctionDeclaration",
						"FunctionExpression"
					]
					.indexOf(instrument.type);
		})
		.length;

	total = instruments.length;

	covered =
		instruments
			.filter(util.hasResults)
			.reduce(function(count, item) {
				return count + getValue(item);
			}, 0);

	return {
		covered: covered  * (strictTotal / total),
		total: strictTotal,
		percentage: util.percentage(covered, total)
	};
};

lib/stats/utils.js

KindCoveredTotalPercentage
lines23.274526336.94%
conditions9.066041464.75%
expressions104.3702725241.41%
functions10.74142444.75%
// Utilities for generating statistics

var util = module.exports = {};

/*
	Public: Very lame memoiser function. Only looks at the first argument.

	fn				-	The function to memoise.

	Returns a wrapped function

	Examples

		myFunction = util.cache(function myFunction() { ... });

*/
util.functionCache = {};
util.cache = function cache(fn) {
	util.functionCache[fn.name] = {};

	return function(key) {
		if (typeof key === "object") {
			if (key["__cache_" + fn.name])
				return key["__cache_" + fn.name];

			Object.defineProperty(key, "__cache_" + fn.name, {
				value: fn.apply(util, arguments),
				enumerable: false
			});

			return key["__cache_" + fn.name];
		}

		if (typeof key !== "object" &&
			util.functionCache[fn.name][key])
				return util.functionCache[fn.name][key];

		return (
			util.functionCache[fn.name][key] =
				fn.apply(util, arguments)
		);
	};
};

/*
	Public: For use in mapping. Returns a function that returns the specified
	key from its input data.

	key				-	The key to return.

	Returns the function that performs the property access.

	Examples

		// Returns all the data properties from the objects in the array
		var arr = arr.map(util.withkey("data"));

*/
util.withkey = util.cache(function withkey(key) {
	return function(data) {
		return data[key];
	};
});

/*
	Public: For use in filtering. Returns a function that returns true for
	instruments of the type (AST Node type) specified in the wrapper function.

	type			-	The AST Node type which will trigger a truthy return

	Returns true or false

	Examples

		var varDecs = instruments.map(util.isOfType("VariableDeclaration"));

*/
util.isOfType = util.cache(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);
	};
});

/*
	Public: For use in filtering. Returns true for a given instrument which has
	results.

	instrument		-	An instrument to test

	Returns true if the instrument has results, false if not.

	Examples

		var instrumentsWithResults = instruments.map(util.hasResults);

*/
util.hasResults = function hasResults(instrument) {
	return (instrument && instrument.results && instrument.results.length);
};

/*
	Public: Returns rounded percentage for the percent a of b.

	Returns a number representing a percentage.

*/
util.percentage = function percentage(a, b) {
	return (+((a/b)*10000)|0) / 100;
};

/*
	Public: Gets the instrument map from the raw instrumentation data.

	instrumentData	-	The complete, raw instrument Data
	filetag			-	Only return instruments matching this filetag

	Returns an array of instruments.

	Examples

		var instruments = util.getInstruments(instrumentData);

*/
util.getInstruments = function getInstruments(instrumentData, filetag) {
	if (instrumentData instanceof Array)
		return instrumentData;

	return Object.keys(instrumentData).reduce(function(acc, cur) {
		var instruments = instrumentData[cur].instruments;

		if (filetag && cur !== filetag) return acc;

		return acc.concat(Object.keys(instruments).map(function(key) {
			instruments[key].id = key;
			instruments[key].filetag = cur;
			return instruments[key];
		}));
	}, []);
};

/*
	Public: Returns the stack depth of the shallowest call across all
	instruments in the instrument map

	instrumentMap	-	The complete instrument map
	filetag			-	Only measure the depth of instruments matching this
						filetag

	Returns a function which calculates the value of a given instrument.

	Examples

		var shallowestCallDepth = util.shallowestCall(instrumentMap);

*/
util.shallowestCall = function shallowestCall(instrumentData, key) {
	return Math.min.apply(Math,
		util.getInstruments(instrumentData, key)
			.filter(util.hasResults)
			.map(function(item) {
				return Math.min.apply(Math,
					item.results.map(util.withkey("depth")));
			})
	);
};

/*
	Public: Given the entire instrument map, returns a function which calculates
	the 'coverage' value of a given instrument.

	instrumentMap	-	The complete instrument map

	Returns a function which calculates the value of a given instrument.

	Examples

		var getValue = util.calculateValue(instrumentMap);
		var value = getValue(instrument);

*/
util.calculateValue = function calculateValue(instrumentMap) {
	var shallowMark = util.shallowestCall(instrumentMap),
		graceThreshold = 5,
		inverseDampingFactor = 1.25;

	return function(item) {
		return (
			Math.max.apply(Math,
				item.results
					.map(util.withkey("depth"))
					.map(util.calculate.bind(	null,
												shallowMark,
												graceThreshold,
												inverseDampingFactor ))));
	};
};

/*
	Public: Given a shallowMark, graceThreshold, and depth, this function runs
	the per-node calculation responsible for delivering the coverage value.

	shallowMark	-	The shallowest measured call in the test suite. This is an
					approximation of the actual call stack depth of the test,
					and is really a balancing term. The true depth of a call as
					far as steamshovel is concerned is 'depth - shallowMark' to
					determine the relative depth.
	grace		-	The relative depth threshold below which the score of a
					given result will not be decreased.
	damping		-	A fitting parameter by which the calculation is made.
					SteamShovel uses a default of 1.25. This value is purely
					one that seemed to produce 'fair' results in testing.
	depth		-	The depth of the invocation.

	Returns a number representing the score of the given instrument. This score
	is derived from the straightforward equation:

		score = 1 / ((damping ^ relativeDepth) / damping)

	relativeDepth is calculated by subtracting the shallowMark and then the
	graceThreshold values from the input depth, and taking the result, or 1 —
	whichever is larger.

	Examples

		nodeValue = calculate(5, 5, 1.25, 15);

*/
util.calculate = function calculate(	shallowMark,
										graceThreshold,
										inverseDampingFactor,
										depth
									) {

	// Calculate inverse logarithm
	var relativeDepth = (depth - shallowMark) + 1;
		relativeDepth = relativeDepth - graceThreshold;
		relativeDepth = relativeDepth <= 1 ? 1 : relativeDepth;

	return 1 / (
		Math.pow(inverseDampingFactor, relativeDepth) /
			inverseDampingFactor);
};

/*
	Public: Given the entire instrument map, burns memory usage information
	calculated from the probe data back into the instrument map.

	This function is poorly realised and is likely to change.

	instrumentMap	-	The complete instrument map

	Returns the mutated instrument map.

	Examples

		var instruments = util.generateMemoryStats(instrumentMap);

*/
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] ? iterationMap[prev].memoryUsage : defaults;

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

	return instruments;
};

/*
	Public: Given the entire instrument map, return a list of probe calls with
	their associated data.

	instrumentMap	-	The complete instrument map
	filetag			-	Scope to instruments matching this filetag

	Returns a list of invocations (probe calls) with associated data.

	Examples

		var iterations = util.generateIterationMap(instrumentMap);

*/
util.generateIterationMap = function generateIterationMap(instrumentMap, key) {
	return (
		util.getInstruments(instrumentMap, key)
			.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,
						isLine:			!!cur.line
					};
				}));
			}, [])
			.sort(function(a, b) {
				return a.invocation - b.invocation;
			})
	);
};

lib/index.js

KindCoveredTotalPercentage
lines060%
conditions000%
expressions0210%
functions000%
"use strict";

// Export the reporter so that mocha can require it directly by name
module.exports = require("./reporter");

// Return the components
module.exports.instrument	= require("./instrumentor");
module.exports.process		= require("./process");
module.exports.reporter		= require("./reporter");
module.exports.recorder		= require("./instrumentor-record");

lib/reporter.js

KindCoveredTotalPercentage
lines0610%
conditions0110%
expressions01850%
functions0170%
"use strict";

/*global global:true, console:true, process:true, require:true */

// For monkey patching mocha, when I can work that out.
var mochaPath = process.argv[1].replace(/\/bin\/_mocha$/i, "");

var instrument		= require("./instrumentor"),
	stats			= require("./stats"),
	columnify		= require("columnify"),
	fs				= require("fs"),
	path			= require("path"),
	_				= require("lodash"),

	// Override require
	override		= require("./require-override");

function replaceIt(module) {
	// Dummy code for now.
	// In future, this is where we'd set the value of
	// __steamshovel_test_depth.
	var originalIt = module.it;
	module.it = originalIt || function shovelIt() {};
}

override(function(module, content, filename) {
	var ptr = module;

	while (ptr) {
		if (ptr.exports && ptr.exports.it)
			replaceIt(ptr.exports);

		ptr = ptr.parent;
	}
});

global.__instrumentor_map__ = {};
global.__steamshovel = true;
global.__steamshovel_milestone = null;
global.__steamshovel_record_expression = !!process.env.SHOVEL_RECORD_EXPRESSION;
global.__steamshovel_test_depth = 0;

function locateMap(done) {
	console.log("Scanning for shovelmap...");

	fs.readdir(process.cwd(), function(err, dir) {
		if (err) throw err;

		var complete = 0;

		dir.forEach(function(file) {
			file = path.join(process.cwd(), file, "./instruments.shovelmap");

			fs.stat(file, function(err, data) {
				complete ++;
				if (!err) {
					global.__instrumentor_map__ =
						_.merge(global.__instrumentor_map__, require(file));

					console.log("Located shovelmap at %s!", file);
				}

				if (complete === dir.length) {
					done();
				}
			});
		});
	});
}

function SteamShovel(runner) {
	console.log("Steam Shovel");

	var failures = [],
		suiteName = null;

	runner.suite.beforeAll(function(done) {
		console.log("Commencing coverage test!");
		locateMap(done);
	});

	runner.on('suite', function(suite){
		suiteName = suite.title;
	});

	runner.on('test', function(test){
		global.__steamshovel_milestone =
			(suiteName ? suiteName + ", " : "") + test.title;
	});

	runner.on('pass', function(test){
		process.stdout.write(".");
	});

	runner.on('fail', function(test, err){
		process.stdout.write("x");
		failures.push([test, err]);
	});

	runner.on('end', function(done){

		console.log("\n\n");

		if (failures.length) {
			console.error("Test failures occurred while processing!");

			return failures.forEach(function(failure) {
				var test = failure[0], err = failure[1];

				console.error("•\t%s\n\n\t%s\n\n", test.title, err.stack);
			});
		}

		var coverageData = global.__instrumentor_map__ || {},
			basicStats = stats.basic(coverageData),
			arrayTransformedStats = Object.keys(basicStats).map(function(key) {
				return {
					"kind": key,
					"covered": basicStats[key].covered,
					"total": basicStats[key].total,
					"percentage": basicStats[key].percentage
				};
			});

		var outputTable = columnify(arrayTransformedStats, {
			columnSplitter: " | "
		});

		console.log(outputTable, "\n\n");

		var output = process.env.REPORTER_OUTPUT || "html",
			outputPath = process.env.OUTPUT_PATH;

		require("./outputs/" + output)(
			coverageData,
			outputPath,
			function(err, result) {
				if (err) throw err;

				console.log("\nReport written to disk.\n");
			}
		);
	});
}

module.exports = SteamShovel;

lib/process.js

KindCoveredTotalPercentage
lines0800%
conditions0290%
expressions03020%
functions0170%
"use strict";

var fs = require("fs"),
	path = require("path"),
	mkdirp = require("mkdirp"),
	instrument = require("./instrumentor"),
	assert = require("assert"),
	EventEmitter = require("events").EventEmitter;

/*
	Public: Instrument a file or folder using the SteamShovel instrumentor.

	inFile			-	The filesystem location of the input resource
	outFile			-	The filesystem destination for the instrumented resource
	emitter			-	An optional pre-defined event emitter to use when
						emitting status events.

	Returns

		emitter		-	An event emitter from which information about the
						instrumentation progress is dispatched.

	Examples

		instrumentor("myfile.js", "myfile-instrumented.js");

*/

module.exports = function instrumentTree(inFile, outFile, emitter) {
	var combinedMap = {}, isDirectory = false;

	// Is this the first call in the stack?
	// We know because every recursion will have an emitter.
	var firstCall = !emitter;

	emitter =
		emitter && emitter instanceof EventEmitter ? emitter :
			new EventEmitter();

	if (!inFile)
		throw new Error("You must specify an input file/directory.");

	if (!outFile)
		throw new Error("You must specify an output file/directory.");

	if (inFile instanceof Array) {
		assert(outFile instanceof Array,
				"If an array if input resources is provided, " +
				"the output must also be an array.");

		assert(outFile.length === inFile.length,
				"The lengths of the input and destination arrays must match.");


		// Loop through both arrays, but retain the same emitter
		inFile.forEach(function(file, index) {
			instrumentTree(file, outFile[index], emitter);
		});

		return emitter;

	} else {
		assert(typeof outFile === "string",
			"If the first parameter is not an array, the second mustn't be.");
	}

	fs.stat(inFile, function(err, stats) {
		if (err) return emitter.emit("error", err, inFile);

		if (stats.isDirectory()) {
			isDirectory = true;

			if (firstCall)
				emitter.on("dircomplete", function(dir) {
					if (dir === inFile) emitter.emit("_int_complete", inFile);
				});

			return module.exports.processDir(inFile, outFile, emitter);
		}

		if (firstCall)
			emitter.on("writefile", function(file) {
				if (file === inFile) emitter.emit("complete", inFile);
			});

		module.exports.processFile(inFile, outFile, emitter);
	});

	// This chunk writes an 'instruments.shovelmap' file into the directory
	// being instrumented. This file contains a map of all the data from all
	// the files which were instrumented, so if a given file is not included
	// in the test suite, its data will still appear in output.

	if (firstCall) {
		emitter.on("map", function(sourceMap) {
			combinedMap[sourceMap.filetag] = sourceMap;
		});

		emitter.on("_int_complete", function(inFile) {
			var writePath = path.join(outFile, "./instruments.shovelmap");

			fs.writeFile(
				writePath,
				"module.exports = " + JSON.stringify(combinedMap),
				function(err) {
					if (err) emitter.emit("maperror", err);
					emitter.emit("complete", inFile);
				});
		});
	}

	return emitter;
};

/*
	Public: Recursively instrument a folder/directory using the SteamShovel
	instrumentor.

	inDir			-	The filesystem location of the input resource
	outDir			-	The filesystem destination for the instrumented resource
	emitter			-	An optional pre-defined event emitter to use when
						emitting status events.

	Returns

		emitter		-	An event emitter from which information about the
						instrumentation progress is dispatched.

	Examples

		instrumentor("./lib", "./lib-cov");

*/

module.exports.processDir = function processDir(dir, out, emitter) {

	emitter =
		emitter && emitter instanceof EventEmitter ? emitter :
			new EventEmitter();

	emitter.emit("processdir", dir, out);

	try { fs.statSync(out); }
	catch (e) {
		emitter.emit("mkdir", out);
		mkdirp.mkdirp(out);
	}

	fs.readdir(dir, function(err, dirContents) {
		if (err) return emitter.emit("error", err, dir);

		emitter.emit("readdir", dir);

		dirContents.forEach(function(file) {

			var filePath = path.join(dir, file),
				outPath = path.join(out, file);

			module.exports(filePath, outPath, emitter);
		});

		var instrumentedFiles = 0;
		function checkComplete(file) {
			if (file instanceof Error)
				file = arguments[1];

			if (dirContents.reduce(function(acc, cur) {
					return acc || (dir + "/" + cur === file);
				}, false))
				instrumentedFiles ++;

			if (instrumentedFiles === dirContents.length) {
				emitter.removeListener("writefile", checkComplete);
				emitter.removeListener("dircomplete", checkComplete);
				emitter.emit("dircomplete", dir);
			}
		}

		emitter.on("writefile", 	checkComplete);
		emitter.on("dircomplete",	checkComplete);
	});

	return emitter;
};

/*
	Public: Instrument a single file using the SteamShovel instrumentor.

	This function will ignore files that the instrumentor cannot parse, files
	which do not have a `.js` file extension, and files which contain

	inFile			-	The filesystem location of the input resource
	outFile			-	The filesystem destination for the instrumented resource
	emitter			-	An optional pre-defined event emitter to use when
						emitting status events.

	Returns

		emitter		-	An event emitter from which information about the
						instrumentation progress is dispatched.

	Examples

		instrumentor("./lib/myfile.js", "./lib-cov/myfile.js");

*/

module.exports.processFile = function processFile(file, out, emitter) {

	emitter =
		emitter && emitter instanceof EventEmitter ? emitter :
			new EventEmitter();

	if (!file.match(/\.js$/i))
		return	emitter.emit("nojs", file),
				fs	.createReadStream(file)
					.pipe(fs.createWriteStream(out))
					.on("close", function() {
						emitter.emit("writefile", file);
					});

	fs.readFile(file, function(err, data) {
		if (err) return emitter.emit("error", err, file);

		emitter.emit("readfile", file);

		var instrumentResult,
			code;
			data = String(data);

		if (~data.indexOf("steamShovel:" + "ignore")) {
			emitter.emit("ignore", file);

		} else {
			try {
				instrumentResult = instrument.withmap(data, file);
				code = instrumentResult.code;

			} catch (err) {
				emitter.emit("instrumenterror", err, file);
			}
		}

		fs.writeFile(out, code || data, function(err) {
			if (err) return emitter.emit("error", err, file);

			if (instrumentResult && instrumentResult.map) {
				emitter.emit("map", instrumentResult.map);
			}

			emitter.emit("writefile", file);
		});
	});

	return emitter;
};

lib/require-override.js

KindCoveredTotalPercentage
lines0240%
conditions0120%
expressions0900%
functions040%
var instrument	= require("./instrumentor"),
	fs			= require("fs"),
	path		= require("path");

var originalFunction = require.extensions[".js"];

function stripBOM(content) {
	// Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
	// because the buffer-to-string conversion in `fs.readFileSync()`
	// translates it to FEFF, the UTF-16 BOM.
	if (content.charCodeAt(0) === 0xFEFF) {
		content = content.slice(1);
	}
	return content;
}

function setup(override) {
	// Override require to perform auto-instrumentation!
	var instrumentCache = {};
	require.extensions[".js"] = function(module, filename) {
		var dir = __dirname.replace(/lib\-cov$/ig, "lib"),
			content;

		if (instrumentCache[filename]) {
			content = instrumentCache[filename];
		} else {
			content = stripBOM(fs.readFileSync(filename, 'utf8'));
		}

		// Bail out for cache.
		// Only auto-instrument where asked.
		// Don't auto-instrument any previously instrumented code.
		// Don't auto-instrument code marked as 'ignore'.
		// Don't auto-instrument files in a node_modules directory.
		// Don't auto-instrument the instrumentor.
		// Don't auto-instrument our tests.

		if (!instrumentCache[filename] &&
			process.env.AUTO_INSTRUMENT === "true" &&
			!~content.indexOf("__instrumentor_map__") &&
			!~content.indexOf("steamShovel:" + "ignore") &&
			!~filename.indexOf("node_modules") &&
			!~filename.indexOf("test") &&
			!~filename.indexOf(dir) &&
			!~filename.indexOf(path.normalize(dir + "/../test"))) {

			console.warn("Overriding require %s",filename);
			content = instrument(content, filename);
			instrumentCache[filename] = content;
		}

		module._compile(content, filename);

		if (override && override instanceof Function) {
			override(module, content, filename);
		}
	};
}

function restore() {
	require.extensions[".js"] = originalFunction;
}

module.exports = setup;
module.exports.restore = restore;

lib/outputs/html.js

KindCoveredTotalPercentage
lines0580%
conditions090%
expressions02100%
functions0100%
"use strict";

var jade	= require("jade"),
	fs		= require("fs"),
	util	= require("../stats/utils"),
	stats	= require("../stats/index");

var scoreClasses = [
	"perfect",
	"great",
	"good",
	"average",
	"poor",
	"worse",
	"abysmal",
	"you-should-be-ashamed"
].reverse();

var generateMap;

module.exports = function generateHTML(inputData, path, callback, template) {

	// Trickery to avoid indent hell
	if (!template)
		return module.exports.loadTemplate(
			generateHTML.bind(null, inputData, path, callback));

	var htmlOutput = [], sources;

	console.log("Getting sources...");
	sources = module.exports.getSources(inputData);

	console.log("Generating memory statistics...");
	util.generateMemoryStats(inputData);

	console.log("Rendering source buffers...");

	sources.forEach(function(source) {
		console.log("\tRendering %s",source.filename);

		htmlOutput.push({
			"stats":	stats.basic(inputData, source.key),
			"codeMap":	generateMap(inputData, source),
			"filename":	source.filename,
			"key":		source.key
		});
	});

	console.log("Loading template resources...");

	var script = fs.readFileSync(__dirname+"/templates/main.js","utf8"),
		styles = fs.readFileSync(__dirname+"/templates/main.css","utf8");

	console.log("Writing rendered template to disk...");

	fs.writeFileSync(path || "./report.html",
		htmlOutput = template({
			"script":	script,
			"style":	styles,
			"files":	htmlOutput,
			"stats":	stats.basic(inputData),
			"classes":	scoreClasses
		})
	);

	callback(null, htmlOutput);
};

module.exports.extension = "html";

generateMap = module.exports.generateMap =
	function generateMap(inputData, source) {
	var instrumentMap	= module.exports.preprocessMap(
									inputData, source.key),
		comments		= source.commentRanges,
		code			= source.source,
		map				= [],
		buffer			= "",
		bufferState		= false,
		pointer			= 0,
		iIdx			= 0;

	for (; pointer < code.length; pointer++) {

		for (; iIdx < instrumentMap.length; iIdx ++) {
			if (instrumentMap[iIdx].range[0] > pointer)
				break;



			if (pointer === instrumentMap[iIdx].range[0]) {
				map.push(buffer);
				buffer = "";
				map.push({
					"open": instrumentMap[iIdx]
				});
			}
		}

		if (bufferState !== (!!code[pointer].match(/\s+/) && buffer.length)) {
			bufferState = !bufferState;
			map.push(buffer);
			buffer = "";
		}

		buffer += code[pointer];

		for (iIdx = 0; iIdx < instrumentMap.length; iIdx ++) {
			if (instrumentMap[iIdx].range[1] > pointer)
				break;

			if (pointer === instrumentMap[iIdx].range[1]) {
				map.push(buffer);
				buffer = "";
				map.push({
					"close": instrumentMap[iIdx]
				});
			}
		}
	}

	if (buffer.length) {
		map.push(buffer);
		buffer = "";
	}

	return map;
};

module.exports.loadTemplate = function loadTemplate(cb) {
	// Flicked this back to synchronous to play nicely with
	// mocha, which wasn't waiting for this to finish.
	var data = fs.readFileSync(__dirname + "/templates/main.jade", "utf8");
	cb(jade.compile(data));
};

module.exports.preprocessMap = function preprocessMap(inputData, key) {
	return (
		util.getInstruments(inputData, key)
			.sort(function(a, b) {
				return a.range[0] - b.range[0];
			})
			.map(function(item) {
				item.score = util.calculateValue(inputData)(item);
				item.depth = Math.min.apply(Math,
								item.results.map(util.withkey("depth")));
				return item;
			}));
};

module.exports.getSources = function getSources(inputData) {
	return (
		Object.keys(inputData)
			.map(function(key) {
				return inputData[key];
			})
			.map(function(item) {
				return {
					"filename":	item.filename,
					"source": 	item.source,
					"key":		item.filetag
				};
			}));
};

lib/outputs/csv.js

KindCoveredTotalPercentage
lines0180%
conditions050%
expressions0860%
functions080%
"use strict";

var fs		= require("fs"),
	util	= require("../stats/utils"),
	stats	= require("../stats/index");

module.exports = function generateCSV(inputData, path, callback) {
	var iterations	= util.generateIterationMap(inputData),
		csvFile		= "",
		csvHeader	=
			Object.keys(iterations[0]).reduce(function keyProc(acc, cur) {
				var item = iterations[0][cur];

				if (item instanceof Object && !Array.isArray(item))
					return acc.concat(
							Object.keys(item)
								.map(function(key) {
									return cur + "." + key;
								}));

				return acc.concat(cur);
			}, [])
			.map(function(key) {
				return '"' + key + '"';
			})
			.join(",");

	csvFile = iterations.reduce(function(file, iteration) {
		return file + "\n" +
			Object.keys(iteration).reduce(function keyProc(acc, cur) {
				var item = iteration[cur];

				if (item instanceof Object && !Array.isArray(item))
					return acc.concat(
							Object.keys(item)
								.map(function(key) {
									return item[key];
								}));

				return acc.concat(item);
			}, [])
			.map(function(key) {
				return '"' + key + '"';
			})
			.join(",");

	}, csvHeader);


	// The intention is of course, to go async as soon as I can work
	// out how to stop mocha killing the script.
	fs.writeFileSync(path || "./report.csv", csvFile);
	callback(null, csvFile);
};

lib/outputs/json.js

KindCoveredTotalPercentage
lines070%
conditions010%
expressions0230%
functions010%
"use strict";

var fs		= require("fs"),
	util	= require("../stats/utils"),
	stats	= require("../stats/index");

module.exports = function generateJSON(inputData, path, callback) {
	console.log("Stringifying JSON...");
	var data = JSON.stringify(inputData);

	// Mocha kills an async operation...
	fs.writeFileSync(path || "./raw-instrument-data.json", data);
	callback();
};

lib/outputs/templates/main.js

KindCoveredTotalPercentage
lines0170%
conditions010%
expressions01100%
functions070%
/*global window:true, document:true*/

(function() {
	"use strict";

	window.addEventListener("load", function() {

		var $ = function() {
			return [].slice.call(
				document.querySelectorAll.apply(document, arguments));
		};

		var scoreClasses = [
			"perfect",
			"great",
			"good",
			"average",
			"poor",
			"worse",
			"abysmal",
			"you-should-be-ashamed"
		].reverse();

		$(".evaluated")
			.sort(function(a, b) {
				return	parseFloat(a.dataset.heapused) -
						parseFloat(b.dataset.heapused);
			})
			.forEach(function(item, index, array) {
				var score = (item.dataset.score) * (scoreClasses.length-1) | 0,
					scoreClass = scoreClasses[score];

				var perc = (array.length - index) / array.length;
					perc = (perc * 10000) / 10000;

				item.title = (
					scoreClass[0].toUpperCase() +
					scoreClass.substr(1) +
					" code coverage.\n" +
					"[" + item.dataset.type + "]\n" +
					"Depth: " + item.dataset.depth + "\n" +
					"Score: " + item.dataset.score + "\n" +
					"Mem|RSS: "+ item.dataset.rss + "\n" +
					"Mem|HeapTotal: "+ item.dataset.heaptotal + "\n" +
					"Mem|HeapUsed: "+ item.dataset.heapused
				);

				[].slice.call(item.childNodes).forEach(function(item) {
					if (!item.classList.contains("token")) return;

					item.style.borderBottom =
						"solid rgba(0,0,0," + perc + ") 5px";
				});
			});

		$(".unevaluated")
			.forEach(function(item) {
				item.title = "WARNING: This code was never evaluated.";
			});

	}, false);
})();