├── .gitignore ├── .jshintrc ├── .npmignore ├── LICENSE ├── README.md ├── lib ├── cli.js ├── index.js ├── instrumentor-config.js ├── instrumentor-record.js ├── instrumentor.js ├── outputs │ ├── csv.js │ ├── html.js │ ├── json.js │ └── templates │ │ ├── main.css │ │ ├── main.jade │ │ └── main.js ├── process.js ├── reporter.js ├── require-override.js └── stats │ ├── basic.js │ ├── index.js │ └── utils.js ├── makefile ├── package.json ├── report.html └── test ├── main.js ├── map.js ├── output.js └── stats.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | lib-cov 17 | reports -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": false, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": "nofunc", 6 | "newcap": false, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "node": true, 13 | "predef": [ 14 | "before", 15 | "beforeEach", 16 | "describe", 17 | "it" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | reports 2 | report.csv 3 | report.html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Christopher Giffard 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SteamShovel 2 | =========== 3 | 4 | JS code coverage done right. [Why?](#why-should-you-use-steamshovel) 5 | 6 | [![See an example report](https://files.app.net/2h8pnciKv.png)](http://rawgithub.com/cgiffard/SteamShovel/master/report.html) 7 | 8 | ### Get Started 9 | 10 | ```sh 11 | 12 | # install 13 | npm install -g steamshovel 14 | 15 | # instrument 16 | shovel mySourceDirectory myInstrumentedDirectory 17 | 18 | # introspect 19 | mocha -R steamshovel 20 | 21 | ``` 22 | 23 | For a more detailed guide to the SteamShovel internals, see 24 | the [API section.](#api) 25 | 26 | ### Why Should You Use SteamShovel 27 | 28 | "But I like Blanket/Istanbul/JScover!" I hear you say. "Why should I move?" 29 | 30 | That's a great question. It comes down to this: 31 | 32 | **Your existing instrumentor is lying to you.** 33 | 34 | That's right. When you run your tests, and the coverage data is collected, the 35 | instrumentor only pays attention to whether a code branch was run... or wasn't. 36 | Unfortunately this is a gross oversimplification of how code is invoked by a 37 | test suite, and it will leave you with a false sense of security. 38 | 39 | The hypothesis behind SteamShovel is this: 40 | 41 | **Your unit tests should run your code directly. Indirect invocation is probably 42 | unintentional, and that code shouldn't be considered tested.** 43 | 44 | In order to support this hypothesis, SteamShovel records the stack depth of the 45 | instrumentor at every invocation, and calculates the 'testedness' of a given 46 | branch by applying a weighted inverse logarithm to this depth. 47 | 48 | This gives you a **much more accurate** code coverage value. The variance in 49 | 'coverage' as defined by SteamShovel and a conventional instrumentor like 50 | Istanbul can be as much as 50% — that's how much Istanbul is over-reporting. 51 | 52 | ### What else? 53 | 54 | SteamShovel can do a whole lot more, too: 55 | 56 | * Profile timing, order, and stack depth 57 | * Record environment data like memory or load on a per-expression level (set the 58 | environment variable `REPORTER_OUTPUT` to `csv` and you'll get an expression 59 | level dump of timing and memory data! 60 | * Save the result of every evaluation of every expression in your codebase, and 61 | let you step through the results of your test run, interactively. **(Coming 62 | soon!)** 63 | * SteamShovel can now auto-instrument your code as you require it! Simply set 64 | the `AUTO_INSTRUMENT` environment variable to `true` before running the 65 | SteamShovel reporter. Remember that this won't instrument code that isn't 66 | required, so the manual instrumentor may be more accurate in some cases. 67 | 68 | You can override the SteamShovel instrumentor with your own, and record anything 69 | you might want to record! The sky is the limit. 70 | 71 | ![SteamShovel's exported memory data from a test run](https://files.app.net/2p54lXIXq.png) 72 | 73 | **Seen here: SteamShovel's recorded memory data from a test run, visualised in, 74 | uh... Numbers.app** 75 | 76 | See also: [Stack depth of calls over time](https://files.app.net/2p5qdpjOu.png), 77 | [Call latency over run-time of test suite](https://files.app.net/2p5qbErUo.png) 78 | — you're only limited by your imagination here. 79 | 80 | ### Caveat 81 | 82 | Because SteamShovel records so much more from your code, it does have a 83 | considerable performance impact. However, the accuracy of your test coverage 84 | data should be more important than how quickly it runs. 85 | 86 | Also, (if you couldn't tell already!) SteamShovel is early in its development. 87 | Be careful: **there will be an excruciating number of bugs.** 88 | 89 | **I am most of the way through an important refactor which will make steamshovel 90 | ready for use by anyone!** 91 | 92 | ### Todo 93 | 94 | * Stats generation needs to be a lot more abstract, and a lot faster. Look 95 | forward to a huge amount of progress in this area! 96 | * Stats should also allow arbitrary queries to be developed against the data 97 | which could be defined by a model and included in the template. 98 | * Gotta make things even easier to use! 99 | * More info in the default HTML output. 100 | * Memory usage and timing output as CSV & JSON! 101 | 102 | ## API 103 | 104 | ### Processing Files 105 | 106 | You can recursively instrument one or more directories or files by using the 107 | SteamShovel instrument processor. This is the same system the CLI tool uses to 108 | instrument your code. 109 | 110 | ```js 111 | var steamshovel = require("steamshovel"); 112 | 113 | steamshovel.process("./lib", "./lib-instrumented") 114 | .on("complete", function(inFile) { 115 | console.log("The instrumentation of %s is complete!", inFile); 116 | }); 117 | ``` 118 | 119 | #### steamshovel.process ( `inFile`, `outFile` , EventEmitter emitter ) 120 | 121 | This function takes a string describing an input file or directory, or an array 122 | of strings of files/directories. If the first parameter is an array, the second 123 | must also be an array, and their lengths must be the same. 124 | 125 | The function returns an event emitter, to which it emits the following events: 126 | 127 | * `complete` (`file`) — the processing for one of the directly specified files 128 | or directories is complete. 129 | * `dircomplete` (`dir`) — the processing for a given directory is complete (this 130 | is emitted for directories that SteamShovel discovers, too — not just for 131 | directly specified trees.) 132 | * `error` (`error`, `path/file`) — an error was emitted from a filesystem 133 | operation. 134 | * `ignore` (`file`) — this file was explicitly ignored (the directive 135 | `steamShovel:ignore` was found in the file.) 136 | * `instrumenterror` (`err`, `file`) — an error occurred while attempting to 137 | instrument a given file (most likely a syntax error.) 138 | * `mkdir` (`path`) — emitted when SteamShovel creates a new directory 139 | * `nojs` (`file`) — SteamShovel discovered a file, but it is not a JavaScript 140 | file and will be ignored. (It will be written into the new tree, but not 141 | instrumented.) 142 | * `processdir` (`dir`, `out`) — Emitted when SteamShovel begins processing a new 143 | directory 144 | * `readdir` (`dir`) — Emitted after SteamShovel reads the contents of a 145 | directory. 146 | * `readfile` (`file`) — Emitted after SteamShovel completes reading a file. 147 | * `writefile` (`file`) — Emitted after SteamShovel completes writing a file. 148 | 149 | ### Instrumenting Code 150 | 151 | You can instrument code on a file by file basis by using the 152 | `steamshovel.instrument` method. 153 | 154 | #### `steamshovel.instrument` ( `data`, [ `filename`, `incorporateMap` ] ) 155 | 156 | This function takes a string containing the JavaScript source code and returns 157 | the transformed code with instrumentation added. 158 | 159 | Presently, the code is instrumented by replacing all applicable expressions with 160 | `SequenceExpressions` (you'll probably know these as lists of expressions 161 | delimited by way of the *comma operator*.) These make a function call with a 162 | unique ID to the individual expression (incorporating a hash of the filename and 163 | an expression index.) 164 | 165 | The filename is required if you want to instrument more than one file. Otherwise 166 | you could end up with ID conflicts. (This won't ever be a problem if you let 167 | SteamShovel instrument for you.) 168 | 169 | The third boolean parameter enables you to turn off the '*map*', basically a 170 | non-human-readable object definition for the expression instrumentation in the 171 | code. It is advisable that you leave this alone, as the code won't run without 172 | it, but switching it off can be useful for testing the output of the instrument 173 | function. -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | function done(message) { 6 | return function() { 7 | var args = [message].concat([].slice.call(arguments,0)); 8 | console.log.apply(console, args); 9 | }; 10 | } 11 | 12 | function errr(message) { 13 | return function(err, file) { 14 | console.error(message, file); 15 | console.error(err.stack || err); 16 | }; 17 | } 18 | 19 | require("./process")(process.argv[2], process.argv[3]) 20 | .on("processdir", done("Processing directory %s...")) 21 | .on("mkdir", done("Making directory %s...")) 22 | .on("readdir", done("Read directory %s.")) 23 | .on("readfile", done("Read file %s.")) 24 | .on("nojs", done("Ignoring non-JS file %s...")) 25 | .on("ignore", done("Ignoring marked file %s...")) 26 | .on("instrumenterror", errr("Instrument failure in %s:")) 27 | .on("writefile", done("Wrote file %s...")) 28 | .on("dircomplete", done("Directory complete %s...")) 29 | .on("complete", done("Processing Complete!")); -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Export the reporter so that mocha can require it directly by name 4 | module.exports = require("./reporter"); 5 | 6 | // Return the components 7 | module.exports.instrument = require("./instrumentor"); 8 | module.exports.process = require("./process"); 9 | module.exports.reporter = require("./reporter"); 10 | module.exports.recorder = require("./instrumentor-record"); -------------------------------------------------------------------------------- /lib/instrumentor-config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // These are expression-context items which can be completely wrapped. 4 | var allowedReplacements = [ 5 | "AssignmentExpression", 6 | "ArrayExpression", 7 | "ArrayPattern", 8 | "ArrowFunctionExpression", 9 | "BinaryExpression", 10 | "CallExpression", 11 | "ConditionalExpression", 12 | "FunctionExpression", 13 | "Identifier", 14 | "Literal", 15 | "LogicalExpression", 16 | "MemberExpression", 17 | "NewExpression", 18 | "ObjectExpression", 19 | "ObjectPattern", 20 | "UnaryExpression", 21 | "UpdateExpression" 22 | ]; 23 | 24 | // These are block/statement level constructs which cannot be wrapped and must 25 | // be prepended. 26 | var allowedPrepend = [ 27 | "BlockStatement", 28 | "BreakStatement", 29 | "CatchClause", 30 | "ContinueStatement", 31 | "DirectiveStatement", 32 | "DoWhileStatement", 33 | "DebuggerStatement", 34 | "EmptyStatement", 35 | "ExportDeclaration", 36 | "ExpressionStatement", 37 | "ForStatement", 38 | "ForInStatement", 39 | "ForOfStatement", 40 | "FunctionDeclaration", 41 | "IfStatement", 42 | "LabeledStatement", 43 | "ReturnStatement", 44 | "SwitchStatement", 45 | "SwitchCase", 46 | "ThisExpression", 47 | "ThrowStatement", 48 | "TryStatement", 49 | "VariableDeclaration", 50 | "VariableDeclarator", 51 | "WhileStatement", 52 | "WithStatement", 53 | "YieldExpression" 54 | ]; 55 | 56 | var noReplaceMap = { 57 | 58 | // If we don't do this, our instrument code removes the context 59 | // from member expression function calls and returns the value 60 | // without the context required by some 'methods'. 61 | // 62 | // Without this, calls like array.indexOf() would break. 63 | "CallExpression": { 64 | "callee": { 65 | "type": "MemberExpression" 66 | } 67 | }, 68 | 69 | // We can't put SequenceExpressions on the left side of 70 | // AssignmentExpressions. 71 | // 72 | // (e.g. (abc, def) = value;) 73 | 74 | "AssignmentExpression": ["left"], 75 | 76 | // Nor can we replace the id component of VariableDeclarators. 77 | // (e.g. var [id] = value) 78 | 79 | "VariableDeclarator": ["id"], 80 | 81 | // The components of MemberExpressions should not be touched. 82 | // (e.g. (instrument, abc).(instrument, def) from `abc.def`.) 83 | 84 | "MemberExpression": ["object", "property"], 85 | 86 | // The id component of Functions should not be touched. 87 | // (e.g. function (instrument, abc)() {} from `function abc() {}`.) 88 | 89 | // The parameters of FunctionExpressions should not be touched. 90 | // (e.g. function abc((instrument,a)) {} from `function abc(a) {}`.) 91 | 92 | "FunctionExpression": ["id", "params"], 93 | "FunctionDeclaration": ["id", "params"], 94 | 95 | // The properties of Objects should not be touched. 96 | // (e.g. {(instrument,a)): b} from `{a:b}`.) 97 | 98 | "Property": ["key"], 99 | 100 | // The parameter of a catch clause should not be touched. 101 | // (e.g. catch (instrument,a) { ... } from `catch (a) { ... }`.) 102 | 103 | "CatchClause": ["param"], 104 | 105 | // The argument of UpdateExpressions should not be touched. 106 | // (e.g. (instrument,a)++ from `a++`.) 107 | 108 | "UpdateExpression": ["argument"], 109 | 110 | // The argument of the UnaryExpression `typeof` should not be touched. 111 | // (e.g. typeof (instrument,a) from `typeof a`.) 112 | 113 | "UnaryExpression": ["argument"], 114 | 115 | // The left side of a ForInStatement should not be touched. 116 | // (e.g. for ((instrument,a) in (instrument,b)) from `for (a in b)`.) 117 | 118 | "ForInStatement": ["left"] 119 | }; 120 | 121 | var noPrependMap = { 122 | 123 | }; 124 | 125 | module.exports = { 126 | noReplaceMap: noReplaceMap, 127 | allowedReplacements: allowedReplacements, 128 | allowedPrepend: allowedPrepend, 129 | noPrependMap: noPrependMap 130 | }; -------------------------------------------------------------------------------- /lib/instrumentor-record.js: -------------------------------------------------------------------------------- 1 | /* steamShovel:ignore */ 2 | /*global __instrumentor_map__:true, window:true, global:true */ 3 | 4 | module.exports = function instrumentor_record(filetag, id, expression) { 5 | var scope = global || window; 6 | scope.st_timeCommenced = scope.st_timeCommenced || Date.now(); 7 | scope.st_iterator = (scope.st_iterator || 0); 8 | 9 | var depth = 10 | (new Error()).stack.split("\n") 11 | .slice(1) 12 | .length; 13 | 14 | if (!scope.mem) 15 | scope.mem = ( 16 | typeof process !== "undefined" ? process.memoryUsage : function(){} 17 | ); 18 | 19 | if (scope.__steamshovel_test_depth) { 20 | depth -= scope.__steamshovel_test_depth; 21 | } 22 | 23 | __instrumentor_map__[filetag].instruments[id].results.push({ 24 | "depth": depth, 25 | "time": Date.now(), 26 | "timeOffset": Date.now() - scope.st_timeCommenced, 27 | "memoryUsage": scope.mem(), 28 | "invocation": scope.st_iterator, 29 | "milestone": scope.__steamshovel_milestone, 30 | "value": scope.__steamshovel_record_expression ? expression : null 31 | }); 32 | 33 | scope.st_iterator ++; 34 | 35 | // Make sure we return the original expresison result 36 | return expression; 37 | }; -------------------------------------------------------------------------------- /lib/instrumentor.js: -------------------------------------------------------------------------------- 1 | /*global require:true, module:true */ 2 | 3 | var crypto = require("crypto"), 4 | esprima = require("esprima"), 5 | escodegen = require("escodegen"), 6 | estraverse = require("estraverse"), 7 | 8 | // Maps for instrumenting 9 | noReplaceMap = require("./instrumentor-config").noReplaceMap, 10 | allowedReplacements = require("./instrumentor-config").allowedReplacements, 11 | noPrependMap = require("./instrumentor-config").noPrependMap, 12 | allowedPrepend = require("./instrumentor-config").allowedPrepend; 13 | 14 | // Local definitions for functions in this file 15 | var callExpression, 16 | expressionStatement, 17 | fileID, 18 | prependInstrumentorMap, 19 | preprocessNode; 20 | 21 | /* 22 | Public: Instrument a string of JS code using the SteamShovel instrumentor. 23 | This function executes synchronously. 24 | 25 | data - The raw JavaScript code to instrument 26 | filename - The filename of the code in question, for inclusion in 27 | the instrument map 28 | incorporateMap - Whether to include the map (defaults to true, but useful 29 | for testing output without the additional noise of the 30 | map.) 31 | returnAsObject - Return the data as an object containing the instrumented 32 | code and the source map. 33 | 34 | Returns a string containing the instrumented code. 35 | 36 | Examples 37 | 38 | code = instrumentCode("function(a) { return a; }", "myfile.js"); 39 | 40 | */ 41 | 42 | module.exports = 43 | function instrumentCode(data, filename, incorporateMap, returnAsObject) { 44 | 45 | filename = filename || "implicit-filename"; 46 | incorporateMap = incorporateMap === false ? false : true; 47 | 48 | var esprimaOptions = {loc: true, range: true, raw: true, comment: true}, 49 | ast = esprima.parse(data, esprimaOptions), 50 | filetag = fileID(filename), 51 | comments = ast.comments, 52 | 53 | // State and storage 54 | id = 0, 55 | code = null, 56 | sourceMap = {}; 57 | 58 | // Add metadata 59 | sourceMap.filetag = filetag; 60 | sourceMap.filename = filename; 61 | 62 | // Add raw sourcecode 63 | sourceMap.source = String(data); 64 | 65 | // Bucket for instruments 66 | sourceMap.instruments = {}; 67 | 68 | // Add comment ranges to sourceMap 69 | sourceMap.comments = comments.map(function(comment) { 70 | return comment.range; 71 | }); 72 | 73 | function sourceMapAdd(id, node, line) { 74 | if (sourceMap.instruments[id]) 75 | throw new Error("Instrument error: Instrument already exists!"); 76 | 77 | sourceMap.instruments[id] = { 78 | "loc": node.loc, 79 | "range": node.range, 80 | "results": [], 81 | "stack": node.stackPath, 82 | "type": node.type, 83 | "line": !!line 84 | }; 85 | } 86 | 87 | // Process AST 88 | estraverse.replace(ast, { 89 | 90 | // Enter is where we mark nodes as noReplace 91 | // which prevents bugs around weird edge cases, which I'll probably 92 | // discover as time goes on. 93 | // 94 | // We also record a stack path to be provided to the instrumentor 95 | // that lets us know our place in the AST when the instrument recorder 96 | // is called. 97 | 98 | enter: preprocessNode, 99 | 100 | // Leave is where we replace the actual nodes. 101 | 102 | leave: function (node) { 103 | 104 | // Does this node have a body? If so we can replace its contents. 105 | if (node.body && node.body.length) { 106 | node.body = 107 | [].slice.call(node.body, 0) 108 | .reduce(function(body, node) { 109 | 110 | if (!~allowedPrepend.indexOf(node.type)) 111 | return body.concat(node); 112 | 113 | id++; 114 | sourceMapAdd(id, node, true); 115 | 116 | return body.concat( 117 | expressionStatement( 118 | callExpression(filetag, id) 119 | ), 120 | node 121 | ); 122 | 123 | }, []); 124 | } 125 | 126 | if (node.noReplace) 127 | return; 128 | 129 | // If we're allowed to replace the node, 130 | // replace it with a Call Expression. 131 | 132 | if (~allowedReplacements.indexOf(node.type)) 133 | return ( 134 | id ++, 135 | sourceMapAdd(id, node, false), 136 | callExpression(filetag, id, node) 137 | ); 138 | } 139 | }); 140 | 141 | code = escodegen.generate(ast); 142 | 143 | if (incorporateMap) 144 | code = prependInstrumentorMap(code, sourceMap); 145 | 146 | if (returnAsObject) 147 | return { "code": code, "map": sourceMap }; 148 | 149 | return code; 150 | }; 151 | 152 | module.exports.withmap = function(data, filename, incorporateMap) { 153 | return module.exports(data, filename, incorporateMap, true); 154 | }; 155 | 156 | /* 157 | Public: Preprocess a node to save the AST stack/path into the node, and 158 | to mark whether its children should be replaced or not. 159 | 160 | data - The AST node as represented by Esprima. 161 | 162 | Returns null. 163 | 164 | Examples 165 | 166 | preprocessNode(astNode); 167 | 168 | */ 169 | 170 | preprocessNode = module.exports.preprocessNode = 171 | function preprocessNode(node) { 172 | 173 | if (!node.stackPath) 174 | node.stackPath = [node.type]; 175 | 176 | // Now mark a path to the node. 177 | Object.keys(node).forEach(function(nodekey) { 178 | var prop = node[nodekey]; 179 | 180 | function saveStack(prop) { 181 | // This property most likely isn't a node. 182 | if (!prop || typeof prop !== "object" || !prop.type) return; 183 | 184 | prop.stackPath = node.stackPath.concat(prop.type); 185 | } 186 | 187 | if (Array.isArray(prop)) 188 | prop.forEach(saveStack); 189 | 190 | saveStack(prop); 191 | }); 192 | 193 | var nodeRule = noReplaceMap[node.type]; 194 | 195 | if (!nodeRule) return; 196 | 197 | // Convert the rule to an array so we can handle it using 198 | // the same logic. 199 | // 200 | // Strings and arrays just wholesale exclude the child nodes 201 | // of the current node where they match. 202 | 203 | if (nodeRule instanceof String) 204 | nodeRule = [nodeRule]; 205 | 206 | if (nodeRule instanceof Array) { 207 | nodeRule.forEach(function(property) { 208 | if (!node[property]) return; 209 | 210 | if (node[property] instanceof Array) 211 | return node[property].forEach(function(item) { 212 | item.noReplace = true; 213 | }); 214 | 215 | node[property].noReplace = true; 216 | }); 217 | } 218 | 219 | // Whereas this more verbose object style allows 220 | // exclusion based on subproperty matches. 221 | 222 | if (nodeRule instanceof Object) { 223 | Object.keys(nodeRule).forEach(function(property) { 224 | if (!node[property]) return; 225 | 226 | var exclude = 227 | Object 228 | .keys(nodeRule[property]) 229 | .reduce(function(prev, cur) { 230 | if (!prev) return prev; 231 | return ( 232 | node[property][cur] === 233 | nodeRule[property][cur]); 234 | }, true); 235 | 236 | if (exclude) node[property].noReplace = true; 237 | }); 238 | } 239 | }; 240 | 241 | /* 242 | Public: Generates a replacement CallExpression for a given node, 243 | which invokes the instrument function, passing an id and the resolved 244 | value of the original expression as the first and second arguments. 245 | 246 | id - A unique ID string to mark the call 247 | node - The AST node as represented by Esprima. 248 | 249 | Returns an object representing a replacement node. 250 | 251 | Examples 252 | 253 | callExpression(id, astNode); 254 | 255 | */ 256 | 257 | callExpression = module.exports.callExpression = 258 | function callExpression(filetag, id, node) { 259 | 260 | var callArgs = [ 261 | { 262 | "type": "Literal", 263 | "value": filetag 264 | }, 265 | { 266 | "type": "Literal", 267 | "value": id 268 | } 269 | ]; 270 | 271 | if (!!node) callArgs.push(node); 272 | 273 | return { 274 | "type": "CallExpression", 275 | "callee": { 276 | "type": "Identifier", 277 | "name": "instrumentor_record" 278 | }, 279 | "arguments": callArgs 280 | }; 281 | }; 282 | 283 | /* 284 | Public: Generates an ExpressionStatement AST node, containing an input 285 | expression. 286 | 287 | node - The expression's AST node as represented by Esprima. 288 | 289 | Returns an object representing a replacement node. 290 | 291 | Examples 292 | 293 | expressionStatement(astNode); 294 | 295 | */ 296 | 297 | expressionStatement = module.exports.expressionStatement = 298 | function expressionStatement(node) { 299 | return { 300 | "type": "ExpressionStatement", 301 | "expression": node 302 | }; 303 | }; 304 | 305 | /* 306 | Public: Given a string representation of JavaScript sourcecode and a map 307 | containing metainformation about the instrument probes themselves, this 308 | function prepends a string/source representation of the instrument probes, 309 | as well as the sourcecode of the function that performs the instrumentation 310 | itself. 311 | 312 | code - The source to which the instrument map should be 313 | appended 314 | sourceMap - A map of information about each instrument probe and 315 | containing the sourcecode of the file. 316 | 317 | Returns the instrumented JavaScript sourcecode to be executed or written 318 | to disk. 319 | 320 | Examples 321 | 322 | prependInstrumentorMap(code, sourceMap); 323 | 324 | */ 325 | prependInstrumentorMap = module.exports.prependInstrumentorMap = 326 | function prependInstrumentorMap(code, sourceMap) { 327 | 328 | var filetag = sourceMap.filetag, 329 | map = JSON.stringify(sourceMap); 330 | 331 | return [ 332 | 333 | // Map initialisation 334 | "if (typeof __instrumentor_map__ === 'undefined') {", 335 | "__instrumentor_map__ = {};", 336 | "}", 337 | 338 | // Map for key 339 | "if (typeof __instrumentor_map__." + filetag + " === 'undefined') {", 340 | "__instrumentor_map__." + filetag + " = " + map + ";", 341 | "}", 342 | 343 | // Instrumentor 344 | require("./instrumentor-record").toString(), 345 | 346 | // Include the code 347 | code 348 | 349 | ].join("\n"); 350 | }; 351 | 352 | /* 353 | Public: Given a string, this function will generate a string md5 digest 354 | which can be safely used barely in JS literal context. 355 | 356 | filename - The filename to digest 357 | 358 | Returns the digest string. 359 | 360 | Examples 361 | 362 | fileID("myfile.js"); 363 | 364 | */ 365 | 366 | fileID = module.exports.fileID = 367 | function fileID(id) { 368 | 369 | return "i" + crypto.createHash("md5").update(id).digest("hex"); 370 | }; -------------------------------------------------------------------------------- /lib/outputs/csv.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"), 4 | util = require("../stats/utils"), 5 | stats = require("../stats/index"); 6 | 7 | module.exports = function generateCSV(inputData, path, callback) { 8 | var iterations = util.generateIterationMap(inputData), 9 | csvFile = "", 10 | csvHeader = 11 | Object.keys(iterations[0]).reduce(function keyProc(acc, cur) { 12 | var item = iterations[0][cur]; 13 | 14 | if (item instanceof Object && !Array.isArray(item)) 15 | return acc.concat( 16 | Object.keys(item) 17 | .map(function(key) { 18 | return cur + "." + key; 19 | })); 20 | 21 | return acc.concat(cur); 22 | }, []) 23 | .map(function(key) { 24 | return '"' + key + '"'; 25 | }) 26 | .join(","); 27 | 28 | csvFile = iterations.reduce(function(file, iteration) { 29 | return file + "\n" + 30 | Object.keys(iteration).reduce(function keyProc(acc, cur) { 31 | var item = iteration[cur]; 32 | 33 | if (item instanceof Object && !Array.isArray(item)) 34 | return acc.concat( 35 | Object.keys(item) 36 | .map(function(key) { 37 | return item[key]; 38 | })); 39 | 40 | return acc.concat(item); 41 | }, []) 42 | .map(function(key) { 43 | return '"' + key + '"'; 44 | }) 45 | .join(","); 46 | 47 | }, csvHeader); 48 | 49 | 50 | // The intention is of course, to go async as soon as I can work 51 | // out how to stop mocha killing the script. 52 | fs.writeFileSync(path || "./report.csv", csvFile); 53 | callback(null, csvFile); 54 | }; -------------------------------------------------------------------------------- /lib/outputs/html.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var jade = require("jade"), 4 | fs = require("fs"), 5 | util = require("../stats/utils"), 6 | stats = require("../stats/index"); 7 | 8 | var scoreClasses = [ 9 | "perfect", 10 | "great", 11 | "good", 12 | "average", 13 | "poor", 14 | "worse", 15 | "abysmal", 16 | "you-should-be-ashamed" 17 | ].reverse(); 18 | 19 | var generateMap; 20 | 21 | module.exports = function generateHTML(inputData, path, callback, template) { 22 | 23 | // Trickery to avoid indent hell 24 | if (!template) 25 | return module.exports.loadTemplate( 26 | generateHTML.bind(null, inputData, path, callback)); 27 | 28 | var htmlOutput = [], sources; 29 | 30 | console.log("Getting sources..."); 31 | sources = module.exports.getSources(inputData); 32 | 33 | console.log("Generating memory statistics..."); 34 | util.generateMemoryStats(inputData); 35 | 36 | console.log("Rendering source buffers..."); 37 | 38 | sources.forEach(function(source) { 39 | console.log("\tRendering %s",source.filename); 40 | 41 | htmlOutput.push({ 42 | "stats": stats.basic(inputData, source.key), 43 | "codeMap": generateMap(inputData, source), 44 | "filename": source.filename, 45 | "key": source.key 46 | }); 47 | }); 48 | 49 | console.log("Loading template resources..."); 50 | 51 | var script = fs.readFileSync(__dirname+"/templates/main.js","utf8"), 52 | styles = fs.readFileSync(__dirname+"/templates/main.css","utf8"); 53 | 54 | console.log("Writing rendered template to disk..."); 55 | 56 | fs.writeFileSync(path || "./report.html", 57 | htmlOutput = template({ 58 | "script": script, 59 | "style": styles, 60 | "files": htmlOutput, 61 | "stats": stats.basic(inputData), 62 | "classes": scoreClasses 63 | }) 64 | ); 65 | 66 | callback(null, htmlOutput); 67 | }; 68 | 69 | module.exports.extension = "html"; 70 | 71 | generateMap = module.exports.generateMap = 72 | function generateMap(inputData, source) { 73 | var instrumentMap = module.exports.preprocessMap( 74 | inputData, source.key), 75 | comments = source.commentRanges, 76 | code = source.source, 77 | map = [], 78 | buffer = "", 79 | bufferState = false, 80 | pointer = 0, 81 | iIdx = 0; 82 | 83 | for (; pointer < code.length; pointer++) { 84 | 85 | for (; iIdx < instrumentMap.length; iIdx ++) { 86 | if (instrumentMap[iIdx].range[0] > pointer) 87 | break; 88 | 89 | 90 | 91 | if (pointer === instrumentMap[iIdx].range[0]) { 92 | map.push(buffer); 93 | buffer = ""; 94 | map.push({ 95 | "open": instrumentMap[iIdx] 96 | }); 97 | } 98 | } 99 | 100 | if (bufferState !== (!!code[pointer].match(/\s+/) && buffer.length)) { 101 | bufferState = !bufferState; 102 | map.push(buffer); 103 | buffer = ""; 104 | } 105 | 106 | buffer += code[pointer]; 107 | 108 | for (iIdx = 0; iIdx < instrumentMap.length; iIdx ++) { 109 | if (instrumentMap[iIdx].range[1] > pointer) 110 | break; 111 | 112 | if (pointer === instrumentMap[iIdx].range[1]) { 113 | map.push(buffer); 114 | buffer = ""; 115 | map.push({ 116 | "close": instrumentMap[iIdx] 117 | }); 118 | } 119 | } 120 | } 121 | 122 | if (buffer.length) { 123 | map.push(buffer); 124 | buffer = ""; 125 | } 126 | 127 | return map; 128 | }; 129 | 130 | module.exports.loadTemplate = function loadTemplate(cb) { 131 | // Flicked this back to synchronous to play nicely with 132 | // mocha, which wasn't waiting for this to finish. 133 | var data = fs.readFileSync(__dirname + "/templates/main.jade", "utf8"); 134 | cb(jade.compile(data)); 135 | }; 136 | 137 | module.exports.preprocessMap = function preprocessMap(inputData, key) { 138 | return ( 139 | util.getInstruments(inputData, key) 140 | .sort(function(a, b) { 141 | return a.range[0] - b.range[0]; 142 | }) 143 | .map(function(item) { 144 | item.score = util.calculateValue(inputData)(item); 145 | item.depth = Math.min.apply(Math, 146 | item.results.map(util.withkey("depth"))); 147 | return item; 148 | })); 149 | }; 150 | 151 | module.exports.getSources = function getSources(inputData) { 152 | return ( 153 | Object.keys(inputData) 154 | .map(function(key) { 155 | return inputData[key]; 156 | }) 157 | .map(function(item) { 158 | return { 159 | "filename": item.filename, 160 | "source": item.source, 161 | "key": item.filetag 162 | }; 163 | })); 164 | }; -------------------------------------------------------------------------------- /lib/outputs/json.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"), 4 | util = require("../stats/utils"), 5 | stats = require("../stats/index"); 6 | 7 | module.exports = function generateJSON(inputData, path, callback) { 8 | console.log("Stringifying JSON..."); 9 | var data = JSON.stringify(inputData); 10 | 11 | // Mocha kills an async operation... 12 | fs.writeFileSync(path || "./raw-instrument-data.json", data); 13 | callback(); 14 | }; -------------------------------------------------------------------------------- /lib/outputs/templates/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 66.67%; 3 | font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; 4 | tab-size: 4; 5 | } 6 | 7 | * { 8 | margin: 0; 9 | padding: 0; 10 | box-sizing: border-box; 11 | } 12 | 13 | body { 14 | margin-right: 28vw; 15 | line-height: 1.6; 16 | } 17 | 18 | div.body { 19 | height: 100vh; 20 | overflow: hidden; 21 | overflow-y: scroll; 22 | } 23 | 24 | h1 { 25 | font-size: 4em; 26 | font-weight: 100; 27 | padding-bottom: 0.4em; 28 | border-bottom: solid #CCC 1px; 29 | margin-bottom: 0.4em; 30 | color: #555; 31 | } 32 | 33 | aside { 34 | position: fixed; 35 | width: 28vw; 36 | right: 0px; 37 | top: 0px; 38 | background-color: #333; 39 | height: 100vh; 40 | box-shadow: rgba(0,0,0,0.5) -1px 0px 0px; 41 | overflow-y: scroll; 42 | } 43 | 44 | aside ul, aside li { 45 | list-style-type: none; 46 | padding: 0px; 47 | margin: 0px; 48 | } 49 | 50 | aside li { 51 | box-shadow: inset rgba(0,0,0,0.5) 1px 0px 0px; 52 | } 53 | 54 | aside a { 55 | display: block; 56 | line-height: 1.4; 57 | font-size: 1.2em; 58 | text-decoration: none; 59 | border-bottom: solid #222 1px; 60 | border-top: solid #444 1px; 61 | border-left: solid 3px black; 62 | padding: 2.5em; 63 | padding-top: 1em; 64 | padding-bottom: 1em; 65 | overflow: hidden; 66 | color: #666; 67 | position: relative; 68 | } 69 | 70 | aside a:hover { 71 | color: white; 72 | background-color: #363636; 73 | } 74 | 75 | aside a { 76 | text-overflow: break-word; 77 | } 78 | 79 | aside a span.coverage { 80 | background-color: #2e2e2e; 81 | border-right: solid #3a3a3a 1px; 82 | color: black; 83 | display: inline-block; 84 | font-weight: bold; 85 | min-width: 2em; 86 | padding: 0.3em; 87 | text-align: center; 88 | padding-top: 1em; 89 | padding-bottom: 1em; 90 | position: absolute; 91 | top: 0px; 92 | left: 0px; 93 | height: 100%; 94 | } 95 | 96 | aside li.perfect a { 97 | border-left-color: hsl(125, 100%, 70%); 98 | } 99 | 100 | aside li.great a { 101 | border-left-color: hsl(115, 100%, 70%); 102 | } 103 | 104 | aside li.good a { 105 | border-left-color: hsl(100, 100%, 70%); 106 | } 107 | 108 | aside li.average a { 109 | border-left-color: hsl(80, 100%, 70%); 110 | } 111 | 112 | aside li.poor a { 113 | border-left-color: hsl(60, 100%, 70%); 114 | } 115 | 116 | aside li.worse a { 117 | border-left-color: hsl(40, 100%, 70%); 118 | } 119 | 120 | aside li.abysmal a { 121 | border-left-color: hsl(20, 100%, 70%); 122 | } 123 | 124 | aside li.you-should-be-ashamed a { 125 | border-left-color: hsl(0, 100%, 70%); 126 | } 127 | 128 | aside li.perfect span.coverage { 129 | color: hsl(125, 100%, 70%); 130 | } 131 | 132 | aside li.great span.coverage { 133 | color: hsl(115, 100%, 70%); 134 | } 135 | 136 | aside li.good span.coverage { 137 | color: hsl(100, 100%, 70%); 138 | } 139 | 140 | aside li.average span.coverage { 141 | color: hsl(80, 100%, 70%); 142 | } 143 | 144 | aside li.poor span.coverage { 145 | color: hsl(60, 100%, 70%); 146 | } 147 | 148 | aside li.worse span.coverage { 149 | color: hsl(40, 100%, 70%); 150 | } 151 | 152 | aside li.abysmal span.coverage { 153 | color: hsl(20, 100%, 70%); 154 | } 155 | 156 | aside li.you-should-be-ashamed span.coverage { 157 | color: hsl(0, 100%, 70%); 158 | } 159 | 160 | 161 | header.docHeader { 162 | padding: 2em; 163 | } 164 | 165 | div.file { 166 | margin: 1em; 167 | padding: 1em; 168 | background-color: #F6F6F6; 169 | } 170 | 171 | div.file header { 172 | border-bottom: dashed #CCC 1px; 173 | } 174 | 175 | pre { 176 | background-color: white; 177 | font-size: 1.2em; 178 | font-family: Menlo, Consolas, Monaco, "Lucida Console", monospace; 179 | padding: 1em; 180 | } 181 | 182 | table { 183 | border-collapse: collapse; 184 | margin-bottom: 1em; 185 | margin-top: 1em; 186 | color: #666; 187 | } 188 | 189 | th, td { 190 | text-align: left; 191 | min-width: 50px; 192 | font-size: 1.1em; 193 | line-height: 1.7em; 194 | padding-right: 1em; 195 | } 196 | 197 | .token { 198 | -webkit-transition: background-color linear 250ms; 199 | transition: background-color linear 250ms; 200 | } 201 | 202 | li a { 203 | -webkit-transition: border-color linear 250ms; 204 | transition: border-color linear 250ms 205 | } 206 | 207 | /* Tokens */ 208 | .evaluated.perfect > .token, .key-item.perfect { 209 | background-color: hsl(125, 100%, 70%); 210 | color: black; 211 | font-weight: normal; 212 | } 213 | 214 | .evaluated.great > .token, .key-item.great { 215 | background-color: hsl(115, 100%, 70%); 216 | color: black; 217 | font-weight: normal; 218 | } 219 | 220 | .evaluated.good > .token, .key-item.good { 221 | background-color: hsl(100, 100%, 70%); 222 | color: black; 223 | font-weight: normal; 224 | } 225 | 226 | .evaluated.average > .token, .key-item.average { 227 | background-color: hsl(80, 100%, 70%); 228 | color: black; 229 | font-weight: normal; 230 | } 231 | 232 | .evaluated.poor > .token, .key-item.poor { 233 | background-color: hsl(60, 100%, 70%); 234 | color: black; 235 | font-weight: normal; 236 | } 237 | 238 | .evaluated.worse > .token, .key-item.worse { 239 | background-color: hsl(40, 100%, 70%); 240 | color: black; 241 | font-weight: normal; 242 | } 243 | 244 | .evaluated.abysmal > .token, .key-item.abysmal { 245 | background-color: hsl(20, 100%, 70%); 246 | color: black; 247 | font-weight: normal; 248 | } 249 | 250 | .evaluated.you-should-be-ashamed > .token, .key-item.you-should-be-ashamed { 251 | background-color: hsl(0, 100%, 70%); 252 | color: black; 253 | font-weight: normal; 254 | } 255 | 256 | .unevaluated > .token, .key-item.unevaluated { 257 | background-color: #CCC; 258 | color: red; 259 | font-weight: bold; 260 | } 261 | 262 | .AssignmentExpression > .token, .AssignmentExpression > * > .token { 263 | border-bottom: solid #AAF 2px; 264 | } 265 | 266 | .CallExpression > .token, .CallExpression > * > .token { 267 | border-bottom: solid #AFA 2px; 268 | } 269 | 270 | #guide { 271 | border: solid #ddd 1px; 272 | margin: 0px; 273 | margin-left: -1em; 274 | margin-right: -1em; 275 | padding: 1em; 276 | border-radius: 4px; 277 | overflow: hidden; 278 | } 279 | 280 | #guide h3, #guide p { 281 | clear: both; 282 | margin-bottom: 0.5em; 283 | } 284 | 285 | #guide p { 286 | font-size: 1.1em; 287 | } 288 | 289 | #guide .key-item { 290 | display: block; 291 | padding: 1em; 292 | float: left; 293 | box-shadow: inset rgba(0,0,0,0.2) 0px 1px 0px, 294 | inset rgba(0,0,0,0.2) -1px -1px 0px; 295 | } -------------------------------------------------------------------------------- /lib/outputs/templates/main.jade: -------------------------------------------------------------------------------- 1 | mixin stats(data) 2 | table.statRundown 3 | thead: tr 4 | th Kind 5 | th Covered 6 | th Total 7 | th Percentage 8 | tbody 9 | each stats, statType in data 10 | tr 11 | th=statType 12 | td=((stats.covered*100000)|0)/100000 13 | td=stats.total 14 | td=(((stats.percentage*1000)|0) / 1000) + "%" 15 | 16 | mixin file(file) 17 | div.file(id=file.key) 18 | header.fileHeader 19 | h2=file.filename 20 | +stats(file.stats) 21 | 22 | pre 23 | each item, key in file.codeMap 24 | if (!item.close && !item.open) 25 | if (item.match(/\s\s+/) || item.match(/\t/)) 26 | span.whitespace=item 27 | else 28 | span.token=item 29 | else 30 | if (item.open && item.open.results.length) 31 | -var score = (item.open.score) * (classes.length-1) | 0; 32 | -var coverageStyle = classes[score]; 33 | | 38 | if (item.open && !item.open.results.length) 39 | | 42 | if (item.close) 43 | | 44 | 45 | mixin sidebar(files) 46 | aside.sidebar 47 | nav: ul 48 | each file, index in files 49 | - var expressions = file.stats.expressions; 50 | - var score = (expressions.percentage/100)*(classes.length-1)|0; 51 | 52 | li( class="file " + classes[score], 53 | data-coverage=expressions.percentage) 54 | 55 | a(href="#"+file.key) 56 | span.coverage= expressions.percentage|0 57 | span.filename!= file.filename.replace(/\//ig,"/­") 58 | 59 | mixin main(files) 60 | main 61 | each file, key in files 62 | +file(file) 63 | 64 | mixin guide 65 | #guide 66 | h3 Key 67 | p 68 | | SteamShovel uses the stack depth to determine how 69 | | directly code was tested. Code which was not tested 70 | | directly is not considered highly in the coverage 71 | | output. 72 | .items 73 | for classN in classes 74 | -classR = classN[0].toUpperCase() + classN.substr(1) 75 | span(class="key-item " + classN)= classR 76 | span.key-item.unevaluated Unevaluated 77 | 78 | 79 | doctype html 80 | html(lang=en) 81 | head 82 | meta(charset="utf8") 83 | style(type="text/css", media="screen")!= style 84 | 85 | body 86 | +sidebar(files) 87 | 88 | div.body 89 | header.docHeader 90 | h1 Coverage Report 91 | +stats(stats) 92 | +guide() 93 | +main(files) 94 | 95 | script!= "var fileData = " + JSON.stringify(files) 96 | script!= script 97 | -------------------------------------------------------------------------------- /lib/outputs/templates/main.js: -------------------------------------------------------------------------------- 1 | /*global window:true, document:true*/ 2 | 3 | (function() { 4 | "use strict"; 5 | 6 | window.addEventListener("load", function() { 7 | 8 | var $ = function() { 9 | return [].slice.call( 10 | document.querySelectorAll.apply(document, arguments)); 11 | }; 12 | 13 | var scoreClasses = [ 14 | "perfect", 15 | "great", 16 | "good", 17 | "average", 18 | "poor", 19 | "worse", 20 | "abysmal", 21 | "you-should-be-ashamed" 22 | ].reverse(); 23 | 24 | $(".evaluated") 25 | .sort(function(a, b) { 26 | return parseFloat(a.dataset.heapused) - 27 | parseFloat(b.dataset.heapused); 28 | }) 29 | .forEach(function(item, index, array) { 30 | var score = (item.dataset.score) * (scoreClasses.length-1) | 0, 31 | scoreClass = scoreClasses[score]; 32 | 33 | var perc = (array.length - index) / array.length; 34 | perc = (perc * 10000) / 10000; 35 | 36 | item.title = ( 37 | scoreClass[0].toUpperCase() + 38 | scoreClass.substr(1) + 39 | " code coverage.\n" + 40 | "[" + item.dataset.type + "]\n" + 41 | "Depth: " + item.dataset.depth + "\n" + 42 | "Score: " + item.dataset.score + "\n" + 43 | "Mem|RSS: "+ item.dataset.rss + "\n" + 44 | "Mem|HeapTotal: "+ item.dataset.heaptotal + "\n" + 45 | "Mem|HeapUsed: "+ item.dataset.heapused 46 | ); 47 | 48 | [].slice.call(item.childNodes).forEach(function(item) { 49 | if (!item.classList.contains("token")) return; 50 | 51 | item.style.borderBottom = 52 | "solid rgba(0,0,0," + perc + ") 5px"; 53 | }); 54 | }); 55 | 56 | $(".unevaluated") 57 | .forEach(function(item) { 58 | item.title = "WARNING: This code was never evaluated."; 59 | }); 60 | 61 | }, false); 62 | })(); -------------------------------------------------------------------------------- /lib/process.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"), 4 | path = require("path"), 5 | mkdirp = require("mkdirp"), 6 | instrument = require("./instrumentor"), 7 | assert = require("assert"), 8 | EventEmitter = require("events").EventEmitter; 9 | 10 | /* 11 | Public: Instrument a file or folder using the SteamShovel instrumentor. 12 | 13 | inFile - The filesystem location of the input resource 14 | outFile - The filesystem destination for the instrumented resource 15 | emitter - An optional pre-defined event emitter to use when 16 | emitting status events. 17 | 18 | Returns 19 | 20 | emitter - An event emitter from which information about the 21 | instrumentation progress is dispatched. 22 | 23 | Examples 24 | 25 | instrumentor("myfile.js", "myfile-instrumented.js"); 26 | 27 | */ 28 | 29 | module.exports = function instrumentTree(inFile, outFile, emitter) { 30 | var combinedMap = {}, isDirectory = false; 31 | 32 | // Is this the first call in the stack? 33 | // We know because every recursion will have an emitter. 34 | var firstCall = !emitter; 35 | 36 | emitter = 37 | emitter && emitter instanceof EventEmitter ? emitter : 38 | new EventEmitter(); 39 | 40 | if (!inFile) 41 | throw new Error("You must specify an input file/directory."); 42 | 43 | if (!outFile) 44 | throw new Error("You must specify an output file/directory."); 45 | 46 | if (inFile instanceof Array) { 47 | assert(outFile instanceof Array, 48 | "If an array if input resources is provided, " + 49 | "the output must also be an array."); 50 | 51 | assert(outFile.length === inFile.length, 52 | "The lengths of the input and destination arrays must match."); 53 | 54 | 55 | // Loop through both arrays, but retain the same emitter 56 | inFile.forEach(function(file, index) { 57 | instrumentTree(file, outFile[index], emitter); 58 | }); 59 | 60 | return emitter; 61 | 62 | } else { 63 | assert(typeof outFile === "string", 64 | "If the first parameter is not an array, the second mustn't be."); 65 | } 66 | 67 | fs.stat(inFile, function(err, stats) { 68 | if (err) return emitter.emit("error", err, inFile); 69 | 70 | if (stats.isDirectory()) { 71 | isDirectory = true; 72 | 73 | if (firstCall) 74 | emitter.on("dircomplete", function(dir) { 75 | if (dir === inFile) emitter.emit("_int_complete", inFile); 76 | }); 77 | 78 | return module.exports.processDir(inFile, outFile, emitter); 79 | } 80 | 81 | if (firstCall) 82 | emitter.on("writefile", function(file) { 83 | if (file === inFile) emitter.emit("complete", inFile); 84 | }); 85 | 86 | module.exports.processFile(inFile, outFile, emitter); 87 | }); 88 | 89 | // This chunk writes an 'instruments.shovelmap' file into the directory 90 | // being instrumented. This file contains a map of all the data from all 91 | // the files which were instrumented, so if a given file is not included 92 | // in the test suite, its data will still appear in output. 93 | 94 | if (firstCall) { 95 | emitter.on("map", function(sourceMap) { 96 | combinedMap[sourceMap.filetag] = sourceMap; 97 | }); 98 | 99 | emitter.on("_int_complete", function(inFile) { 100 | var writePath = path.join(outFile, "./instruments.shovelmap"); 101 | 102 | fs.writeFile( 103 | writePath, 104 | "module.exports = " + JSON.stringify(combinedMap), 105 | function(err) { 106 | if (err) emitter.emit("maperror", err); 107 | emitter.emit("complete", inFile); 108 | }); 109 | }); 110 | } 111 | 112 | return emitter; 113 | }; 114 | 115 | /* 116 | Public: Recursively instrument a folder/directory using the SteamShovel 117 | instrumentor. 118 | 119 | inDir - The filesystem location of the input resource 120 | outDir - The filesystem destination for the instrumented resource 121 | emitter - An optional pre-defined event emitter to use when 122 | emitting status events. 123 | 124 | Returns 125 | 126 | emitter - An event emitter from which information about the 127 | instrumentation progress is dispatched. 128 | 129 | Examples 130 | 131 | instrumentor("./lib", "./lib-cov"); 132 | 133 | */ 134 | 135 | module.exports.processDir = function processDir(dir, out, emitter) { 136 | 137 | emitter = 138 | emitter && emitter instanceof EventEmitter ? emitter : 139 | new EventEmitter(); 140 | 141 | emitter.emit("processdir", dir, out); 142 | 143 | try { fs.statSync(out); } 144 | catch (e) { 145 | emitter.emit("mkdir", out); 146 | mkdirp.mkdirp(out); 147 | } 148 | 149 | fs.readdir(dir, function(err, dirContents) { 150 | if (err) return emitter.emit("error", err, dir); 151 | 152 | emitter.emit("readdir", dir); 153 | 154 | dirContents.forEach(function(file) { 155 | 156 | var filePath = path.join(dir, file), 157 | outPath = path.join(out, file); 158 | 159 | module.exports(filePath, outPath, emitter); 160 | }); 161 | 162 | var instrumentedFiles = 0; 163 | function checkComplete(file) { 164 | if (file instanceof Error) 165 | file = arguments[1]; 166 | 167 | if (dirContents.reduce(function(acc, cur) { 168 | return acc || (dir + "/" + cur === file); 169 | }, false)) 170 | instrumentedFiles ++; 171 | 172 | if (instrumentedFiles === dirContents.length) { 173 | emitter.removeListener("writefile", checkComplete); 174 | emitter.removeListener("dircomplete", checkComplete); 175 | emitter.emit("dircomplete", dir); 176 | } 177 | } 178 | 179 | emitter.on("writefile", checkComplete); 180 | emitter.on("dircomplete", checkComplete); 181 | }); 182 | 183 | return emitter; 184 | }; 185 | 186 | /* 187 | Public: Instrument a single file using the SteamShovel instrumentor. 188 | 189 | This function will ignore files that the instrumentor cannot parse, files 190 | which do not have a `.js` file extension, and files which contain 191 | 192 | inFile - The filesystem location of the input resource 193 | outFile - The filesystem destination for the instrumented resource 194 | emitter - An optional pre-defined event emitter to use when 195 | emitting status events. 196 | 197 | Returns 198 | 199 | emitter - An event emitter from which information about the 200 | instrumentation progress is dispatched. 201 | 202 | Examples 203 | 204 | instrumentor("./lib/myfile.js", "./lib-cov/myfile.js"); 205 | 206 | */ 207 | 208 | module.exports.processFile = function processFile(file, out, emitter) { 209 | 210 | emitter = 211 | emitter && emitter instanceof EventEmitter ? emitter : 212 | new EventEmitter(); 213 | 214 | if (!file.match(/\.js$/i)) 215 | return emitter.emit("nojs", file), 216 | fs .createReadStream(file) 217 | .pipe(fs.createWriteStream(out)) 218 | .on("close", function() { 219 | emitter.emit("writefile", file); 220 | }); 221 | 222 | fs.readFile(file, function(err, data) { 223 | if (err) return emitter.emit("error", err, file); 224 | 225 | emitter.emit("readfile", file); 226 | 227 | var instrumentResult, 228 | code; 229 | data = String(data); 230 | 231 | if (~data.indexOf("steamShovel:" + "ignore")) { 232 | emitter.emit("ignore", file); 233 | 234 | } else { 235 | try { 236 | instrumentResult = instrument.withmap(data, file); 237 | code = instrumentResult.code; 238 | 239 | } catch (err) { 240 | emitter.emit("instrumenterror", err, file); 241 | } 242 | } 243 | 244 | fs.writeFile(out, code || data, function(err) { 245 | if (err) return emitter.emit("error", err, file); 246 | 247 | if (instrumentResult && instrumentResult.map) { 248 | emitter.emit("map", instrumentResult.map); 249 | } 250 | 251 | emitter.emit("writefile", file); 252 | }); 253 | }); 254 | 255 | return emitter; 256 | }; -------------------------------------------------------------------------------- /lib/reporter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /*global global:true, console:true, process:true, require:true */ 4 | 5 | // For monkey patching mocha, when I can work that out. 6 | var mochaPath = process.argv[1].replace(/\/bin\/_mocha$/i, ""); 7 | 8 | var instrument = require("./instrumentor"), 9 | stats = require("./stats"), 10 | columnify = require("columnify"), 11 | fs = require("fs"), 12 | path = require("path"), 13 | _ = require("lodash"), 14 | 15 | // Override require 16 | override = require("./require-override"); 17 | 18 | function replaceIt(module) { 19 | // Dummy code for now. 20 | // In future, this is where we'd set the value of 21 | // __steamshovel_test_depth. 22 | var originalIt = module.it; 23 | module.it = originalIt || function shovelIt() {}; 24 | } 25 | 26 | override(function(module, content, filename) { 27 | var ptr = module; 28 | 29 | while (ptr) { 30 | if (ptr.exports && ptr.exports.it) 31 | replaceIt(ptr.exports); 32 | 33 | ptr = ptr.parent; 34 | } 35 | }); 36 | 37 | global.__instrumentor_map__ = {}; 38 | global.__steamshovel = true; 39 | global.__steamshovel_milestone = null; 40 | global.__steamshovel_record_expression = !!process.env.SHOVEL_RECORD_EXPRESSION; 41 | global.__steamshovel_test_depth = 0; 42 | 43 | function locateMap(done) { 44 | console.log("Scanning for shovelmap..."); 45 | 46 | fs.readdir(process.cwd(), function(err, dir) { 47 | if (err) throw err; 48 | 49 | var complete = 0; 50 | 51 | dir.forEach(function(file) { 52 | file = path.join(process.cwd(), file, "./instruments.shovelmap"); 53 | 54 | fs.stat(file, function(err, data) { 55 | complete ++; 56 | if (!err) { 57 | global.__instrumentor_map__ = 58 | _.merge(global.__instrumentor_map__, require(file)); 59 | 60 | console.log("Located shovelmap at %s!", file); 61 | } 62 | 63 | if (complete === dir.length) { 64 | done(); 65 | } 66 | }); 67 | }); 68 | }); 69 | } 70 | 71 | function SteamShovel(runner) { 72 | console.log("Steam Shovel"); 73 | 74 | var failures = [], 75 | suiteName = null; 76 | 77 | runner.suite.beforeAll(function(done) { 78 | console.log("Commencing coverage test!"); 79 | locateMap(done); 80 | }); 81 | 82 | runner.on('suite', function(suite){ 83 | suiteName = suite.title; 84 | }); 85 | 86 | runner.on('test', function(test){ 87 | global.__steamshovel_milestone = 88 | (suiteName ? suiteName + ", " : "") + test.title; 89 | }); 90 | 91 | runner.on('pass', function(test){ 92 | process.stdout.write("."); 93 | }); 94 | 95 | runner.on('fail', function(test, err){ 96 | process.stdout.write("x"); 97 | failures.push([test, err]); 98 | }); 99 | 100 | runner.on('end', function(done){ 101 | 102 | console.log("\n\n"); 103 | 104 | if (failures.length) { 105 | console.error("Test failures occurred while processing!"); 106 | 107 | return failures.forEach(function(failure) { 108 | var test = failure[0], err = failure[1]; 109 | 110 | console.error("•\t%s\n\n\t%s\n\n", test.title, err.stack); 111 | }); 112 | } 113 | 114 | var coverageData = global.__instrumentor_map__ || {}, 115 | basicStats = stats.basic(coverageData), 116 | arrayTransformedStats = Object.keys(basicStats).map(function(key) { 117 | return { 118 | "kind": key, 119 | "covered": basicStats[key].covered, 120 | "total": basicStats[key].total, 121 | "percentage": basicStats[key].percentage 122 | }; 123 | }); 124 | 125 | var outputTable = columnify(arrayTransformedStats, { 126 | columnSplitter: " | " 127 | }); 128 | 129 | console.log(outputTable, "\n\n"); 130 | 131 | var output = process.env.REPORTER_OUTPUT || "html", 132 | outputPath = process.env.OUTPUT_PATH; 133 | 134 | require("./outputs/" + output)( 135 | coverageData, 136 | outputPath, 137 | function(err, result) { 138 | if (err) throw err; 139 | 140 | console.log("\nReport written to disk.\n"); 141 | } 142 | ); 143 | }); 144 | } 145 | 146 | module.exports = SteamShovel; -------------------------------------------------------------------------------- /lib/require-override.js: -------------------------------------------------------------------------------- 1 | var instrument = require("./instrumentor"), 2 | fs = require("fs"), 3 | path = require("path"); 4 | 5 | var originalFunction = require.extensions[".js"]; 6 | 7 | function stripBOM(content) { 8 | // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) 9 | // because the buffer-to-string conversion in `fs.readFileSync()` 10 | // translates it to FEFF, the UTF-16 BOM. 11 | if (content.charCodeAt(0) === 0xFEFF) { 12 | content = content.slice(1); 13 | } 14 | return content; 15 | } 16 | 17 | function setup(override) { 18 | // Override require to perform auto-instrumentation! 19 | var instrumentCache = {}; 20 | require.extensions[".js"] = function(module, filename) { 21 | var dir = __dirname.replace(/lib\-cov$/ig, "lib"), 22 | content; 23 | 24 | if (instrumentCache[filename]) { 25 | content = instrumentCache[filename]; 26 | } else { 27 | content = stripBOM(fs.readFileSync(filename, 'utf8')); 28 | } 29 | 30 | // Bail out for cache. 31 | // Only auto-instrument where asked. 32 | // Don't auto-instrument any previously instrumented code. 33 | // Don't auto-instrument code marked as 'ignore'. 34 | // Don't auto-instrument files in a node_modules directory. 35 | // Don't auto-instrument the instrumentor. 36 | // Don't auto-instrument our tests. 37 | 38 | if (!instrumentCache[filename] && 39 | process.env.AUTO_INSTRUMENT === "true" && 40 | !~content.indexOf("__instrumentor_map__") && 41 | !~content.indexOf("steamShovel:" + "ignore") && 42 | !~filename.indexOf("node_modules") && 43 | !~filename.indexOf("test") && 44 | !~filename.indexOf(dir) && 45 | !~filename.indexOf(path.normalize(dir + "/../test"))) { 46 | 47 | console.warn("Overriding require %s",filename); 48 | content = instrument(content, filename); 49 | instrumentCache[filename] = content; 50 | } 51 | 52 | module._compile(content, filename); 53 | 54 | if (override && override instanceof Function) { 55 | override(module, content, filename); 56 | } 57 | }; 58 | } 59 | 60 | function restore() { 61 | require.extensions[".js"] = originalFunction; 62 | } 63 | 64 | module.exports = setup; 65 | module.exports.restore = restore; -------------------------------------------------------------------------------- /lib/stats/basic.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require("./utils"); 4 | 5 | 6 | /* 7 | Public: Given a map of instrument data, this function returns a map of 8 | generated statistics regarding coverage, split into four groups: 9 | lines, conditions, expressions, and functions. This is a convenience 10 | function which simply calls the other four functions in this file. 11 | 12 | For more detail on how each statistic is calculated, refer to the notes 13 | against each function. 14 | 15 | instrumentData - The map of instrument data. 16 | filetag - Specify the tag associated with a specific file, 17 | allowing statistics to be scoped a single file. 18 | 19 | Returns a map of maps containing statistics grouped into the categories 20 | lines, conditions, expressions, and functions. 21 | 22 | Examples 23 | 24 | stats = stats.basic(instrumentData); 25 | 26 | */ 27 | module.exports = function generateStatistics(instrumentData, key) { 28 | var self = module.exports; 29 | 30 | if (!instrumentData) 31 | throw new Error("Input was undefined."); 32 | 33 | return { 34 | "lines": self.lines(instrumentData, key), 35 | "conditions": self.conditions(instrumentData, key), 36 | "expressions": self.expressions(instrumentData, key), 37 | "functions": self.functions(instrumentData, key) 38 | }; 39 | }; 40 | 41 | /* 42 | Public: Given a map of instrument data, this function returns a map of 43 | generated statistics regarding code coverage on a per-line level. 44 | 45 | This is ascertained by filtering the probes which were identified at 46 | the time of instrumentation as 'line' probes — that is, they don't execute 47 | in expression context, and are tied to statements, rather than expressions. 48 | 49 | instrumentData - The map of instrument data. 50 | filetag - Specify the tag associated with a specific file, 51 | allowing statistics to be scoped a single file. 52 | 53 | Returns a map of the coverage statistics associated with these probes. The 54 | map contains a count of the number of probes covered, a total number of 55 | probes, and a percentage of probes covered. 56 | 57 | For example: 58 | 59 | { covered: 15, total: 25, percentage: 0.6 } 60 | 61 | Examples 62 | 63 | stats = stats.basic.lines(instrumentData); 64 | 65 | */ 66 | module.exports.lines = function(instrumentData, key) { 67 | var totalInstruments, coveredInstruments, covered, total = 0, 68 | getValue = util.calculateValue(instrumentData); 69 | 70 | totalInstruments = 71 | util.getInstruments(instrumentData, key) 72 | .filter(util.withkey("line")); 73 | 74 | total = totalInstruments.length; 75 | 76 | coveredInstruments = 77 | totalInstruments.filter(util.hasResults); 78 | 79 | covered = 80 | coveredInstruments 81 | .reduce(function(count, instrument) { 82 | return count + getValue(instrument); 83 | }, 0); 84 | 85 | return { 86 | covered: covered, 87 | total: total, 88 | percentage: util.percentage(covered, total) 89 | }; 90 | }; 91 | 92 | /* 93 | Public: Given a map of instrument data, this function returns a map of 94 | generated statistics regarding code coverage on a per-expression level. 95 | 96 | This is ascertained by filtering the probes which were identified at 97 | the time of instrumentation as 'expression' probes — that is, they execute 98 | in expression context, and are tied to expressions, rather than statements. 99 | 100 | instrumentData - The map of instrument data. 101 | filetag - Specify the tag associated with a specific file, 102 | allowing statistics to be scoped a single file. 103 | 104 | Returns a map of the coverage statistics associated with these probes. The 105 | map contains a count of the number of probes covered, a total number of 106 | probes, and a percentage of probes covered. 107 | 108 | For example: 109 | 110 | { covered: 15, total: 25, percentage: 0.6 } 111 | 112 | Examples 113 | 114 | stats = stats.basic.expressions(instrumentData); 115 | 116 | */ 117 | module.exports.expressions = function(inputData, key) { 118 | var covered = 0, total = 0, 119 | getValue = util.calculateValue(inputData), 120 | instruments = 121 | util.getInstruments(inputData, key) 122 | .filter(function(item) { 123 | return !item.line; 124 | }); 125 | 126 | total = instruments.length; 127 | covered = 128 | instruments 129 | .filter(util.hasResults) 130 | .reduce(function(count, item) { 131 | return count + getValue(item); 132 | }, 0); 133 | 134 | return { 135 | covered: covered, 136 | total: total, 137 | percentage: util.percentage(covered, total) 138 | }; 139 | }; 140 | 141 | /* 142 | Public: Given a map of instrument data, this function returns a map of 143 | generated statistics regarding code coverage within conditional statements 144 | in the instrumented code. 145 | 146 | This is ascertained by filtering the probes which were identified at 147 | the time of instrumentation as being conditional expressions or statements, 148 | or exist within conditional expressions or statements. 149 | 150 | In this instance, the 'total' value returned is the number of probes which 151 | correspond *explicitly* to conditionals in the document. However, the 152 | coverage value returned is calculated based on the coverage of the probes 153 | directly associated with these conditionals *as well as* the probes for the 154 | expressions within them. 155 | 156 | In order for a conditional to be considered completely tested, all the 157 | codepaths within it must have been evaluated at appropriate depth. 158 | 159 | instrumentData - The map of instrument data. 160 | filetag - Specify the tag associated with a specific file, 161 | allowing statistics to be scoped a single file. 162 | 163 | Returns a map of the coverage statistics associated with these probes. The 164 | map contains a count of the number of probes covered, a total number of 165 | probes, and a percentage of probes covered. 166 | 167 | For example: 168 | 169 | { covered: 15, total: 25, percentage: 0.6 } 170 | 171 | Examples 172 | 173 | stats = stats.basic.conditions(instrumentData); 174 | 175 | */ 176 | module.exports.conditions = function(inputData, key) { 177 | var covered = 0, total = 0, 178 | getValue = util.calculateValue(inputData), 179 | instruments = 180 | util.getInstruments(inputData, key) 181 | .filter(util.isOfType([ 182 | "IfStatement", 183 | "LogicalExpression", 184 | "ConditionalExpression" 185 | ])); 186 | 187 | var strictTotal = 188 | instruments.filter(function(instrument) { 189 | return ~[ 190 | "IfStatement", 191 | "LogicalExpression", 192 | "ConditionalExpression" 193 | ] 194 | .indexOf(instrument.type); 195 | }) 196 | .length; 197 | 198 | total = instruments.length; 199 | covered = 200 | instruments 201 | .filter(util.hasResults) 202 | .reduce(function(count, instrument) { 203 | return count + getValue(instrument); 204 | }, 0); 205 | 206 | return { 207 | covered: covered * (strictTotal / total), 208 | total: strictTotal, 209 | percentage: util.percentage(covered, total) 210 | }; 211 | }; 212 | 213 | /* 214 | Public: Given a map of instrument data, this function returns a map of 215 | generated statistics regarding code coverage within function declarations 216 | and expressions in the instrumented code. 217 | 218 | This is ascertained by filtering the probes which were identified at 219 | the time of instrumentation as being function expressions or declarations, 220 | or exist within function expressions or declarations. 221 | 222 | In this instance, the 'total' value returned is the number of probes which 223 | correspond *explicitly* to functions in the document. However, the coverage 224 | value returned is calculated based on the coverage of the probes directly 225 | associated with these declarations and expressions *as well as* the probes 226 | for the expressions within them. 227 | 228 | In order for a function to be considered completely tested, all the 229 | codepaths within it must have been evaluated at appropriate depth. 230 | 231 | instrumentData - The map of instrument data. 232 | filetag - Specify the tag associated with a specific file, 233 | allowing statistics to be scoped a single file. 234 | 235 | Returns a map of the coverage statistics associated with these probes. The 236 | map contains a count of the number of probes covered, a total number of 237 | probes, and a percentage of probes covered. 238 | 239 | For example: 240 | 241 | { covered: 15, total: 25, percentage: 0.6 } 242 | 243 | Examples 244 | 245 | stats = stats.basic.conditions(instrumentData); 246 | 247 | */ 248 | module.exports.functions = function(inputData, key) { 249 | var covered = 0, total = 0, 250 | getValue = util.calculateValue(inputData), 251 | instruments = 252 | util.getInstruments(inputData, key) 253 | .filter(util.isOfType([ 254 | "FunctionDeclaration", 255 | "FunctionExpression" 256 | ])); 257 | 258 | var strictTotal = 259 | instruments.filter(function(instrument) { 260 | return ~[ 261 | "FunctionDeclaration", 262 | "FunctionExpression" 263 | ] 264 | .indexOf(instrument.type); 265 | }) 266 | .length; 267 | 268 | total = instruments.length; 269 | 270 | covered = 271 | instruments 272 | .filter(util.hasResults) 273 | .reduce(function(count, item) { 274 | return count + getValue(item); 275 | }, 0); 276 | 277 | return { 278 | covered: covered * (strictTotal / total), 279 | total: strictTotal, 280 | percentage: util.percentage(covered, total) 281 | }; 282 | }; -------------------------------------------------------------------------------- /lib/stats/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | "basic": require("./basic") 5 | }; -------------------------------------------------------------------------------- /lib/stats/utils.js: -------------------------------------------------------------------------------- 1 | // Utilities for generating statistics 2 | 3 | var util = module.exports = {}; 4 | 5 | /* 6 | Public: Very lame memoiser function. Only looks at the first argument. 7 | 8 | fn - The function to memoise. 9 | 10 | Returns a wrapped function 11 | 12 | Examples 13 | 14 | myFunction = util.cache(function myFunction() { ... }); 15 | 16 | */ 17 | util.functionCache = {}; 18 | util.cache = function cache(fn) { 19 | util.functionCache[fn.name] = {}; 20 | 21 | return function(key) { 22 | if (typeof key === "object") { 23 | if (key["__cache_" + fn.name]) 24 | return key["__cache_" + fn.name]; 25 | 26 | Object.defineProperty(key, "__cache_" + fn.name, { 27 | value: fn.apply(util, arguments), 28 | enumerable: false 29 | }); 30 | 31 | return key["__cache_" + fn.name]; 32 | } 33 | 34 | if (typeof key !== "object" && 35 | util.functionCache[fn.name][key]) 36 | return util.functionCache[fn.name][key]; 37 | 38 | return ( 39 | util.functionCache[fn.name][key] = 40 | fn.apply(util, arguments) 41 | ); 42 | }; 43 | }; 44 | 45 | /* 46 | Public: For use in mapping. Returns a function that returns the specified 47 | key from its input data. 48 | 49 | key - The key to return. 50 | 51 | Returns the function that performs the property access. 52 | 53 | Examples 54 | 55 | // Returns all the data properties from the objects in the array 56 | var arr = arr.map(util.withkey("data")); 57 | 58 | */ 59 | util.withkey = util.cache(function withkey(key) { 60 | return function(data) { 61 | return data[key]; 62 | }; 63 | }); 64 | 65 | /* 66 | Public: For use in filtering. Returns a function that returns true for 67 | instruments of the type (AST Node type) specified in the wrapper function. 68 | 69 | type - The AST Node type which will trigger a truthy return 70 | 71 | Returns true or false 72 | 73 | Examples 74 | 75 | var varDecs = instruments.map(util.isOfType("VariableDeclaration")); 76 | 77 | */ 78 | util.isOfType = util.cache(function isOfType(types) { 79 | types = types instanceof Array ? types : [types]; 80 | 81 | return function(item) { 82 | return types.reduce(function(acc, cur) { 83 | return acc || ~item.stack.indexOf(cur); 84 | }, false); 85 | }; 86 | }); 87 | 88 | /* 89 | Public: For use in filtering. Returns true for a given instrument which has 90 | results. 91 | 92 | instrument - An instrument to test 93 | 94 | Returns true if the instrument has results, false if not. 95 | 96 | Examples 97 | 98 | var instrumentsWithResults = instruments.map(util.hasResults); 99 | 100 | */ 101 | util.hasResults = function hasResults(instrument) { 102 | return (instrument && instrument.results && instrument.results.length); 103 | }; 104 | 105 | /* 106 | Public: Returns rounded percentage for the percent a of b. 107 | 108 | Returns a number representing a percentage. 109 | 110 | */ 111 | util.percentage = function percentage(a, b) { 112 | return (+((a/b)*10000)|0) / 100; 113 | }; 114 | 115 | /* 116 | Public: Gets the instrument map from the raw instrumentation data. 117 | 118 | instrumentData - The complete, raw instrument Data 119 | filetag - Only return instruments matching this filetag 120 | 121 | Returns an array of instruments. 122 | 123 | Examples 124 | 125 | var instruments = util.getInstruments(instrumentData); 126 | 127 | */ 128 | util.getInstruments = function getInstruments(instrumentData, filetag) { 129 | if (instrumentData instanceof Array) 130 | return instrumentData; 131 | 132 | return Object.keys(instrumentData).reduce(function(acc, cur) { 133 | var instruments = instrumentData[cur].instruments; 134 | 135 | if (filetag && cur !== filetag) return acc; 136 | 137 | return acc.concat(Object.keys(instruments).map(function(key) { 138 | instruments[key].id = key; 139 | instruments[key].filetag = cur; 140 | return instruments[key]; 141 | })); 142 | }, []); 143 | }; 144 | 145 | /* 146 | Public: Returns the stack depth of the shallowest call across all 147 | instruments in the instrument map 148 | 149 | instrumentMap - The complete instrument map 150 | filetag - Only measure the depth of instruments matching this 151 | filetag 152 | 153 | Returns a function which calculates the value of a given instrument. 154 | 155 | Examples 156 | 157 | var shallowestCallDepth = util.shallowestCall(instrumentMap); 158 | 159 | */ 160 | util.shallowestCall = function shallowestCall(instrumentData, key) { 161 | return Math.min.apply(Math, 162 | util.getInstruments(instrumentData, key) 163 | .filter(util.hasResults) 164 | .map(function(item) { 165 | return Math.min.apply(Math, 166 | item.results.map(util.withkey("depth"))); 167 | }) 168 | ); 169 | }; 170 | 171 | /* 172 | Public: Given the entire instrument map, returns a function which calculates 173 | the 'coverage' value of a given instrument. 174 | 175 | instrumentMap - The complete instrument map 176 | 177 | Returns a function which calculates the value of a given instrument. 178 | 179 | Examples 180 | 181 | var getValue = util.calculateValue(instrumentMap); 182 | var value = getValue(instrument); 183 | 184 | */ 185 | util.calculateValue = function calculateValue(instrumentMap) { 186 | var shallowMark = util.shallowestCall(instrumentMap), 187 | graceThreshold = 5, 188 | inverseDampingFactor = 1.25; 189 | 190 | return function(item) { 191 | return ( 192 | Math.max.apply(Math, 193 | item.results 194 | .map(util.withkey("depth")) 195 | .map(util.calculate.bind( null, 196 | shallowMark, 197 | graceThreshold, 198 | inverseDampingFactor )))); 199 | }; 200 | }; 201 | 202 | /* 203 | Public: Given a shallowMark, graceThreshold, and depth, this function runs 204 | the per-node calculation responsible for delivering the coverage value. 205 | 206 | shallowMark - The shallowest measured call in the test suite. This is an 207 | approximation of the actual call stack depth of the test, 208 | and is really a balancing term. The true depth of a call as 209 | far as steamshovel is concerned is 'depth - shallowMark' to 210 | determine the relative depth. 211 | grace - The relative depth threshold below which the score of a 212 | given result will not be decreased. 213 | damping - A fitting parameter by which the calculation is made. 214 | SteamShovel uses a default of 1.25. This value is purely 215 | one that seemed to produce 'fair' results in testing. 216 | depth - The depth of the invocation. 217 | 218 | Returns a number representing the score of the given instrument. This score 219 | is derived from the straightforward equation: 220 | 221 | score = 1 / ((damping ^ relativeDepth) / damping) 222 | 223 | relativeDepth is calculated by subtracting the shallowMark and then the 224 | graceThreshold values from the input depth, and taking the result, or 1 — 225 | whichever is larger. 226 | 227 | Examples 228 | 229 | nodeValue = calculate(5, 5, 1.25, 15); 230 | 231 | */ 232 | util.calculate = function calculate( shallowMark, 233 | graceThreshold, 234 | inverseDampingFactor, 235 | depth 236 | ) { 237 | 238 | // Calculate inverse logarithm 239 | var relativeDepth = (depth - shallowMark) + 1; 240 | relativeDepth = relativeDepth - graceThreshold; 241 | relativeDepth = relativeDepth <= 1 ? 1 : relativeDepth; 242 | 243 | return 1 / ( 244 | Math.pow(inverseDampingFactor, relativeDepth) / 245 | inverseDampingFactor); 246 | }; 247 | 248 | /* 249 | Public: Given the entire instrument map, burns memory usage information 250 | calculated from the probe data back into the instrument map. 251 | 252 | This function is poorly realised and is likely to change. 253 | 254 | instrumentMap - The complete instrument map 255 | 256 | Returns the mutated instrument map. 257 | 258 | Examples 259 | 260 | var instruments = util.generateMemoryStats(instrumentMap); 261 | 262 | */ 263 | util.generateMemoryStats = function generateMemoryStats(inputData) { 264 | var defaults = { rss: 0, heapTotal: 0, heapUsed: 0 }, 265 | instruments = util.getInstruments(inputData), 266 | iterationMap = util.generateIterationMap(inputData); 267 | 268 | function mapper(result) { 269 | var prev = result.invocation > 0 ? result.invocation - 1 : 0; 270 | prev = 271 | iterationMap[prev] ? iterationMap[prev].memoryUsage : defaults; 272 | 273 | return { 274 | rss: result.memoryUsage.rss - prev.rss, 275 | heapTotal: result.memoryUsage.heapTotal - prev.heapTotal, 276 | heapUsed: result.memoryUsage.heapUsed - prev.heapUsed, 277 | }; 278 | } 279 | 280 | function reducer(acc, cur, idx, array) { 281 | var factor = 1 / array.length; 282 | acc.rss += cur.rss * factor; 283 | acc.heapTotal += cur.heapTotal * factor; 284 | acc.heapUsed += cur.heapUsed * factor; 285 | return acc; 286 | } 287 | 288 | for (var i = 0; i < instruments.length; i++) 289 | instruments[i].avgMemChanges = 290 | instruments[i] 291 | .results.map(mapper) 292 | .reduce(reducer, defaults); 293 | 294 | return instruments; 295 | }; 296 | 297 | /* 298 | Public: Given the entire instrument map, return a list of probe calls with 299 | their associated data. 300 | 301 | instrumentMap - The complete instrument map 302 | filetag - Scope to instruments matching this filetag 303 | 304 | Returns a list of invocations (probe calls) with associated data. 305 | 306 | Examples 307 | 308 | var iterations = util.generateIterationMap(instrumentMap); 309 | 310 | */ 311 | util.generateIterationMap = function generateIterationMap(instrumentMap, key) { 312 | return ( 313 | util.getInstruments(instrumentMap, key) 314 | .reduce(function(acc, cur) { 315 | return acc.concat(cur.results.map(function(result) { 316 | return { 317 | invocation: result.invocation, 318 | key: cur.key, 319 | loc: cur.loc.start, 320 | kind: cur.stack[cur.stack.length-1], 321 | depth: result.depth, 322 | time: result.time, 323 | timeOffset: result.timeOffset, 324 | memoryUsage: result.memoryUsage, 325 | milestone: result.milestone, 326 | isLine: !!cur.line 327 | }; 328 | })); 329 | }, []) 330 | .sort(function(a, b) { 331 | return a.invocation - b.invocation; 332 | }) 333 | ); 334 | }; -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Define local bin 2 | BIN=./node_modules/.bin 3 | MOCHA=${BIN}/mocha 4 | JSHINT=${BIN}/jshint 5 | REPORTER=$(shell pwd)/lib/reporter.js 6 | 7 | all: install lint test instrument report memdump file restore 8 | 9 | install: 10 | npm install 11 | 12 | # Linting 13 | # Uses the local jshint script to provide a code lint report 14 | lint: jshint 15 | jshint: install 16 | ${JSHINT} ./lib 17 | @echo "Linting: Everything seems OK!"; 18 | 19 | test: install 20 | ${MOCHA} -R spec 21 | @echo "Testing: Everything seems OK!"; 22 | 23 | cover: coverage 24 | 25 | coverage: test instrument report file restore 26 | 27 | # Instrument the code 28 | instrument: 29 | ./lib/cli.js lib lib-cov 30 | 31 | # Run the tests and generate the report 32 | report: instrument 33 | ${MOCHA} -R ${REPORTER} 34 | 35 | # File away the report html 36 | file: report 37 | mkdir -p ./reports 38 | cp report.html "./reports/$(shell date).html" 39 | cp report.csv "./reports/$(shell date).csv" 40 | @echo "Archived reports."; 41 | 42 | # Restore the lib folder to its original location 43 | clean: restore 44 | restore: 45 | rm -rfv lib-cov 46 | 47 | # Memory instrumentation 48 | memory: memdump restore 49 | memdump: instrument 50 | REPORTER_OUTPUT=csv ${MOCHA} -R ${REPORTER} 51 | 52 | # Demonstrate instrumentor 53 | demonstrate: instrument 54 | cat ./lib-cov/instrumentor.js | more 55 | make clean 56 | 57 | # Output RAW instrument map 58 | raw: instrument 59 | REPORTER_OUTPUT=json ${MOCHA} -R ${REPORTER} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steamshovel", 3 | "version": "0.1.0a3", 4 | "description": "Code Coverage Done Right!", 5 | "main": "lib/index.js", 6 | "dependencies": { 7 | "escodegen": "~1.3.0", 8 | "esprima": "~1.0.4", 9 | "estraverse": "~1.5.0", 10 | "mocha": "~1.18.0", 11 | "mkdirp": "~0.3.5", 12 | "columnify": "~0.1.2", 13 | "jade": "~1.3.0", 14 | "lodash": "~2.4.1" 15 | }, 16 | "repository": "https://github.com/cgiffard/SteamShovel.git", 17 | "devDependencies": { 18 | "chai": "~1.9.1", 19 | "jshint": "^2.5.1" 20 | }, 21 | "scripts": { 22 | "test": "./node_modules/.bin/mocha -R spec test", 23 | "coverage": "make coverage" 24 | }, 25 | "bin": { 26 | "shovel": "lib/cli.js" 27 | }, 28 | "keywords": [ 29 | "coverage", 30 | "mocha", 31 | "test", 32 | "framework", 33 | "steam", 34 | "shovel", 35 | "instrument", 36 | "source", 37 | "code" 38 | ], 39 | "author": "Christopher Giffard ", 40 | "license": "BSD-3-Clause" 41 | } 42 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | chai.should(); 3 | 4 | path = global.__steamshovel ? "../lib-cov" : "../lib"; 5 | 6 | describe("Instrumentor", function() { 7 | var instrument = require(path + "/instrumentor"); 8 | 9 | it("should export a function", function() { 10 | instrument.should.be.a("function"); 11 | }); 12 | 13 | describe("when parsing a simple AST", function() { 14 | 15 | var inCode, outCode; 16 | 17 | it("should appropriately instrument the code", function() { 18 | 19 | inCode = "console.log('fish');", 20 | outCode = instrument(inCode); 21 | 22 | outCode.length.should.be.greaterThan(inCode.length); 23 | }); 24 | 25 | it("should incorporate the instrumentor in the output", function() { 26 | var instrumentor = require("../lib/instrumentor-record").toString(); 27 | 28 | outCode.indexOf(instrumentor).should.be.greaterThan(-1); 29 | }); 30 | 31 | it("should incorporate the instrumentor map", function() { 32 | outCode.indexOf("__instrumentor_map__").should.be.greaterThan(-1); 33 | }); 34 | }); 35 | 36 | it("should embed the filename in the output, where specified", function() { 37 | instrument("abc = edf;", "hijklmnop") 38 | .indexOf("hijklmnop").should.be.greaterThan(0); 39 | }); 40 | 41 | it("should return accurate loc and range info in map", function() { 42 | var result = instrument("var abc = ('def' || 'abc');"); 43 | 44 | result.indexOf('"line":1,"column":11').should.be.greaterThan(0); 45 | result.indexOf('"line":1,"column":25').should.be.greaterThan(0); 46 | result.indexOf('[11,25]').should.be.greaterThan(0); 47 | 48 | }); 49 | 50 | describe("Edge Cases", function() { 51 | 52 | it("should not break CallExpressions with MemberExpression callees", 53 | function() { 54 | 55 | instrument("abc.def('abc')", null, false) 56 | .indexOf("abc.def)").should.equal(-1); 57 | }); 58 | 59 | it("should not replace the left side of AssignmentExpressions", 60 | function() { 61 | 62 | instrument("var abc = def;", null, false) 63 | .indexOf("var abc = ").should.be.greaterThan(-1); 64 | }); 65 | 66 | it("should not instrument the components of MemberExpressions", 67 | function() { 68 | 69 | var code = 70 | "module.exports = function instrumentCode(data, filename) {};"; 71 | 72 | instrument(code, null, false) 73 | .indexOf("module.exports = ").should.be.greaterThan(-1); 74 | 75 | }); 76 | 77 | it("should not break Functions into their components", 78 | function() { 79 | 80 | var code = 81 | "module.exports = function instrumentCode(data, filename) {};", 82 | code2 = "function instrumentCode(data, filename) {};"; 83 | 84 | // FunctionExpression 85 | 86 | instrument(code, null, false) 87 | .indexOf("function instrumentCode").should.be.greaterThan(-1); 88 | 89 | instrument(code, null, false) 90 | .indexOf("data, filename").should.be.greaterThan(-1); 91 | 92 | // FunctionDeclaration 93 | 94 | instrument(code2, null, false) 95 | .indexOf("function instrumentCode").should.be.greaterThan(-1); 96 | 97 | instrument(code2, null, false) 98 | .indexOf("data, filename").should.be.greaterThan(-1); 99 | }); 100 | 101 | it("should not touch the arguments to UpdateExpressions", 102 | function() { 103 | 104 | var code = "abc.def ++", 105 | code2 = "++ abc.def"; 106 | 107 | // Infix 108 | 109 | instrument(code, null, false) 110 | .indexOf("abc.def++").should.be.greaterThan(-1); 111 | 112 | // Prefix 113 | 114 | instrument(code2, null, false) 115 | .indexOf("++abc.def").should.be.greaterThan(-1); 116 | 117 | }); 118 | 119 | }); 120 | 121 | }); -------------------------------------------------------------------------------- /test/map.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"), 2 | expect = chai.expect; 3 | chai.should(); 4 | 5 | path = global.__steamshovel ? "../lib-cov" : "../lib"; 6 | 7 | describe("Instrument map validity", function() { 8 | var instrument = require(path + "/instrumentor"); 9 | 10 | describe("when parsing a simple AST", function() { 11 | 12 | // Fixture for testing stats 13 | var fixtureSource = "console.log('hello world!');//hello!\n"; 14 | fixtureSource+= "console.log(a = b + 1);"; 15 | 16 | // Generating a very simple instrument map to test statistics against 17 | var testFixture = instrument(fixtureSource, "dummy.js"); 18 | testFixture = testFixture.split(/\n+/)[4]; 19 | testFixture = testFixture.substr(57).replace(/\;$/,""); 20 | testFixture = JSON.parse(testFixture); 21 | 22 | it("includes the original source of the document", function() { 23 | expect(testFixture.source).to.equal(fixtureSource); 24 | }); 25 | 26 | it("includes the document filename, prefixed",function(){ 27 | expect(testFixture.filename).to.equal("dummy.js"); 28 | }); 29 | 30 | it("includes the md5 sum of the document filename, prefixed",function(){ 31 | expect(testFixture.filetag) 32 | .to.equal("ic6c44d10e8569f579e7a40c3a91caad0"); 33 | }); 34 | 35 | it("includes a list of comment character ranges", function() { 36 | expect(testFixture.comments).to.be.an("array"); 37 | expect(testFixture.comments.length).to.equal(1); 38 | expect(testFixture.comments[0]).to.be.an("array"); 39 | expect(testFixture.comments[0].length).to.equal(2); 40 | expect(testFixture.comments[0][0]).to.equal(28); 41 | expect(testFixture.comments[0][1]).to.equal(36); 42 | }); 43 | 44 | it("correctly locates all the instrumentable expressions", function() { 45 | expect(testFixture.instruments).to.be.an("object"); 46 | expect(Object.keys(testFixture.instruments).length).to.equal(9) 47 | }); 48 | 49 | it("exposes the loc and range objects", function() { 50 | for (var instrument in testFixture.instruments) { 51 | if (!testFixture.instruments.hasOwnProperty(instrument)) 52 | continue; 53 | 54 | instrument = testFixture.instruments[instrument]; 55 | 56 | expect(instrument).to.have.property("loc"); 57 | expect(instrument.loc).to.be.an("object"); 58 | expect(instrument.loc).to.have.property("start"); 59 | expect(instrument.loc).to.have.property("end"); 60 | expect(instrument.loc.start).to.be.an("object"); 61 | expect(instrument.loc.start).to.have.property("line"); 62 | expect(instrument.loc.start).to.have.property("column"); 63 | expect(instrument.loc.end).to.be.an("object"); 64 | expect(instrument.loc.end).to.have.property("line"); 65 | expect(instrument.loc.end).to.have.property("column"); 66 | 67 | expect(instrument).to.have.property("range"); 68 | expect(instrument.range).to.be.an.instanceOf(Array); 69 | expect(instrument.range.length).to.equal(2); 70 | } 71 | }); 72 | 73 | it("exposes a results array", function() { 74 | for (var instrument in testFixture.instruments) { 75 | if (!testFixture.instruments.hasOwnProperty(instrument)) 76 | continue; 77 | 78 | instrument = testFixture.instruments[instrument]; 79 | 80 | expect(instrument).to.have.property("results"); 81 | expect(instrument.results).to.be.an("array"); 82 | } 83 | }); 84 | 85 | it("exposes a stack", function() { 86 | for (var instrument in testFixture.instruments) { 87 | if (!testFixture.instruments.hasOwnProperty(instrument)) 88 | continue; 89 | 90 | instrument = testFixture.instruments[instrument]; 91 | 92 | expect(instrument).to.have.property("stack"); 93 | expect(instrument.stack).to.be.an("array"); 94 | expect(instrument.stack.length).to.be.greaterThan(0); 95 | } 96 | }); 97 | 98 | it("exposes a type", function() { 99 | for (var instrument in testFixture.instruments) { 100 | if (!testFixture.instruments.hasOwnProperty(instrument)) 101 | continue; 102 | 103 | instrument = testFixture.instruments[instrument]; 104 | 105 | expect(instrument).to.have.property("type"); 106 | expect(instrument.type).to.be.a("string"); 107 | } 108 | }); 109 | }); 110 | }); -------------------------------------------------------------------------------- /test/output.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgiffard/SteamShovel/7c9a4baf4e1f9390b352742ab26a6900c78b6e25/test/output.js -------------------------------------------------------------------------------- /test/stats.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"), 2 | expect = chai.expect; 3 | chai.should(); 4 | 5 | // In this instance, we're not testing the instrumentor, so we want the 6 | // un-instrumented version. 7 | var instrument = require("../lib/instrumentor"); 8 | 9 | path = global.__steamshovel ? "../lib-cov" : "../lib"; 10 | 11 | // A quick function to add some dummy results to the map. 12 | function addSomeResults(map, instrumentcount, filterise, depth) { 13 | var iteration = 0; 14 | 15 | filterise = filterise || function() { return true; }; 16 | depth = depth || 1; 17 | 18 | Object.keys(map).forEach(function(key) { 19 | Object.keys(map[key].instruments) 20 | .map(function(instrument) { 21 | return map[key].instruments[instrument]; 22 | }) 23 | .filter(filterise) 24 | .slice(0,instrumentcount).forEach(function(instrument) { 25 | iteration ++; 26 | instrument.results.push({ 27 | "depth": depth, 28 | "time": Date.now(), 29 | "timeOffset": 1, 30 | "memoryUsage": process.memoryUsage(), 31 | "invocation": iteration, 32 | "milestone": "Hello!", 33 | "value": Math.random() 34 | }); 35 | }); 36 | }); 37 | } 38 | 39 | function clearResults(map) { 40 | Object.keys(map).forEach(function(key) { 41 | Object.keys(map[key].instruments) 42 | .map(function(instrument) { 43 | return map[key].instruments[instrument]; 44 | }) 45 | .forEach(function(instrument) { 46 | instrument.results.length = 0; 47 | }); 48 | }); 49 | } 50 | 51 | describe("Statistics", function() { 52 | var stats = require(path + "/stats"), 53 | util = require(path + "/stats/utils"); 54 | 55 | // Fixture for testing stats 56 | var fixtureSource = "if (1) { console.log(1 ? 'hello world!' : a); }\n"; 57 | fixtureSource+= "console.log(a = b + 1);\n" 58 | fixtureSource+= "function console() { return function() {}; }"; 59 | 60 | // Generating a very simple instrument map to test statistics against 61 | var testFixture = instrument(fixtureSource, "dummy.js"); 62 | testFixture = testFixture.split(/\n+/)[4]; 63 | testFixture = testFixture.substr(57).replace(/\;$/,""); 64 | testFixture = JSON.parse(testFixture); 65 | 66 | var instrumentMap = {}; 67 | instrumentMap[testFixture.filetag] = testFixture; 68 | 69 | beforeEach(function() { 70 | clearResults(instrumentMap); 71 | }); 72 | 73 | it("should export an object with methods", function() { 74 | stats.should.be.an("object"); 75 | stats.basic.should.be.a("function"); 76 | }); 77 | 78 | it("should calculate all basic statistics when called generally", 79 | function() { 80 | 81 | var data = stats.basic(instrumentMap); 82 | 83 | data.lines.should.be.an("object"); 84 | data.conditions.should.be.an("object"); 85 | data.expressions.should.be.an("object"); 86 | data.functions.should.be.an("object"); 87 | }); 88 | 89 | it("should be able to retrieve instruments from the map", function() { 90 | var instruments = util.getInstruments(instrumentMap); 91 | expect(instruments.length).to.equal(17); 92 | }); 93 | 94 | it("should retrieve instruments from the map filtered by key", function() { 95 | var demoKey = "ic6c44d10e8569f579e7a40c3a91caad0"; 96 | expect(util.getInstruments(instrumentMap, "abc").length).to.equal(0); 97 | expect(util.getInstruments(instrumentMap, demoKey).length).to.equal(17); 98 | }); 99 | 100 | it("should be able to get the shallowest map call depth", function() { 101 | addSomeResults(instrumentMap, 5, util.withkey("line"), 10); 102 | 103 | expect(util.shallowestCall(instrumentMap)).to.equal(10); 104 | 105 | addSomeResults(instrumentMap, 10, null, 50); 106 | 107 | expect(util.shallowestCall(instrumentMap)).to.equal(10); 108 | 109 | addSomeResults(instrumentMap, 10, null, 3); 110 | 111 | expect(util.shallowestCall(instrumentMap)).to.equal(3); 112 | }); 113 | 114 | it("should be able to get the computed value of an instrument", function() { 115 | addSomeResults(instrumentMap, 3, util.withkey("line"), 10); 116 | addSomeResults(instrumentMap, 10, null, 50); 117 | 118 | var instruments = util.getInstruments(instrumentMap); 119 | var getValue = util.calculateValue(instrumentMap); 120 | 121 | expect(getValue(instruments[0])).to.be.greaterThan(0); 122 | expect(getValue(instruments[0])).to.be.lessThan(0.01); 123 | expect(getValue(instruments[0])).to.be.greaterThan(0.000001); 124 | 125 | clearResults(instrumentMap); 126 | addSomeResults(instrumentMap, 10, null, 2); 127 | expect(getValue(instruments[0])).to.equal(1); 128 | }); 129 | 130 | it("should calculate lines from an example map", function() { 131 | addSomeResults(instrumentMap, 3, util.withkey("line")); 132 | var data = stats.basic.lines(instrumentMap); 133 | 134 | // First with the very even data... 135 | expect(data.total).to.equal(5); 136 | expect(data.covered).to.equal(3); 137 | expect(data.percentage).to.equal(3/5 * 100); 138 | 139 | // Now with some messy data... 140 | addSomeResults(instrumentMap, 5, util.withkey("line"), 10); 141 | data = stats.basic.lines(instrumentMap); 142 | 143 | expect(data.total).to.equal(5); 144 | expect(data.covered).to.equal(3.8192000000000004); 145 | expect(Math.floor(data.percentage * 100)/100) 146 | .to.equal(Math.floor(3.8192000000000004/5 * 10000) / 100); 147 | }); 148 | 149 | it("should calculate conditions from an example map", function() { 150 | addSomeResults(instrumentMap, 3, util.isOfType([ 151 | "IfStatement", 152 | "LogicalExpression", 153 | "ConditionalExpression", 154 | ])); 155 | 156 | var data = stats.basic.conditions(instrumentMap); 157 | 158 | expect(data.total).to.equal(2); 159 | expect(data.covered).to.equal(0.75); 160 | expect(Math.floor(data.percentage * 100)/100) 161 | .to.equal(Math.floor(3/8 * 10000) / 100); 162 | 163 | 164 | addSomeResults(instrumentMap, 8, util.isOfType([ 165 | "IfStatement", 166 | "LogicalExpression", 167 | "ConditionalExpression", 168 | ]), 169 | 10); 170 | 171 | data = stats.basic.conditions(instrumentMap); 172 | 173 | expect(data.total).to.equal(2); 174 | expect(data.covered).to.equal(1.2620000000000002); 175 | expect(Math.floor(data.percentage * 100)/100) 176 | .to.equal(Math.floor(5.048000000000001/8 * 10000) / 100); 177 | }); 178 | 179 | it("should calculate expressions from an example map", function() { 180 | var data = stats.basic.expressions(instrumentMap); 181 | 182 | expect(data.total).to.equal(12); 183 | expect(data.covered).to.equal(0); 184 | expect(data.percentage).to.equal(0); 185 | 186 | function expression(instrument) { 187 | return !instrument.line; 188 | } 189 | 190 | addSomeResults(instrumentMap, 3, expression, 1); 191 | addSomeResults(instrumentMap, 10, expression, 7); 192 | 193 | data = stats.basic.expressions(instrumentMap); 194 | 195 | expect(data.total).to.equal(12); 196 | expect(data.covered).to.equal(8.6); 197 | expect(Math.floor(data.percentage * 100)/100) 198 | .to.equal(Math.floor(8.6/12 * 10000) / 100); 199 | }); 200 | 201 | it("should calculate functions from an example map", function() { 202 | 203 | addSomeResults(instrumentMap, 1, util.isOfType([ 204 | "FunctionDeclaration", 205 | "FunctionExpression" 206 | ]), 207 | 1); 208 | addSomeResults(instrumentMap, 2, util.isOfType([ 209 | "FunctionDeclaration", 210 | "FunctionExpression" 211 | ]), 212 | 10); 213 | 214 | var data = stats.basic.functions(instrumentMap); 215 | 216 | expect(data.total).to.equal(2); 217 | expect(data.covered).to.equal(0.9397333333333333); 218 | expect(Math.floor(data.percentage * 100)/100) 219 | .to.equal(Math.floor(1.4096/3 * 10000) / 100); 220 | }); 221 | 222 | }); --------------------------------------------------------------------------------