├── .gitignore ├── test ├── code │ ├── base.js │ ├── label_continue.js │ ├── longlines.js │ ├── function.js │ ├── no_block.js │ ├── cond_group_if.js │ ├── for_in.js │ ├── if_no_block.js │ ├── empty_function.js │ ├── function_object.js │ ├── if_else.js │ ├── minified.js │ ├── ternary.js │ ├── cond_multiple_if.js │ ├── cond_decision_if.js │ ├── cond_simple_if.js │ └── cond_simple_if_false.js ├── stats │ ├── one │ │ ├── sub │ │ │ └── second.js │ │ └── first.js │ ├── two │ │ ├── second │ │ │ └── third │ │ │ │ ├── leaf.js │ │ │ │ └── branch.js │ │ ├── first │ │ │ └── sub.js │ │ └── base.js │ └── top.js ├── names │ ├── ternary.js │ ├── objects.js │ └── variables.js ├── reports │ ├── file1.js │ ├── file3.js │ └── file2.js ├── quote.js ├── results │ ├── reports.js │ ├── code.js │ └── details.js ├── details.js ├── statistics.js ├── executeCode.js ├── functionNames.js ├── interpreters.js ├── instrumentFiles.js ├── mergeReports.js └── helpers │ └── utils.js ├── .travis.yml ├── index.js ├── lib ├── instrument.js ├── server │ ├── common.js │ ├── proxy.js │ ├── instrumentation.js │ └── administration.js ├── statistics.js ├── highlight.js ├── fileSystem.js ├── clientCode.js ├── report.js └── interpreters │ └── basic javascript.js ├── views ├── admin.jade ├── stats.jade ├── statics │ ├── loadCharts.js │ └── style.css ├── file.jade └── report.jade ├── merge.js ├── package.json ├── LICENSE ├── instrument.js ├── server.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .* 3 | !.gitignore -------------------------------------------------------------------------------- /test/code/base.js: -------------------------------------------------------------------------------- 1 | var a = {}; 2 | a.something = "hello"; 3 | world = "global"; -------------------------------------------------------------------------------- /test/code/label_continue.js: -------------------------------------------------------------------------------- 1 | mylabel : for (var i = 0; i < 1; i += 1) continue mylabel; -------------------------------------------------------------------------------- /test/code/longlines.js: -------------------------------------------------------------------------------- 1 | var obj = { 2 | a : "a", 3 | b : "b", 4 | c : "c" 5 | }, d = 4; -------------------------------------------------------------------------------- /test/stats/one/sub/second.js: -------------------------------------------------------------------------------- 1 | var one = 1, two = 2, three = 3; if (three = one + two) three = true; -------------------------------------------------------------------------------- /test/code/function.js: -------------------------------------------------------------------------------- 1 | function something (a, b) { 2 | var c = a || b; 3 | var d = a && b; 4 | } 5 | something(); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6.9.1 4 | before_install: 5 | - npm install -g npm@6.4.1 6 | -------------------------------------------------------------------------------- /test/code/no_block.js: -------------------------------------------------------------------------------- 1 | for (;false;) break; 2 | 3 | for (i in {}) break; 4 | 5 | while (false) break; 6 | 7 | do a = 1; while (false); -------------------------------------------------------------------------------- /test/code/cond_group_if.js: -------------------------------------------------------------------------------- 1 | if ((true || true) && false) { 2 | a = 0; 3 | } 4 | 5 | if ((2 + 2 || 3 + 3) && (!(2 - 2) && 3 - 3)) { 6 | a = 1; 7 | } -------------------------------------------------------------------------------- /test/stats/two/second/third/leaf.js: -------------------------------------------------------------------------------- 1 | var aFileHere = 1; 2 | 3 | if (false) { 4 | var a = 1; 5 | a = 2; 6 | a = 3; 7 | } 8 | 9 | aFileHere = true; -------------------------------------------------------------------------------- /test/stats/two/second/third/branch.js: -------------------------------------------------------------------------------- 1 | var a = function () { 2 | return true; 3 | }; 4 | var b = false; 5 | if (a) { 6 | if (b) { 7 | a(); 8 | } 9 | } -------------------------------------------------------------------------------- /test/code/for_in.js: -------------------------------------------------------------------------------- 1 | var obj = { 2 | num : 1, 3 | fn : function () { 4 | var num = 1; 5 | }, 6 | arr : [] 7 | }; 8 | for (key in obj) if (obj[key].call) obj[key].call(this) -------------------------------------------------------------------------------- /test/stats/two/first/sub.js: -------------------------------------------------------------------------------- 1 | function nothing () { 2 | var a = 1; 3 | a = 2; 4 | a = 3; 5 | } 6 | 7 | function nothingElse () { 8 | var a = 1; 9 | a = 2; 10 | a = 3; 11 | } -------------------------------------------------------------------------------- /test/stats/one/first.js: -------------------------------------------------------------------------------- 1 | switch (4) { 2 | case 0 : 3 | var a = 1; 4 | case 1 : 5 | var b = 2; 6 | case 4 : 7 | var c = 3; 8 | break; 9 | default : 10 | var d = 4; 11 | } -------------------------------------------------------------------------------- /test/stats/two/base.js: -------------------------------------------------------------------------------- 1 | function nothing () { 2 | var a = 1; 3 | a = 2; 4 | a = 3; 5 | } 6 | 7 | var doNothing = { 8 | a : 1, 9 | b : 1, 10 | c : function () { 11 | var d = 1; 12 | d = 2; 13 | } 14 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Allow this module to be used as a library 2 | 3 | exports.report = require("./lib/report"); 4 | exports.instrument = require("./lib/instrument"); 5 | exports.admin = require("./lib/server/administration"); 6 | -------------------------------------------------------------------------------- /test/stats/top.js: -------------------------------------------------------------------------------- 1 | var glob = { 2 | a : function () { 3 | if (true) { 4 | var b = 1; 5 | } 6 | }, 7 | b : function () { 8 | var c = (function () { 9 | return false 10 | })(); 11 | } 12 | }; 13 | 14 | glob.a(); -------------------------------------------------------------------------------- /test/code/if_no_block.js: -------------------------------------------------------------------------------- 1 | function doSomething () { 2 | var a = 1; 3 | } 4 | function somethingElse () { 5 | var a = 2; 6 | } 7 | 8 | if (true) doSomething() 9 | else sometingElse() 10 | 11 | if (false) { 12 | doSomething(); 13 | } -------------------------------------------------------------------------------- /test/code/empty_function.js: -------------------------------------------------------------------------------- 1 | var empty = function () {}; 2 | 3 | var full = function (a, b) { 4 | a.call(); 5 | } 6 | 7 | var useMePlease = function () { 8 | return (function b () { 9 | return true; 10 | })(); 11 | } 12 | 13 | full(useMePlease); -------------------------------------------------------------------------------- /test/code/function_object.js: -------------------------------------------------------------------------------- 1 | var fun1 = function () { 2 | return 1 3 | }, fun2 = function () { 4 | return 2 5 | }; 6 | 7 | var ofun = { 8 | one : function () { 9 | return 1 10 | }, 11 | two : function () { 12 | return 2 13 | } 14 | } 15 | 16 | ofun.one(fun2()) == fun1(ofun.two()) -------------------------------------------------------------------------------- /test/code/if_else.js: -------------------------------------------------------------------------------- 1 | function not(a, b) { 2 | if (a && b) { 3 | return true; 4 | } else if (a || b) { 5 | return true; 6 | } else { 7 | return false; 8 | } 9 | } 10 | 11 | if (not(true, true)) { 12 | not(true, false); 13 | } else if (not(false, false)) { 14 | not(false, true); 15 | } -------------------------------------------------------------------------------- /test/code/minified.js: -------------------------------------------------------------------------------- 1 | // Minified file 2 | function one () {var two = 2;}; function two () {var two = 2;}; function three () {var two = 2;}; one(); var two = 2; one (2); 3 | 4 | if (false) {var b = 0;} if (true) {var c = 0;} if (false) {var d = 0;} 5 | 6 | var first = {method : function () {}}, second = {method : function () {}}, third = {method : function () {}}; first.method(); -------------------------------------------------------------------------------- /test/names/ternary.js: -------------------------------------------------------------------------------- 1 | var outside = true ? function () {return true} : function () {return false}; 2 | 3 | var inside = false ? function one () {return true} : function () {return false}; 4 | 5 | var obj = {}; 6 | obj.property = true ? function () {return true} : function () {return false}; 7 | 8 | obj.another = false ? function () {return true} : function two () {return false}; -------------------------------------------------------------------------------- /test/names/objects.js: -------------------------------------------------------------------------------- 1 | function withName () {}; 2 | 3 | var someObject = { 4 | first : function () {}, 5 | "second" : function () {}, 6 | "th:ird" : function () {} 7 | }; 8 | 9 | someObject.assigned = function () {}; 10 | 11 | // some statements to confuse 12 | someObject.counter = 0; 13 | someObject.counter += 1; 14 | 15 | var a, b; 16 | a = 0; 17 | b = function () {}; -------------------------------------------------------------------------------- /lib/instrument.js: -------------------------------------------------------------------------------- 1 | var clientCode = require("./clientCode"); 2 | 3 | function instrument (file, content, options) { 4 | var interpreter = require("./fileSystem").getInterpreter(file, content); 5 | 6 | if (interpreter) { 7 | return interpreter.interpret(file, content, options); 8 | } else { 9 | return clientCode.formatContent(content); 10 | } 11 | } 12 | 13 | module.exports = instrument; -------------------------------------------------------------------------------- /test/reports/file1.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var closure = "something", position; 3 | 4 | switch (closure.indexOf("e")) { 5 | case 0: 6 | position = 1; 7 | break; 8 | case 3: 9 | position = 4; 10 | // There's not a break 11 | default: 12 | position = 5; 13 | break; 14 | } 15 | 16 | for (var i = 0; i < position; i += 1) { 17 | var doNothing = "but with class"; 18 | } 19 | })(); -------------------------------------------------------------------------------- /test/reports/file3.js: -------------------------------------------------------------------------------- 1 | var someFalsyValue = false; 2 | 3 | function justAFunction () {}; 4 | 5 | if (someFalsyValue || window.thisIsAGlobalVariable) { 6 | var whatsThis = (function () { 7 | return function (anotherFunction) { 8 | anotherFunction.call(); 9 | } 10 | })(); 11 | 12 | whatsThis(justAFunction); 13 | } else { 14 | for (var i = 0; i < 10; i += 1) { 15 | someFalsyValue = true; 16 | } 17 | } -------------------------------------------------------------------------------- /test/code/ternary.js: -------------------------------------------------------------------------------- 1 | var bool = true ? true : false; 2 | 3 | var fn = false ? function () {return true} : function () {return false} 4 | 5 | var cond = { 6 | num : fn() ? 1 : 0 7 | }; 8 | 9 | var multiple = false || fn() || cond.num > 0 ? 1 : 2; 10 | 11 | var obj = { 12 | one : true ? function () {return true} : function () {return false} 13 | }; 14 | obj.two = false ? function () {return true} : function () {return false}; -------------------------------------------------------------------------------- /test/code/cond_multiple_if.js: -------------------------------------------------------------------------------- 1 | var saySomething = function (arg) { 2 | return arg; 3 | }, a; 4 | 5 | if (true && true) { 6 | a = 0; 7 | } 8 | 9 | if (true && false) { 10 | a = 1; 11 | } 12 | 13 | if (false || true) { 14 | a = 2; 15 | } 16 | 17 | if (saySomething(false) || true) { 18 | a = 3; 19 | } 20 | 21 | if (saySomething(true) && saySomething(false)) { 22 | a = 4; 23 | } 24 | 25 | if (true && false && true && false) { 26 | a = 5; 27 | } -------------------------------------------------------------------------------- /test/code/cond_decision_if.js: -------------------------------------------------------------------------------- 1 | var saySomething = function (arg) { 2 | return arg; 3 | }, a; 4 | 5 | if (0 + 1) { 6 | a = 0; 7 | } 8 | 9 | if (saySomething(5) - 1) { 10 | a = 1; 11 | } 12 | 13 | if (a = 3) { 14 | a = 2; 15 | } 16 | 17 | if (a = 3, true) { 18 | a = 3; 19 | } 20 | 21 | if (true, false) { 22 | a = 4; 23 | } 24 | 25 | if (0 > saySomething(-3)) { 26 | a = 5; 27 | } 28 | 29 | var isFalse = !(!!saySomething(true)); 30 | if (isFalse) { 31 | a = 6; 32 | } -------------------------------------------------------------------------------- /test/code/cond_simple_if.js: -------------------------------------------------------------------------------- 1 | var sayTrue = function () {return true}, a = 1; 2 | 3 | // true conditions 4 | if (true) { 5 | a = 1 6 | } 7 | 8 | if (sayTrue()) { 9 | a = 1 10 | } 11 | 12 | if ((function () {return true})()) { 13 | a = 1 14 | } 15 | 16 | if ((function (arg) {return arg})(true)) { 17 | a = 1 18 | } 19 | 20 | if (a) { 21 | a = 1 22 | } 23 | 24 | if (1) { 25 | a = 1 26 | } 27 | 28 | if (!0) { 29 | a = 1 30 | } 31 | 32 | if ({}) { 33 | a = 1 34 | } 35 | 36 | if ([]) { 37 | a = 1 38 | } -------------------------------------------------------------------------------- /test/code/cond_simple_if_false.js: -------------------------------------------------------------------------------- 1 | var sayTrue = function () {return true}, a = 1; 2 | 3 | if (false) { 4 | a = 1 5 | } 6 | 7 | if (!sayTrue()) { 8 | a = 1 9 | } 10 | 11 | if ((function () {return false})()) { 12 | a = 1 13 | } 14 | 15 | if ((function (arg) {return arg})(false)) { 16 | a = 1 17 | } 18 | 19 | if (!a) { 20 | a = 1 21 | } 22 | 23 | if (0) { 24 | a = 1 25 | } 26 | 27 | if (!1) { 28 | a = 1 29 | } 30 | 31 | if (!{}) { 32 | a = 1 33 | } 34 | 35 | if ([].length) { 36 | a = 1 37 | } 38 | 39 | if ("a string") { 40 | a = 1; 41 | } -------------------------------------------------------------------------------- /test/names/variables.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // this function is really anonymous 3 | }); 4 | 5 | function glob () { 6 | // this function has a name 7 | }; 8 | 9 | var name = function () { 10 | // could use "name" for this function 11 | }; 12 | 13 | var first = function () {}, second = function () {}; 14 | 15 | var override = function thisHasAName () {}; 16 | 17 | // some variable to mess up things 18 | var a = 1, b; 19 | 20 | var nested = function () { 21 | var inner = function () { 22 | function inner () { 23 | // enough 24 | }; 25 | }; 26 | }; -------------------------------------------------------------------------------- /test/reports/file2.js: -------------------------------------------------------------------------------- 1 | (function (context) { 2 | var myRunningFunction = function (shallIDoIt) { 3 | if (shallIDoIt) { 4 | return myRunningFunction(false); 5 | } else { 6 | myStupidFunction(true); 7 | } 8 | }; 9 | 10 | var myStupidFunction = function () { 11 | if (true) { 12 | var doNothing = true; 13 | } else { 14 | var one = 1; 15 | var anotherOne = 1; 16 | var two = one + anotherOne; 17 | } 18 | 19 | return true; 20 | }; 21 | 22 | if (context && context.isTrue) { 23 | myRunningFunction(myStupidFunction()); 24 | } 25 | })({ 26 | isTrue : true, 27 | isFalse : (function () {return false;})() 28 | }); -------------------------------------------------------------------------------- /test/quote.js: -------------------------------------------------------------------------------- 1 | var helpers = require("./helpers/utils"); 2 | var instrument = require("../lib/instrument"); 3 | var report = require("../lib/report"); 4 | 5 | exports.quote = function (test) { 6 | test.expect(2); 7 | 8 | // This filename contains weird symbols 9 | var fileName = "a\\té.js"; 10 | 11 | var code = instrument(fileName, "var a = function () { if (true) {}};", { 12 | "function" : true, 13 | "condition" : true, 14 | "highlight" : true 15 | }).clientCode; 16 | 17 | var result = helpers.executeCode(fileName, code); 18 | 19 | // I expect to find it in the result 20 | test.ok(!!result.files[fileName]); 21 | 22 | test.equals(50, result.files[fileName].statements.percentage); 23 | 24 | test.done(); 25 | }; -------------------------------------------------------------------------------- /views/admin.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html(lang="en") 3 | head 4 | title node-coverage Admin Interface 5 | link(rel="stylesheet", href="#{serverRoot}/style.css") 6 | body 7 | h3.header node-coverage 8 | em=conf.htdoc 9 | form.content(action="merge", method="GET") 10 | h4 Reports from 11 | em=conf.report 12 | 13 | table.reports 14 | thead 15 | tr 16 | -if (canMerge) 17 | th.merge 18 | input(type="submit", value="Merge") 19 | th Report 20 | th Date 21 | tbody 22 | each report in reports 23 | tr 24 | -if (canMerge) 25 | td 26 | input(type="checkbox", name="report", value=report.id) 27 | td 28 | a(href="#{serverRoot}/r/#{report.id}") #{report.id} 29 | td 30 | em=report.date -------------------------------------------------------------------------------- /views/stats.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html(lang="en") 3 | head 4 | title node-coverage Admin Interface 5 | link(rel="stylesheet", href="#{serverRoot}/style.css") 6 | script(src="#{serverRoot}/jQuery/jquery.min.js", type="text/javascript") 7 | script(src="#{serverRoot}/highcharts/highcharts.js", type="text/javascript") 8 | 9 | script 10 | var __report = !{JSON.stringify(report)} 11 | script(src="#{serverRoot}/loadCharts.js", type="text/javascript") 12 | body 13 | h3.header node-coverage 14 | em Stats & Graphs - #{name} 15 | 16 | .summary 17 | h2 Unused lines of code 18 | h1=report.unused 19 | .clear 20 | 21 | .content 22 | each packageReport, length in report.byPackage 23 | div(id="package"+length) 24 | table.chart 25 | thead 26 | tr 27 | th 28 | th Unused lines 29 | th Percentage 30 | tbody 31 | each unused, file in packageReport 32 | tr 33 | td=file 34 | td=unused 35 | td 36 | -var percentage = 100.0 * unused / report.unused 37 | #{percentage.toFixed(2)}% 38 | -------------------------------------------------------------------------------- /merge.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require("fs"); 3 | var report = require("./lib/report"); 4 | var mkdirp = require("mkdirp"); 5 | var path = require("path"); 6 | var argv = require("optimist") 7 | .usage("Merge coverage reports.\n$0 -o destination source [source ...]") 8 | .boolean("h").alias("h", "help") 9 | .alias("o", "output").describe("o", "Output file.") 10 | .argv; 11 | 12 | 13 | if (argv.h) { 14 | help(true); 15 | } else if (!argv.o) { 16 | console.error("Output file is mandatory."); 17 | help(); 18 | } else if (argv._.length < 2) { 19 | console.error("Merge requires at least two files."); 20 | help(); 21 | } else { 22 | good(argv.o, argv._); 23 | } 24 | 25 | function help (cleanExit) { 26 | require("optimist").showHelp(); 27 | if (!cleanExit) { 28 | process.exit(1); 29 | } 30 | } 31 | 32 | function good (destination, files) { 33 | var reports = []; 34 | files.forEach(function (file) { 35 | reports.push(JSON.parse(fs.readFileSync(file))); 36 | }); 37 | mkdirp.sync(path.dirname(destination)); 38 | fs.writeFileSync(destination, JSON.stringify(report.mergeReports(reports))); 39 | } 40 | -------------------------------------------------------------------------------- /test/results/reports.js: -------------------------------------------------------------------------------- 1 | exports.results = { 2 | "file1.js" : { 3 | total : 10, 4 | visited : 8, 5 | statementsPercentage : 100 * 8 / 10, 6 | conditions : 0, 7 | conditionsTrue : 0, 8 | conditionsFalse : 0, 9 | conditionsPercentage : 100, 10 | functions : 1, 11 | functionsCalled : 1, 12 | functionsPercentage : 100 13 | }, 14 | "file2.js" : { 15 | total : 15, 16 | visited : 12, 17 | statementsPercentage : 100 * 12 / 15, 18 | conditions : 4, 19 | conditionsTrue : 4, 20 | conditionsFalse : 1, 21 | conditionsPercentage : 50 * 5 / 4, 22 | functions : 4, 23 | functionsCalled : 4, 24 | functionsPercentage : 100 25 | }, 26 | "merge_1_2" : { 27 | total : 25, 28 | visited : 20, 29 | statementsPercentage : 100 * 20 / 25, 30 | conditions : 4, 31 | conditionsTrue : 4, 32 | conditionsFalse : 1, 33 | conditionsPercentage : 50 * 5 / 4, 34 | functions : 5, 35 | functionsCalled : 5, 36 | functionsPercentage : 100 37 | }, 38 | "file3.js" : { 39 | total : 9, 40 | visited : 5, 41 | statementsPercentage : 100 * 5 / 9, 42 | conditions : 2, 43 | conditionsTrue : 0, 44 | conditionsFalse : 2, 45 | conditionsPercentage : 50, 46 | functions : 3, 47 | functionsCalled : 0, 48 | functionsPercentage : 0 49 | } 50 | }; -------------------------------------------------------------------------------- /lib/server/common.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var fs = require("fs"); 3 | var report = require("../report"); 4 | 5 | var createReportName = exports.createReportName = function (desiredName) { 6 | var now = new Date(); 7 | 8 | var name = "report"; 9 | if (desiredName) { 10 | desiredName = path.basename(desiredName); 11 | 12 | if (desiredName.length > 125) { 13 | desiredName.substring(0, 125); 14 | } 15 | 16 | if (desiredName.charAt(0) !== ".") { 17 | name = desiredName; 18 | } 19 | } 20 | 21 | return name + "_" + now.getTime() + ".json"; 22 | }; 23 | 24 | exports.saveCoverage = function (body, staticInfo, adminRoot, callback) { 25 | var msg; 26 | try { 27 | var coverage = report.generateAll(body, staticInfo); 28 | var fileName = adminRoot + "/" + createReportName(body.name); 29 | 30 | console.log("Saving report", fileName); 31 | fs.writeFile(fileName, JSON.stringify(coverage), "utf-8", function (err) { 32 | if (err) { 33 | msg = "Error while saving coverage report to " + fileName; 34 | console.error(msg, err); 35 | callback.call(null, msg); 36 | } else { 37 | callback.call(null); 38 | } 39 | }); 40 | } catch (ex) { 41 | msg = "Error parsing coverage report"; 42 | console.error(msg, ex); 43 | callback.call(null, msg); 44 | } 45 | }; -------------------------------------------------------------------------------- /test/details.js: -------------------------------------------------------------------------------- 1 | var helpers = require("./helpers/utils"); 2 | var fileSystem = require("../lib/fileSystem"); 3 | 4 | var expectedCoverage = require("./results/details").results; 5 | var totalAssertsPerFile = 9; 6 | 7 | function compare (file, code) { 8 | var generatedReport = helpers.executeCode(file, code); 9 | var shortFileName = helpers.shortName(file); 10 | 11 | var expected = expectedCoverage.code[shortFileName] || expectedCoverage.merge[shortFileName]; 12 | 13 | helpers.assertDetailsEquals(generatedReport.files[file], expected, file, testObject); 14 | 15 | waitingFiles -= 1; 16 | if (waitingFiles < 1) { 17 | testObject.done(); 18 | } 19 | }; 20 | 21 | var testObject; 22 | var waitingFiles; 23 | 24 | exports.codeDetails = function (test) { 25 | var files = Object.keys(expectedCoverage.code); 26 | test.expect(files.length * totalAssertsPerFile); 27 | 28 | testObject = test; 29 | waitingFiles = files.length; 30 | 31 | fileSystem.statFileOrFolder(["test/code/"], "", compare); 32 | }; 33 | 34 | exports.mergeDetails = function (test) { 35 | var files = Object.keys(expectedCoverage.merge); 36 | test.expect(files.length * totalAssertsPerFile); 37 | 38 | testObject = test; 39 | waitingFiles = files.length; 40 | 41 | fileSystem.statFileOrFolder(["test/reports/"], "", compare); 42 | }; -------------------------------------------------------------------------------- /views/statics/loadCharts.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | var report = __report; 3 | if (!__report) { 4 | return; 5 | } 6 | 7 | for (var length in report.byPackage) { 8 | if (report.byPackage.hasOwnProperty(length)) { 9 | var packageReport = report.byPackage[length]; 10 | 11 | var chartInfo = { 12 | chart: { 13 | renderTo: 'package' + length 14 | }, 15 | title: { 16 | text: 'Unused lines of code by package' 17 | }, 18 | tooltip: { 19 | formatter: function() { 20 | return ''+ this.key +': '+ this.percentage.toFixed(2) +' %'; 21 | } 22 | }, 23 | plotOptions: { 24 | pie: { 25 | allowPointSelect: false, 26 | cursor: 'pointer', 27 | dataLabels: { 28 | enabled: true, 29 | color: '#000000', 30 | connectorColor: '#000000', 31 | formatter: function() { 32 | return ''+ this.key +': '+ this.percentage.toFixed(2) +' %'; 33 | } 34 | } 35 | } 36 | }, 37 | series: [{ 38 | type: 'pie', 39 | name: 'Unused code', 40 | data: [] 41 | }] 42 | }; 43 | 44 | for (var file in packageReport) { 45 | if (packageReport.hasOwnProperty(file)) { 46 | chartInfo.series[0].data.push([file, packageReport[file]]) 47 | } 48 | } 49 | 50 | new Highcharts.Chart(chartInfo); 51 | } 52 | } 53 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Fabio Crisci ", 3 | "contributors": [ 4 | { 5 | "name": "David-Emmanuel Divernois", 6 | "email": "david-emmanuel.divernois@amadeus.com" 7 | }, 8 | { 9 | "name": "Francesco Longo", 10 | "email": "francesco.longo@amadeus.com" 11 | }, 12 | { 13 | "name": "Fabiano Bernardo", 14 | "email": "f.bernardo.it@gmail.com" 15 | } 16 | ], 17 | "name": "node-coverage", 18 | "description": "node-coverage is a tool that measures code coverage of JavaScript application.", 19 | "version": "2.1.0", 20 | "homepage": "https://github.com/piuccio/node-coverage", 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/piuccio/node-coverage.git" 24 | }, 25 | "main": "./index", 26 | "bin": { 27 | "node-coverage-instrument": "./instrument.js", 28 | "node-coverage": "./server.js", 29 | "node-coverage-merge": "./merge.js" 30 | }, 31 | "dependencies": { 32 | "mime": "1.x", 33 | "optimist": "~0.3.1", 34 | "uglify-js": "~1.2.5", 35 | "connect": "1.x", 36 | "connect-restreamer": "~1.0.0", 37 | "express": "2.x", 38 | "http-proxy": "~0.8.0", 39 | "jade": "~0.20", 40 | "mkdirp": "~0.3.0" 41 | }, 42 | "devDependencies": { 43 | "nodeunit": "~0.11.3" 44 | }, 45 | "scripts": { 46 | "test": "nodeunit test" 47 | }, 48 | "license": "MIT", 49 | "engines": { 50 | "node": ">=0.6.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/statistics.js: -------------------------------------------------------------------------------- 1 | function iterateOnReport (report, callback) { 2 | for (var fileName in report.files) { 3 | callback.call(this, fileName, report.files[fileName].statements); 4 | } 5 | } 6 | 7 | function normalize (container, count, partial) { 8 | if (!container[count]) { 9 | container[count] = {}; 10 | } 11 | 12 | if (!container[count][partial]) { 13 | container[count][partial] = 0; 14 | } 15 | } 16 | 17 | function statistics (report) { 18 | var totalUnusedCode = 0; 19 | var unused = {}; 20 | var unusedByPackage = {}; 21 | var maxPackage = 0; 22 | 23 | iterateOnReport(report, function (fileName, fileReport) { 24 | var missingLines = fileReport.total - fileReport.covered; 25 | 26 | unused[fileName] = missingLines; 27 | totalUnusedCode += unused[fileName]; 28 | 29 | var packages = fileName.split("/"); 30 | maxPackage = Math.max(maxPackage, packages.length); 31 | }); 32 | 33 | //second loop becose I was missing maxPackage 34 | iterateOnReport(report, function (fileName, fileReport) { 35 | var missingLines = fileReport.total - fileReport.covered; 36 | 37 | var packages = fileName.split("/"); 38 | 39 | packages.forEach(function (folder, count) { 40 | if (!folder) { 41 | // because count = 0 is the first / 42 | return; 43 | } 44 | 45 | var partial = packages.slice(0, count + 1).join("/"); 46 | 47 | normalize(unusedByPackage, count, partial); 48 | 49 | unusedByPackage[count][partial] += missingLines; 50 | }); 51 | 52 | // add it also on higher packages 53 | for (var i = packages.length; i < maxPackage; i += 1) { 54 | normalize(unusedByPackage, i, fileName); 55 | 56 | unusedByPackage[i][fileName] += missingLines; 57 | } 58 | }); 59 | 60 | return { 61 | unused : totalUnusedCode, 62 | byFile : unused, 63 | byPackage : unusedByPackage 64 | }; 65 | } 66 | 67 | module.exports = statistics; -------------------------------------------------------------------------------- /lib/highlight.js: -------------------------------------------------------------------------------- 1 | // Looking for $$_l("filename", "lineId"); 2 | var regStatement = /\$\$_l\("[^"]+",\s?"([^"]+)"\);/g; 3 | var regCondition = /\$\$_c\("[^"]+",\s?"([^"]+)",\s?/g; 4 | var regFunction = /\$\$_f\("[^"]+",\s?"([^"]+)"\);/g; 5 | 6 | /** 7 | * This function returns a cleaned representation of the generated code. 8 | * The return value is an object containing 9 | * 10 | * src : array (one entry per line of code) where value are object with 11 | * s : source line 12 | * l : lineid of the instrumented function 13 | * c : list of conditions (array) 14 | * fns : object mapping a function id to the generated line of code 15 | */ 16 | exports.highlight = function (code) { 17 | var mapped = [], nextCode; 18 | var result = { 19 | src : [], 20 | fns : {} 21 | }; 22 | var split = code.split("\n"); 23 | split.forEach(function (line) { 24 | var match = regStatement.exec(line); 25 | if (match) { 26 | // This line has $$_l it's not code, remember for later 27 | nextCode = match[1]; 28 | } else { 29 | var fnMatch = regFunction.exec(line); 30 | if (fnMatch) { 31 | // The previous line was a function 32 | result.fns[fnMatch[1]] = mapped.length - 1; 33 | } else { 34 | // Code, not necessarly mapped to statement 35 | var tags = [], generatedLine = {}; 36 | 37 | if (nextCode) { 38 | generatedLine.l = nextCode; 39 | } 40 | 41 | while (condition = regCondition.exec(line)) { 42 | tags.push(condition[1]); 43 | } 44 | line = line.replace(regCondition, "("); // because we miss a closing bracket in regexp 45 | if (tags.length > 0) { 46 | generatedLine.c = tags; 47 | } 48 | 49 | generatedLine.s = line; 50 | 51 | mapped.push(generatedLine); 52 | nextCode = null; 53 | } 54 | } 55 | }); 56 | 57 | result.src = mapped; 58 | 59 | return result; 60 | }; -------------------------------------------------------------------------------- /lib/server/proxy.js: -------------------------------------------------------------------------------- 1 | var url = require("url"); 2 | var http = require("http"); 3 | var httpProxy = require("http-proxy"); 4 | var instrument = require("../instrument"); 5 | var fileSystem = require("../fileSystem"); 6 | var common = require("./common"); 7 | var bodyParser = require("connect").bodyParser; 8 | var restreamer = require("connect-restreamer"); 9 | 10 | var options, administrationRoot; 11 | 12 | function proxyServer (request, response, proxy) { 13 | var parsedRequest = url.parse(request.url); 14 | 15 | if (parsedRequest.path == "/node-coverage-store") { 16 | common.saveCoverage(request.body, null, administrationRoot, function (error) { 17 | if (error) { 18 | response.writeHead(500); 19 | response.write(error); 20 | response.end(); 21 | } else { 22 | response.end(); 23 | } 24 | }); 25 | } else if (fileSystem.getInterpreter(request.url, "")) { 26 | // There's at least one interpreter to handle this 27 | var proxyedRequest = http.request(parsedRequest, function (proxyedResponse) { 28 | proxyedResponse.setEncoding('utf-8'); 29 | 30 | var buffer = []; 31 | 32 | proxyedResponse.on('data', function (chunk) { 33 | buffer.push(chunk); 34 | }); 35 | 36 | proxyedResponse.on('end', function () { 37 | var code = instrument(parsedRequest.path, buffer.join(""), options); 38 | response.end(code.clientCode); 39 | }); 40 | }).on('error', function(error) { 41 | response.writeHead(500); 42 | response.write(error.message); 43 | response.end(); 44 | }); 45 | 46 | proxyedRequest.end(); 47 | } else { 48 | proxy.proxyRequest(request, response, { 49 | host: parsedRequest.host, 50 | port: parsedRequest.port || 80 51 | }); 52 | } 53 | } 54 | 55 | exports.start = function (port, adminRoot, coverageOptions) { 56 | options = coverageOptions; 57 | administrationRoot = adminRoot; 58 | 59 | httpProxy.createServer( 60 | bodyParser(), 61 | restreamer(), 62 | proxyServer 63 | ).listen(port); 64 | }; -------------------------------------------------------------------------------- /test/statistics.js: -------------------------------------------------------------------------------- 1 | var helpers = require("./helpers/utils"); 2 | var fileSystem = require("../lib/fileSystem"); 3 | var report = require("../lib/report"); 4 | 5 | var unusedLines = 21; 6 | var byFile = { 7 | 'test/stats/top.js': 2, 8 | 9 | 'test/stats/one/first.js': 3, 10 | 'test/stats/one/sub/second.js': 0, 11 | 12 | 'test/stats/two/base.js': 5, 13 | 'test/stats/two/first/sub.js': 6, 14 | 'test/stats/two/second/third/leaf.js': 3, 15 | 'test/stats/two/second/third/branch.js': 2 16 | }; 17 | var byPackage = { 18 | 0 : { 19 | 'test' : unusedLines 20 | }, 21 | 1 : { 22 | 'test/stats' : unusedLines 23 | }, 24 | 2 : { 25 | 'test/stats/top.js': 2, 26 | 'test/stats/one' : 3, 27 | 'test/stats/two' : 16 28 | }, 29 | 3 : { 30 | 'test/stats/top.js': 2, 31 | 'test/stats/one/first.js': 3, 32 | 'test/stats/one/sub': 0, 33 | 'test/stats/two/base.js': 5, 34 | 'test/stats/two/first': 6, 35 | 'test/stats/two/second': 5 36 | }, 37 | 4 : { 38 | 'test/stats/top.js': 2, 39 | 'test/stats/one/first.js': 3, 40 | 'test/stats/one/sub/second.js': 0, 41 | 'test/stats/two/base.js': 5, 42 | 'test/stats/two/first/sub.js': 6, 43 | 'test/stats/two/second/third': 5 44 | }, 45 | 5 : byFile 46 | }; 47 | 48 | exports.stats = function (test) { 49 | test.expect(8); 50 | 51 | var allReports = []; 52 | 53 | fileSystem.statFileOrFolder(["test/stats/"], "", function (file, code) { 54 | allReports.push(helpers.executeCode(file, code)); 55 | }); 56 | 57 | var merged = report.mergeReports(allReports); 58 | 59 | var statistics = report.stats(merged); 60 | 61 | test.equal(statistics.unused, unusedLines, "Total unused lines"); 62 | test.ok(helpers.objectEquals(statistics.byFile, byFile), "Group by file"); 63 | 64 | for (var length in statistics.byPackage) { 65 | test.ok(helpers.objectEquals( 66 | statistics.byPackage[length], byPackage[length]), "Group by package, depth " + length); 67 | } 68 | 69 | test.done(); 70 | }; -------------------------------------------------------------------------------- /views/file.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html(lang="en") 3 | head 4 | title node-coverage Admin Interface 5 | link(rel="stylesheet", href="#{serverRoot}/style.css") 6 | link(href="http://fonts.googleapis.com/css?family=Inconsolata", rel="stylesheet", type="text/css") 7 | body 8 | h3.header node-coverage 9 | em=name 10 | em=file 11 | .summary 12 | h2 Statement coverage: 13 | h1 #{report.statements.percentage.toFixed(2)}% 14 | .summary 15 | h2 Condition coverage: 16 | h1 #{report.conditions.percentage.toFixed(2)}% 17 | .summary 18 | h2 Function coverage: 19 | h1 #{report.functions.percentage.toFixed(2)}% 20 | .legend 21 | dl 22 | dt # 23 | dd line number 24 | dt count 25 | dd how many times the statement was executed 26 | dt true 27 | dd conditions that were never evaluated true 28 | dt false 29 | dd conditions that were never evaluated false 30 | .clear 31 | 32 | .content 33 | table.small 34 | thead 35 | tr 36 | th # 37 | th count 38 | th true 39 | th false 40 | th 41 | tbody 42 | -var counter = 1 43 | each loc in report.code.src 44 | -var covered = loc.l ? (report.statements.detail[loc.l] > 0) : true 45 | -var coveredClass = covered ? "" : "not-covered" 46 | -var missing = {"true" : [], "false" : []} 47 | -if (covered && loc.c) 48 | -for (i=0; i#{loc.s} -------------------------------------------------------------------------------- /test/executeCode.js: -------------------------------------------------------------------------------- 1 | var helpers = require("./helpers/utils"); 2 | var fileSystem = require("../lib/fileSystem"); 3 | 4 | var expectedCoverage = require("./results/code").results; 5 | // count the number of assert expected (for each test) 6 | var files = Object.keys(expectedCoverage); 7 | var asserts = Object.keys(expectedCoverage[files[0]]); 8 | var totalAssertsPerTest = 2 * files.length * asserts.length; 9 | 10 | function compare (file, code, options) { 11 | var generatedReport = helpers.executeCode(file, code); 12 | var shortFileName = helpers.shortName(file); 13 | 14 | var expected = expectedCoverage[shortFileName]; 15 | if (options && options["condition"] === false) { 16 | expected.conditions = 0; 17 | expected.conditionsTrue = 0; 18 | expected.conditionsFalse = 0; 19 | expected.conditionsPercentage = 100; 20 | } 21 | if (options && options["function"] === false) { 22 | expected.functions = 0; 23 | expected.functionsCalled = 0; 24 | expected.functionsPercentage = 100; 25 | } 26 | 27 | helpers.assertCoverageEquals(generatedReport.files[file], expected, file, testObject); 28 | helpers.assertCoverageEquals(generatedReport.global, expected, file, testObject); 29 | 30 | waitingFiles -= 1; 31 | if (waitingFiles < 1) { 32 | testObject.done(); 33 | } 34 | }; 35 | 36 | function compareWithOptions (options) { 37 | return function (file, code) { 38 | compare(file, code, options); 39 | }; 40 | }; 41 | 42 | var testObject; 43 | var waitingFiles; 44 | 45 | exports.globalMetrics = function (test) { 46 | test.expect(totalAssertsPerTest); 47 | 48 | testObject = test; 49 | waitingFiles = files.length; 50 | 51 | fileSystem.statFileOrFolder(["test/code/"], "", compare); 52 | }; 53 | 54 | exports.disabledMetrics = function (test) { 55 | test.expect(totalAssertsPerTest); 56 | 57 | testObject = test; 58 | waitingFiles = files.length; 59 | 60 | var options = { 61 | "function" : false, 62 | "condition" : false 63 | }; 64 | 65 | fileSystem.statFileOrFolder(["test/code/"], "", compareWithOptions(options), options); 66 | }; -------------------------------------------------------------------------------- /test/functionNames.js: -------------------------------------------------------------------------------- 1 | var helpers = require("./helpers/utils"); 2 | var fileSystem = require("../lib/fileSystem"); 3 | 4 | exports.fromVariables = function (test) { 5 | test.expect(1); 6 | 7 | fileSystem.statFileOrFolder(["test/names/variables.js"], "", function (file, code) { 8 | var report = helpers.executeCode(file, code); 9 | 10 | var expected = { 11 | "(?)" : 1, 12 | "glob" : 1, 13 | "name" : 1, 14 | "first" : 1, 15 | "second" : 1, 16 | "thisHasAName" : 1, 17 | "nested" : 1, 18 | "inner" : 2 19 | }; 20 | 21 | var functions = Object.keys(report.files["test/names/variables.js"].functions.detail); 22 | var got = helpers.clusterFunctions(functions); 23 | 24 | test.ok(helpers.objectEquals(expected, got), "Functions don't match"); 25 | }); 26 | 27 | test.done(); 28 | }; 29 | 30 | 31 | exports.fromObjects = function (test) { 32 | test.expect(1); 33 | 34 | fileSystem.statFileOrFolder(["test/names/objects.js"], "", function (file, code) { 35 | var report = helpers.executeCode(file, code); 36 | 37 | var expected = { 38 | "withName" : 1, 39 | "first" : 1, 40 | "second" : 1, 41 | "th:ird" : 1, 42 | "assigned" : 1, 43 | "b" : 1 44 | }; 45 | 46 | var functions = Object.keys(report.files["test/names/objects.js"].functions.detail); 47 | var got = helpers.clusterFunctions(functions); 48 | 49 | test.ok(helpers.objectEquals(expected, got), "Functions don't match"); 50 | }); 51 | 52 | test.done(); 53 | }; 54 | 55 | exports.ternary = function (test) { 56 | test.expect(1); 57 | 58 | fileSystem.statFileOrFolder(["test/names/ternary.js"], "", function (file, code) { 59 | var report = helpers.executeCode(file, code); 60 | 61 | var expected = { 62 | "outside" : 2, 63 | "inside" : 1, 64 | "one" : 1, 65 | "property" : 2, 66 | "another" : 1, 67 | "two" : 1 68 | }; 69 | 70 | var functions = Object.keys(report.files["test/names/ternary.js"].functions.detail); 71 | var got = helpers.clusterFunctions(functions); 72 | 73 | test.ok(helpers.objectEquals(expected, got), "Functions don't match"); 74 | }); 75 | 76 | test.done(); 77 | }; -------------------------------------------------------------------------------- /test/interpreters.js: -------------------------------------------------------------------------------- 1 | var fs = require("../lib/fileSystem"); 2 | 3 | function generateExpectedObjectStructure () { 4 | return { 5 | all : false, 6 | js : false, 7 | content : false, 8 | tpl : false 9 | }; 10 | } 11 | 12 | var whatWasCalled = generateExpectedObjectStructure(); 13 | var interpreters = [ 14 | { 15 | filter : { 16 | files : /.*/ 17 | }, 18 | interpret : function () { 19 | whatWasCalled.all = true; 20 | } 21 | }, 22 | { 23 | filter : { 24 | files : /\.js$/ 25 | }, 26 | interpret : function () { 27 | whatWasCalled.js = true; 28 | } 29 | }, 30 | { 31 | filter : { 32 | files : /\.js$/, 33 | content : /myCoolFramework/ 34 | }, 35 | interpret : function () { 36 | whatWasCalled.content = true; 37 | } 38 | }, 39 | { 40 | filter : { 41 | files : /\.tpl$/ 42 | }, 43 | interpret : function () { 44 | whatWasCalled.tpl = true; 45 | } 46 | } 47 | ]; 48 | 49 | exports.sort = function (test) { 50 | test.expect(16); 51 | 52 | fs.getInterpreter("a", "b", interpreters).interpret(); 53 | test.ok(whatWasCalled.all, "all : a"); 54 | test.ok(!whatWasCalled.js, "js : a"); 55 | test.ok(!whatWasCalled.content, "content : a"); 56 | test.ok(!whatWasCalled.tpl, "tpl : a"); 57 | 58 | 59 | whatWasCalled = generateExpectedObjectStructure(); 60 | fs.getInterpreter("file.js", "content", interpreters).interpret(); 61 | test.ok(!whatWasCalled.all, "all : a"); 62 | test.ok(whatWasCalled.js, "js : a"); 63 | test.ok(!whatWasCalled.content, "content : a"); 64 | test.ok(!whatWasCalled.tpl, "tpl : a"); 65 | 66 | whatWasCalled = generateExpectedObjectStructure(); 67 | fs.getInterpreter("file.js", "content contains myCoolFramework", interpreters).interpret(); 68 | test.ok(!whatWasCalled.all, "all : a"); 69 | test.ok(!whatWasCalled.js, "js : a"); 70 | test.ok(whatWasCalled.content, "content : a"); 71 | test.ok(!whatWasCalled.tpl, "tpl : a"); 72 | 73 | whatWasCalled = generateExpectedObjectStructure(); 74 | fs.getInterpreter("file.tpl", "content contains myCoolFramework", interpreters).interpret(); 75 | test.ok(!whatWasCalled.all, "all : a"); 76 | test.ok(!whatWasCalled.js, "js : a"); 77 | test.ok(!whatWasCalled.content, "content : a"); 78 | test.ok(whatWasCalled.tpl, "tpl : a"); 79 | 80 | test.done(); 81 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | node-coverage is licensed under the following MIT license: 2 | 3 | ==== 4 | Copyright (c) 2012 Fabio Crisci 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | ==== 25 | 26 | node-coverage includes as dependencies 27 | 28 | * Optimist - MIT/X11 license 29 | https://github.com/substack/node-optimist 30 | Copyright 2010 James Halliday (mail@substack.net) 31 | * UglifyJS - BSD license 32 | https://github.com/mishoo/UglifyJS 33 | Copyright 2010 (c) Mihai Bazon 34 | Based on parse-js (http://marijn.haverbeke.nl/parse-js/). 35 | * Express - MIT license 36 | https://github.com/visionmedia/express 37 | Copyright (c) 2009-2011 TJ Holowaychuk 38 | * Jade - MIT license 39 | https://github.com/visionmedia/jade 40 | Copyright (c) 2009-2010 TJ Holowaychuk 41 | * mkdirp - MIT/X11 license 42 | https://github.com/substack/node-mkdirp 43 | Copyright 2010 James Halliday (mail@substack.net) 44 | * Nodeunit - MIT license 45 | https://github.com/caolan/nodeunit 46 | Copyright (c) 2010 Caolan McMahon 47 | * jQuery - MIT license 48 | http://jquery.com/ 49 | Copyright (c) 2011 John Resig, http://jquery.com/ 50 | * Highcarts - Creative Commons Attribution-NonCommercial 3.0 License. 51 | http://www.highcharts.com/ 52 | Copyright (c) 2011 Highsoft -------------------------------------------------------------------------------- /test/instrumentFiles.js: -------------------------------------------------------------------------------- 1 | var fileSystem = require("../lib/fileSystem"); 2 | var path = require("path"); 3 | var clientCode = require("../lib/clientCode"); 4 | 5 | function createInstrumentCallback (container) { 6 | return function (file, code) { 7 | var isJs = (path.extname(file) === ".js"); 8 | 9 | container[file] = isJs ? { 10 | isJs : true, 11 | hasStatement : code.indexOf("$$_l(") > 0, 12 | hasConditions : code.indexOf("$$_c(") > 0, 13 | hasFunctions : code.indexOf("$$_f(") > 0, 14 | hasHeader : clientCode.isInstrumented(code) 15 | } : { 16 | isJs : false 17 | }; 18 | }; 19 | } 20 | 21 | exports.exclude = function (test) { 22 | var options = { 23 | exclude : [".git", "node_modules", "test/instrumentFiles.js", "views/statics"] 24 | }; 25 | 26 | var instrumented = {}; 27 | 28 | fileSystem.statFileOrFolder(["."], "", createInstrumentCallback(instrumented), options); 29 | 30 | var allFiles = Object.keys(instrumented); 31 | 32 | test.expect(Math.max(allFiles.length * 3, 20)); 33 | 34 | allFiles.forEach(function (fileName) { 35 | test.equals(fileName.indexOf(".git"), -1, "Found a file in exlude folder " + fileName); 36 | test.equals(fileName.indexOf("node_modules"), -1, "Found a file in exlude folder " + fileName); 37 | test.equals(fileName.indexOf("views/statics"), -1, "Found a file in exlude folder " + fileName); 38 | 39 | if (fileName.indexOf("instrumentFiles") > 0) { 40 | test.ok(false, "instrumentFiles was included"); 41 | } 42 | }); 43 | 44 | test.done(); 45 | }; 46 | 47 | exports.ignore = function (test) { 48 | var options = { 49 | exclude : [".git", "node_modules", "views/statics"], 50 | ignore : ["test/code"] 51 | }; 52 | 53 | var instrumented = {}; 54 | 55 | fileSystem.statFileOrFolder(["."], "", createInstrumentCallback(instrumented), options); 56 | 57 | var allFiles = Object.keys(instrumented); 58 | 59 | test.expect(Math.max(allFiles.length * 4, 20)); 60 | 61 | allFiles.forEach(function (fileName) { 62 | test.equals(fileName.indexOf(".git"), -1, "Found a file in exlude folder " + fileName); 63 | test.equals(fileName.indexOf("node_modules"), -1, "Found a file in exlude folder " + fileName); 64 | test.equals(fileName.indexOf("views/statics"), -1, "Found a file in exlude folder " + fileName); 65 | 66 | var wasIgnoredIfNeeded = true; 67 | var descriptor = instrumented[fileName]; 68 | if (descriptor.isJs && fileName.indexOf("test/code") === 0) { 69 | if (!descriptor.hasHeader) { 70 | // no header : will be instrumented by the server 71 | wasIgnoredIfNeeded = false; 72 | } 73 | 74 | if (descriptor.hasStatement) { 75 | // has instrumentation code 76 | wasIgnoredIfNeeded = false; 77 | } 78 | } 79 | 80 | test.ok(wasIgnoredIfNeeded, "File was not ignored " + fileName); 81 | }); 82 | 83 | test.done(); 84 | }; -------------------------------------------------------------------------------- /lib/server/instrumentation.js: -------------------------------------------------------------------------------- 1 | var express = require("express"), fs = require("fs"); 2 | 3 | var instrument = require("../instrument"); 4 | var common = require("./common"); 5 | 6 | exports.start = function (docRoot, port, adminRoot, coverageOptions, onClose, initialStaticInfo) { 7 | var sourceCodeCache = { 8 | code : {}, 9 | staticInfo : initialStaticInfo || {} 10 | }; 11 | 12 | var app = express.createServer(); 13 | 14 | app.use(express.bodyParser()); 15 | 16 | app.get("/*.js", function (req, res) { 17 | var url = req.path; 18 | var instrumentedCode = sourceCodeCache[url]; 19 | 20 | if (coverageOptions.verbose) { 21 | console.log("Requesting", url); 22 | } 23 | 24 | var headers = { 25 | "Content-Type": "text/javascript", 26 | "Connection": "close" 27 | }; 28 | 29 | if (instrumentedCode) { 30 | res.send(instrumentedCode.clientCode, headers); 31 | } else { 32 | fs.readFile(docRoot + url, "utf-8", function (err, content) { 33 | if (err) { 34 | res.send("Error while reading " + url + err, 500); 35 | } else { 36 | if (coverageOptions.verbose) { 37 | console.log("Instrumenting", docRoot + url); 38 | } 39 | var code = instrument(url, content, coverageOptions); 40 | sourceCodeCache.code[url] = code.clientCode; 41 | 42 | if (code.staticInfo && coverageOptions.staticInfo === false) { 43 | sourceCodeCache.staticInfo[url] = code.staticInfo; 44 | } 45 | 46 | res.send(code.clientCode, headers); 47 | } 48 | }); 49 | } 50 | }); 51 | 52 | var whileStoringCoverage = 0; 53 | app.post("/node-coverage-store", function (req, res) { 54 | whileStoringCoverage += 1; 55 | if (req.headers["content-type"] === "application/x-www-form-urlencoded") { 56 | // Using a real form submit the coverage reports happens to be a string 57 | req.body = JSON.parse(req.body.coverage); 58 | } 59 | 60 | common.saveCoverage(req.body, sourceCodeCache.staticInfo, adminRoot, function (error) { 61 | whileStoringCoverage -= 1; 62 | if (error) { 63 | res.send(error, 500); 64 | } else { 65 | res.send(200, coverageOptions["exit-on-submit"] ? {"Connection": "close"} : null); 66 | } 67 | 68 | if (coverageOptions["exit-on-submit"]) { 69 | process.nextTick(function () { 70 | server.close(onClose); 71 | }); 72 | } 73 | }); 74 | }); 75 | 76 | app.all("/node-coverage-please-exit", function (req, res) { 77 | function exitWhenDone () { 78 | if (whileStoringCoverage) { 79 | setTimeout(exitWhenDone, 100); 80 | } else { 81 | res.send(200, {"Connection": "close"}); 82 | process.nextTick(function () { 83 | server.close(onClose); 84 | }); 85 | } 86 | } 87 | exitWhenDone(); 88 | }); 89 | 90 | app.post("/*", function (req, res, next) { 91 | res.sendfile(docRoot + req.path); 92 | }); 93 | 94 | app.use(express.static(docRoot)); 95 | 96 | var server = app.listen(port); 97 | return server; 98 | }; 99 | -------------------------------------------------------------------------------- /instrument.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fileSystem = require("./lib/fileSystem"); 3 | var fs = require("fs"); 4 | var argv = require("optimist") 5 | .usage("Instrument a folder for code coverage.\n$0 source destination") 6 | .boolean("h").alias("h", "help") 7 | .boolean("function") 8 | .default("function", false) 9 | .describe("function", "Enable function coverage. Disable with --no-function") 10 | .boolean("condition") 11 | .default("condition", true) 12 | .describe("condition", "Enable condition coverage. Disable with --no-condition") 13 | .boolean("submit") 14 | .default("submit", true) 15 | .describe("submit", "Include submit code in instrumented file. Disable with --no-submit") 16 | .options("s", { 17 | "alias" : "static-info" 18 | }).describe("s", "Path to a JSON output file which will contain static information about instrumented files. Using this option reduces the size of instrumented files.") 19 | .options("x", { 20 | "alias" : "exclude" 21 | }).describe("x", "Exclude file or folder. This file/folder won't be copied in target folder. Path relative to the source folder") 22 | .options("i", { 23 | "alias" : "ignore" 24 | }).describe("i", "Ignore file or folder. This file/folder is copied in target folder but not instrumented. Path relative to the source folder") 25 | .boolean("v").alias("v", "verbose").default("v", false) 26 | .argv; 27 | 28 | 29 | 30 | switch (argv._.length) { 31 | case 1: 32 | instrumentFile(argv._[0], argv); 33 | break; 34 | case 2: 35 | instrumentFolder(argv._[0], argv._[1], argv); 36 | break; 37 | default: 38 | displayHelp(); 39 | break; 40 | } 41 | 42 | function displayHelp () { 43 | require("optimist").showHelp(); 44 | }; 45 | 46 | function instrumentFolder (source, destination, options) { 47 | try { 48 | var callback = fileSystem.writeFileTo(source, destination); 49 | var staticInfoFile = options["static-info"]; 50 | var staticInfo = staticInfoFile ? {} : null; 51 | 52 | fileSystem.statFileOrFolder([source], "", callback, { 53 | "function" : options["function"], 54 | "condition" : options["condition"], 55 | "submit" : options["submit"], 56 | "staticInfo": !staticInfo, 57 | "exclude" : options.exclude, 58 | "ignore" : options.ignore, 59 | "verbose" : options.verbose 60 | }, staticInfo); 61 | 62 | if (staticInfo) { 63 | fs.writeFileSync(staticInfoFile, JSON.stringify(staticInfo)); 64 | } 65 | } catch (ex) { 66 | require("optimist").showHelp(); 67 | return console.error(ex); 68 | } 69 | }; 70 | 71 | function instrumentFile (fileName, options) { 72 | var callback = function (file, code) { 73 | console.log(code); 74 | }; 75 | 76 | fileSystem.statFileOrFolder([fileName], "", callback, { 77 | "function" : options["function"], 78 | "condition" : options["condition"], 79 | "submit" : options["submit"], 80 | "staticInfo": !options["static-info"], 81 | "exclude" : options.exclude, 82 | "ignore" : options.ignore, 83 | "verbose" : options.verbose 84 | }); 85 | }; -------------------------------------------------------------------------------- /views/report.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html(lang="en") 3 | head 4 | title node-coverage Admin Interface 5 | link(rel="stylesheet", href="#{serverRoot}/style.css") 6 | body 7 | h3.header node-coverage 8 | em=name 9 | .summary 10 | h2 Statement coverage: 11 | h1 #{report.global.statements.percentage.toFixed(2)}% 12 | .summary 13 | h2 Condition coverage: 14 | h1 #{report.global.conditions.percentage.toFixed(2)}% 15 | .summary 16 | h2 Function coverage: 17 | h1 #{report.global.functions.percentage.toFixed(2)}% 18 | .clear 19 | 20 | .content 21 | table 22 | thead 23 | tr.partial 24 | th 25 | th(colspan=3).flap 26 | a(href="#{serverRoot}/stat/#{encodeURIComponent(name)}") Stats & Graphs 27 | tr 28 | -var isSorted = sort.what=="file" 29 | th(class=isSorted ? "sorted" : "") 30 | a(href="#{serverRoot}/r/#{encodeURIComponent(name)}/sort/file/"+(sort.how == "asc" || !isSorted ? "desc" : "asc")) File 31 | -if (isSorted) 32 | span.sort-direction=sort.how == "asc" ? "↑" : "↓" 33 | 34 | -var isSorted = sort.what=="statement" 35 | th(class=isSorted ? "sorted" : "") 36 | a(href="#{serverRoot}/r/#{encodeURIComponent(name)}/sort/statement/"+(sort.how == "asc" || !isSorted ? "desc" : "asc")) Statement 37 | -if (isSorted) 38 | span.sort-direction=sort.how == "asc" ? "↑" : "↓" 39 | 40 | -var isSorted = sort.what=="condition" 41 | th(class=isSorted ? "sorted" : "") 42 | a(href="#{serverRoot}/r/#{encodeURIComponent(name)}/sort/condition/"+(sort.how == "asc" || !isSorted ? "desc" : "asc")) Condition 43 | -if (isSorted) 44 | span.sort-direction=sort.how == "asc" ? "↑" : "↓" 45 | 46 | -var isSorted = sort.what=="function" 47 | th(class=isSorted ? "sorted" : "") 48 | a(href="#{serverRoot}/r/#{encodeURIComponent(name)}/sort/function/"+(sort.how == "asc" || !isSorted ? "desc" : "asc")) Function 49 | -if (isSorted) 50 | span.sort-direction=sort.how == "asc" ? "↑" : "↓" 51 | tbody 52 | each single in report.files 53 | tr 54 | td 55 | a(href="#{serverRoot}/r/#{encodeURIComponent(name)}/file/#{encodeURIComponent(single.file)}") #{single.file} 56 | 57 | -var percentage = single.report.statements.percentage 58 | if percentage < 30 59 | td.low #{percentage.toFixed(2)}% 60 | else if percentage > 80 61 | td.high #{percentage.toFixed(2)}% 62 | else 63 | td.notbad #{percentage.toFixed(2)}% 64 | 65 | -var percentage = single.report.conditions.percentage 66 | if percentage < 30 67 | td.low #{percentage.toFixed(2)}% 68 | else if percentage > 80 69 | td.high #{percentage.toFixed(2)}% 70 | else 71 | td.notbad #{percentage.toFixed(2)}% 72 | 73 | -var percentage = single.report.functions.percentage 74 | if percentage < 30 75 | td.low #{percentage.toFixed(2)}% 76 | else if percentage > 80 77 | td.high #{percentage.toFixed(2)}% 78 | else 79 | td.notbad #{percentage.toFixed(2)}% 80 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require("fs"); 3 | var argv = require("optimist") 4 | .usage("Start a simple web server to instrument JS code.") 5 | .options("d", { 6 | "alias" : "doc-root", 7 | "default" : "/var/www" 8 | }).describe("d", "Document Root. Content from this path will be served by the server") 9 | .options("r", { 10 | "alias" : "report-dir", 11 | "default" : "/var/log/node-coverage" 12 | }).describe("r", "Directory where reports are stored.") 13 | .options("p", { 14 | "alias" : "port", 15 | "default" : 8080 16 | }).describe("p", "Web server port") 17 | .options("a", { 18 | "alias" : "admin-port", 19 | "default" : 8787 20 | }).describe("a", "Admin server port") 21 | .boolean("h").alias("h", "help") 22 | .boolean("function") 23 | .default("function", false) 24 | .describe("function", "Enable function coverage. Disable with --no-function") 25 | .boolean("condition") 26 | .default("condition", true) 27 | .describe("condition", "Enable condition coverage. Disable with --no-condition") 28 | .boolean("session") 29 | .default("session", true) 30 | .describe("session", "Store instrumented code in session storage. This reduces the burden on browsers. Disable with --no-session") 31 | .options("s", { 32 | alias: "static-info" 33 | }).describe("s", "In case files are pre-instrumented, path to the JSON file containing static information about instrumented files.") 34 | .options("i", { 35 | "alias" : "ignore" 36 | }).describe("i", "Ignore file or folder. This file/folder won't be instrumented. Path relative to document root") 37 | .boolean("proxy") 38 | .default("proxy", false) 39 | .describe("proxy", "Start the instrumentation server in HTTP proxy mode on port specified by -p.") 40 | .boolean("v").alias("v", "verbose").default("v", false) 41 | .boolean("exit-on-submit") 42 | .default("exit-on-submit", false) 43 | .describe("exit-on-submit", "Close the server and exit the process after a coverage report is submitted.") 44 | .argv; 45 | 46 | 47 | var admin_server; 48 | if (argv.h) { 49 | require("optimist").showHelp(); 50 | } else { 51 | try { 52 | var stat = fs.statSync(argv.r); 53 | if (!stat.isDirectory()) { 54 | throw new Error(argv.r + " is not a directory"); 55 | } 56 | 57 | var ignore = argv.i || []; 58 | if (!ignore.forEach) { 59 | ignore = [ignore]; 60 | } 61 | 62 | /* Instrumentation server */ 63 | var config; 64 | if (argv.proxy) { 65 | config = { 66 | "function" : argv["function"], 67 | "condition" : argv.condition, 68 | "staticInfo" : true, 69 | "verbose" : argv.v 70 | }; 71 | require("./lib/server/proxy").start(argv.p, argv.r, config); 72 | 73 | console.log("Starting proxy server on port ", argv.p); 74 | } else { 75 | config = { 76 | "function" : argv["function"], 77 | "condition" : argv.condition, 78 | "staticInfo" : !argv.session, 79 | "ignore" : ignore.map(function (path) { 80 | if (path.charAt(0) !== "/") { 81 | return "/" + path; 82 | } else { 83 | return path; 84 | } 85 | }), 86 | "verbose" : argv.v, 87 | "exit-on-submit" : argv["exit-on-submit"] 88 | }; 89 | var initialStaticInfo = null; 90 | var staticInfoFile = argv["static-info"]; 91 | if (staticInfoFile) { 92 | initialStaticInfo = JSON.parse(fs.readFileSync(staticInfoFile, "utf8")); 93 | } 94 | require("./lib/server/instrumentation").start(argv.d, argv.p, argv.r, config, onClose, initialStaticInfo); 95 | 96 | console.log("Starting server on port", argv.p); 97 | } 98 | 99 | /* Admin server */ 100 | admin_server = require("./lib/server/administration").start(argv.d, argv.p, argv.r, argv.a); 101 | console.log("Starting administration interface on port", argv.a); 102 | 103 | if (argv.v) { 104 | console.log("Server configuration", config); 105 | } 106 | } catch (ex) { 107 | console.error("Please specify a valid report directory", ex); 108 | } 109 | } 110 | 111 | // Called when the instrumentation server closes 112 | function onClose () { 113 | admin_server.close(); 114 | } -------------------------------------------------------------------------------- /lib/fileSystem.js: -------------------------------------------------------------------------------- 1 | var instrument = require("./instrument"); 2 | var clientCode = require("./clientCode"); 3 | var fs = require("fs"); 4 | var path = require("path"); 5 | var mkdirp = require("mkdirp"); 6 | 7 | var interpreters = []; 8 | var subModules = fs.readdirSync(__dirname + "/interpreters"); 9 | subModules.forEach(function (moduleName) { 10 | var module = require("./interpreters/" + moduleName); 11 | 12 | if (!module.filter || !module.interpret) { 13 | console.error("Invalid module definition, " + moduleName); 14 | } else { 15 | interpreters.push(module); 16 | } 17 | }); 18 | 19 | /* These functions are sync to avoid too many opened files */ 20 | function statFileOrFolder (fileOrFolder, basePath, callback, options, staticInfo) { 21 | var toBeExluded = options ? options.exclude : null; 22 | 23 | fileOrFolder.forEach(function (item) { 24 | var fileName = path.join(basePath, item); 25 | var stat = fs.statSync(fileName); 26 | 27 | if (!clientCode.shouldBeExcluded(osIndependentFileName(fileName), toBeExluded)) { 28 | if (stat.isFile()) { 29 | instrumentFile(item, basePath, callback, options, staticInfo); 30 | } else if (stat.isDirectory()) { 31 | instrumentFolder(item, basePath, callback, options, staticInfo); 32 | } else { 33 | console.error("Unable to instrument, neither a file nor a folder.", item, stats); 34 | } 35 | } 36 | }); 37 | } 38 | 39 | function instrumentFile (file, basePath, callback, options, staticInfo) { 40 | var fileName = path.join(basePath, file); 41 | var osIndependentName = osIndependentFileName(fileName); 42 | 43 | var encoding = "utf-8"; 44 | var content = fs.readFileSync(fileName, encoding); 45 | 46 | var instrumentationResult = instrument(osIndependentName, content, options); 47 | if (staticInfo && instrumentationResult.staticInfo) { 48 | staticInfo[osIndependentName] = instrumentationResult.staticInfo; 49 | } 50 | var instructed = instrumentationResult.clientCode; 51 | if (instructed === content) { 52 | instructed = fs.readFileSync(fileName); 53 | encoding = null; 54 | } 55 | 56 | if (callback) { 57 | callback.call(null, osIndependentName, instructed, encoding); 58 | } 59 | } 60 | 61 | function instrumentFolder (folder, basePath, callback, options, staticInfo) { 62 | var folderPath = path.join(basePath, folder); 63 | var files = fs.readdirSync(folderPath); 64 | 65 | statFileOrFolder(files, folderPath, callback, options, staticInfo); 66 | } 67 | 68 | function writeFileTo (src, dest) { 69 | var destinationRoot = path.resolve(dest); 70 | if (fs.existsSync(destinationRoot)) { 71 | throw new Error(destinationRoot + " exists already"); 72 | } 73 | 74 | fs.mkdirSync(destinationRoot); 75 | 76 | return function (file, code, encoding) { 77 | var relative = path.relative(src, file); 78 | var fileName = path.resolve(destinationRoot, relative); 79 | var dirName = path.dirname(fileName); 80 | 81 | mkdirp.sync(dirName, 0777); 82 | 83 | fs.writeFileSync(fileName, code, encoding); 84 | fs.chmodSync(fileName, 0777); 85 | }; 86 | } 87 | 88 | function getInterpreter (file, content, override) { 89 | var chooseFrom = override || interpreters; 90 | var specificify = 0, bestModule = null; 91 | 92 | chooseFrom.forEach(function (module) { 93 | var value = 0; 94 | 95 | if (matchFile(module.filter, file)) { 96 | value += 1; 97 | 98 | if (matchContent(module.filter, content)) { 99 | value += 2; 100 | } 101 | } 102 | 103 | if (value > specificify) { 104 | bestModule = module; 105 | specificify = value; 106 | } else if (value > 0 && value === specificify) { 107 | if (module.filter.files.toString().length > bestModule.filter.files.toString().length) { 108 | bestModule = module; 109 | } 110 | } 111 | }); 112 | 113 | return bestModule; 114 | } 115 | 116 | function matchFile (filter, file) { 117 | if (!filter || !filter.files || !(filter.files instanceof RegExp)) { 118 | return false; 119 | } 120 | 121 | return filter.files.test(file); 122 | } 123 | 124 | function matchContent (filter, content) { 125 | if (!filter || !filter.content || !(filter.content instanceof RegExp)) { 126 | return false; 127 | } 128 | 129 | return filter.content.test(content); 130 | } 131 | 132 | function osIndependentFileName (fileName) { 133 | return fileName.split(path.sep).join("/"); 134 | } 135 | 136 | 137 | exports.statFileOrFolder = statFileOrFolder; 138 | exports.instrumentFile = instrumentFile; 139 | exports.instrumentFolder = instrumentFolder; 140 | exports.writeFileTo = writeFileTo; 141 | exports.interpreters = interpreters; 142 | exports.getInterpreter = getInterpreter; -------------------------------------------------------------------------------- /test/mergeReports.js: -------------------------------------------------------------------------------- 1 | var helpers = require("./helpers/utils"); 2 | var report = require("../lib/report"); 3 | var fileSystem = require("../lib/fileSystem"); 4 | 5 | var expectedCoverage = require("./results/reports").results; 6 | var expectedDetails = require("./results/details").results; 7 | // count the number of assert expected (for each test) 8 | var files = Object.keys(expectedCoverage); 9 | var asserts = Object.keys(expectedCoverage[files[0]]); 10 | // This test will call assertCoverageEquals 11 | // twice for every file 12 | // three times for the merge 1_2 13 | // twice for every file merged with itself 14 | // 15 | // and assertDetailsEquals (9 asserts) 16 | // twice merging 1 and 2 17 | // once for every file merged with itself 18 | var totalAssertsPerTest = (15 * asserts.length) + (5 * 9); 19 | 20 | function measureCoverage (file, code) { 21 | var generatedReport = helpers.executeCode(file, code); 22 | var shortFileName = helpers.shortName(file); 23 | 24 | var expected = expectedCoverage[shortFileName]; 25 | generatedReports[shortFileName] = generatedReport; 26 | 27 | helpers.assertCoverageEquals(generatedReport.files[file], expected, file, testObject); 28 | helpers.assertCoverageEquals(generatedReport.global, expected, file, testObject); 29 | 30 | waitingFiles -= 1; 31 | if (waitingFiles < 1) { 32 | assertMerge(); 33 | testObject.done(); 34 | } 35 | }; 36 | 37 | function assertMerge () { 38 | merge_1_and_2(testObject); 39 | 40 | merge_with_itself("test/reports/file1.js", testObject); 41 | 42 | merge_with_itself("test/reports/file2.js", testObject); 43 | 44 | merge_with_itself("test/reports/file3.js", testObject); 45 | }; 46 | 47 | function merge_1_and_2 (testObject) { 48 | var merged = report.mergeReports([ 49 | generatedReports["file1.js"], generatedReports["file2.js"] 50 | ]); 51 | 52 | // File reports shouldn't change 53 | for (var fileName in merged.files) { 54 | var shortFileName = helpers.shortName(fileName); 55 | 56 | helpers.assertCoverageEquals(merged.files[fileName], expectedCoverage[shortFileName], shortFileName, testObject); 57 | 58 | helpers.assertDetailsEquals(merged.files[fileName], expectedDetails.merge[shortFileName], shortFileName, testObject); 59 | } 60 | helpers.assertCoverageEquals(merged.global, expectedCoverage["merge_1_2"], "merge_1_2", testObject); 61 | }; 62 | 63 | function merge_with_itself (fileName, testObject) { 64 | var shortFileName = helpers.shortName(fileName); 65 | var merged = report.mergeReports([ 66 | generatedReports[shortFileName], generatedReports[shortFileName] 67 | ]); 68 | 69 | helpers.assertCoverageEquals(merged.files[fileName], expectedCoverage[shortFileName], "merge_" + shortFileName, testObject); 70 | helpers.assertCoverageEquals(merged.global, expectedCoverage[shortFileName], "global_" + shortFileName, testObject); 71 | 72 | helpers.assertDetailsEquals(merged.files[fileName], expectedDetails.mergeSelf[shortFileName], "merge_" + shortFileName, testObject); 73 | }; 74 | 75 | 76 | function mergeSpecial (file, code) { 77 | // Run file3 with different global variables -> different paths 78 | var generatedReportTrue = helpers.executeCode(file, code, { 79 | thisIsAGlobalVariable : false 80 | }); 81 | var generatedReportFalse = helpers.executeCode(file, code, { 82 | thisIsAGlobalVariable : true 83 | }); 84 | 85 | var specialCoverage = expectedDetails.mergeSpecial.coverage; 86 | var specialDetails = expectedDetails.mergeSpecial.details; 87 | 88 | var merged = report.mergeReports([ 89 | generatedReportTrue, generatedReportFalse 90 | ]); 91 | 92 | helpers.assertCoverageEquals(merged.files[file], specialCoverage, "special", testObject); 93 | helpers.assertCoverageEquals(merged.global, specialCoverage, "global special", testObject); 94 | 95 | helpers.assertDetailsEquals(merged.files[file], specialDetails, "details special", testObject); 96 | 97 | testObject.done(); 98 | }; 99 | 100 | var testObject; 101 | var waitingFiles = 3; 102 | var generatedReports = {}; 103 | 104 | exports.mergeResults = function (test) { 105 | test.expect(totalAssertsPerTest); 106 | 107 | testObject = test; 108 | 109 | fileSystem.instrumentFolder("test/reports", "", measureCoverage, { 110 | "function" : true, 111 | "condition" : true 112 | }); 113 | }; 114 | 115 | exports.differentGlobals = function (test) { 116 | // this special test does 2 assertCoverageEquals and 1 assertDetailsEquals 117 | test.expect(2 * asserts.length + 9); 118 | 119 | testObject = test; 120 | 121 | fileSystem.instrumentFile("test/reports/file3.js", "", mergeSpecial, { 122 | "function" : true, 123 | "condition" : true 124 | }); 125 | }; -------------------------------------------------------------------------------- /test/helpers/utils.js: -------------------------------------------------------------------------------- 1 | var vm = require("vm"); 2 | var report = require("../../lib/report"); 3 | var path = require("path"); 4 | 5 | exports.executeCode = function (file, code, globals) { 6 | var serialized; 7 | var sandbox = { 8 | XMLHttpRequest : function () { 9 | this.open = function () {}; 10 | this.setRequestHeader = function () {}; 11 | this.send = function (data) { 12 | serialized = data; 13 | }; 14 | }, 15 | window : globals || {} 16 | }; 17 | vm.runInNewContext(code, sandbox, file); 18 | sandbox.$$_l.submit(); 19 | 20 | var json = JSON.parse(serialized); 21 | 22 | return report.generateAll(json); 23 | }; 24 | 25 | exports.shortName = function (fileName) { 26 | return path.basename(fileName); 27 | }; 28 | 29 | exports.assertCoverageEquals = function (measured, expected, file, testObject) { 30 | 31 | var statementCoverage = measured.statements; 32 | testObject.equal(statementCoverage.total, expected.total, "total statements " + file); 33 | testObject.equal(statementCoverage.covered, expected.visited, "covered statements " + file); 34 | // being float we compare to 1E-5 35 | testObject.equal(statementCoverage.percentage.toFixed(5), 36 | expected.statementsPercentage.toFixed(5), "percentage statements " + file); 37 | 38 | var conditionCoverage = measured.conditions; 39 | testObject.equal(conditionCoverage.total, expected.conditions, "conditions " + file); 40 | testObject.equal(conditionCoverage.coveredTrue, expected.conditionsTrue, "conditionsTrue " + file); 41 | testObject.equal(conditionCoverage.coveredFalse, expected.conditionsFalse, "conditionsFalse " + file); 42 | testObject.equal(conditionCoverage.percentage.toFixed(5), 43 | expected.conditionsPercentage.toFixed(5), "percentage conditions " + file); 44 | 45 | var functionCoverage = measured.functions; 46 | testObject.equal(functionCoverage.total, expected.functions, "functions " + file); 47 | testObject.equal(functionCoverage.covered, expected.functionsCalled, "functionsCalled " + file); 48 | testObject.equal(functionCoverage.percentage.toFixed(5), 49 | expected.functionsPercentage.toFixed(5), "percentage functions " + file); 50 | }; 51 | 52 | exports.assertDetailsEquals = function (measured, expected, file, testObject) { 53 | var statementsDetails = measured.statements.detail; 54 | 55 | var totalExecutions = 0, howManyLines = 0; 56 | for (var lineId in statementsDetails) { 57 | howManyLines += 1; 58 | totalExecutions += statementsDetails[lineId]; 59 | } 60 | 61 | testObject.equal(howManyLines, expected.statements.number, "number of statements detail " + file); 62 | testObject.equal(totalExecutions, expected.statements.total, "total statements detail " + file); 63 | 64 | var conditionsDetails = measured.conditions.detail; 65 | ["true", "false"].forEach(function (condType) { 66 | testObject.equal( 67 | conditionsDetails[condType].length, 68 | expected.conditions[condType].number, 69 | "number of conditions detail " + condType + " " + file 70 | ); 71 | }); 72 | 73 | var totalConditions = 0, totalTrue = 0, totalFalse = 0; 74 | for (var condId in conditionsDetails.all) { 75 | totalConditions += 1; 76 | totalTrue += conditionsDetails.all[condId]["true"]; 77 | totalFalse += conditionsDetails.all[condId]["false"]; 78 | } 79 | testObject.equal(totalConditions, expected.conditions.all, "all conditions detail " + file); 80 | testObject.equal(totalTrue, expected.conditions["true"].total, "total true conditions detail " + file); 81 | testObject.equal(totalFalse, expected.conditions["false"].total, "total false conditions detail " + file); 82 | 83 | var functionsDetails = measured.functions.detail; 84 | var totalFunctions = 0, howManyFunctions = 0; 85 | for (var fnId in functionsDetails) { 86 | howManyFunctions += 1; 87 | totalFunctions += functionsDetails[fnId]; 88 | } 89 | 90 | testObject.equal(howManyFunctions, expected.functions.number, "number of functions detail " + file); 91 | testObject.equal(totalFunctions, expected.functions.total, "total functions detail " + file); 92 | }; 93 | 94 | exports.clusterFunctions = function (functions) { 95 | var map = {}; 96 | functions.forEach(function (item) { 97 | var match = /(\D+)_\d+_\d+$/.exec(item); 98 | var name = match[1]; 99 | 100 | if (!map[name]) { 101 | map[name] = 0; 102 | } 103 | 104 | map[name] += 1; 105 | }); 106 | 107 | return map; 108 | }; 109 | 110 | exports.objectEquals = function (compare, expected) { 111 | for (var key in compare) { 112 | if (expected[key] !== compare[key]) { 113 | return false; 114 | } 115 | } 116 | 117 | for (var key in expected) { 118 | if (compare[key] !== expected[key]) { 119 | return false; 120 | } 121 | } 122 | 123 | return true; 124 | }; -------------------------------------------------------------------------------- /views/statics/style.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | 51 | 52 | body { 53 | background-color: #E5E5E5; 54 | font-family: 'TurkishCSs', "proxima-nova-1","proxima-nova-2", Helmet, Freesans, sans-serif !important; 55 | } 56 | 57 | h1, 58 | h2, 59 | h3, 60 | h4, 61 | h5, 62 | h6 { 63 | font-weight: bold; 64 | color: #404040; 65 | text-rendering: optimizelegibility; 66 | } 67 | h1 { 68 | font-size: 90px; 69 | line-height: 78px; 70 | } 71 | h2 { 72 | font-size: 24px; 73 | line-height: 36px; 74 | letter-spacing: 2px; 75 | word-spacing : 4px; 76 | } 77 | h3 { 78 | line-height: 27px; 79 | font-size: 18px; 80 | } 81 | h4 { 82 | font-size: 16px; 83 | line-height: 36px; 84 | } 85 | h5 { 86 | font-size: 14px; 87 | line-height: 18px; 88 | } 89 | em { 90 | font-size : 90%; 91 | color : #999; 92 | font-weight : bold; 93 | margin-left : 1em; 94 | } 95 | 96 | .header { 97 | height : 40px; 98 | background-color : #2A2A2A; 99 | padding: 10px 0px 0px 40px; 100 | color : white; 101 | } 102 | .content { 103 | padding : 15px; 104 | } 105 | .summary { 106 | padding: 10px 50px; 107 | float : left; 108 | letter-spacing : -4px; 109 | } 110 | .clear { 111 | clear: both; 112 | } 113 | 114 | .high { 115 | color : #06724C; 116 | font-weight: bold; 117 | } 118 | .low { 119 | color : #C30604; 120 | } 121 | 122 | table { 123 | -webkit-border-radius: 4px; 124 | -moz-border-radius: 4px; 125 | border-radius: 4px; 126 | width: 100%; 127 | max-width: 100%; 128 | } 129 | 130 | thead tr { 131 | border: 1px solid #CCC; 132 | } 133 | thead tr.partial { 134 | border: 0; 135 | } 136 | thead tr.partial th:first-child { 137 | background-color: transparent; 138 | border: 0; 139 | } 140 | th { 141 | padding: 8px; 142 | line-height: 24px; 143 | text-align: left; 144 | border-top: 1px solid #CCC; 145 | background-color: #F1F1F1; 146 | } 147 | th:first-child { 148 | text-align: center; 149 | } 150 | th.flap { 151 | border: 1px solid #CCC; 152 | border-top-left-radius: 8px; 153 | border-top-right-radius: 8px; 154 | text-align: center; 155 | } 156 | 157 | table tbody tr { 158 | border: 1px solid #CCC; 159 | border-collapse: separate; 160 | } 161 | 162 | td { 163 | padding: 8px; 164 | line-height: 18px; 165 | text-align: left; 166 | border-top: 1px solid #CCC; 167 | border-right: 1px solid #CCC; 168 | } 169 | 170 | .reports th { 171 | text-align: center; 172 | } 173 | th a, th a:visited { 174 | color: black; 175 | } 176 | tr:nth-child(odd) td, tr:nth-child(odd) th { 177 | background-color: #F1F1F1; 178 | } 179 | .code { 180 | background-color: #F1F1F1; 181 | border: 0; 182 | font-family: 'Inconsolata', sans-serif; 183 | white-space: pre-wrap; 184 | white-space: -moz-pre-wrap !important; 185 | white-space: -pre-wrap; 186 | white-space: -o-pre-wrap; 187 | word-wrap: break-word; 188 | } 189 | tr.not-covered td { 190 | background-color: #CFB7B9; 191 | } 192 | tr.partially-covered td { 193 | background-color: #F8EDD1; 194 | } 195 | .small td { 196 | line-height: 14px; 197 | } 198 | .small td:nth-child(2), .small th:nth-child(2), 199 | .small td:nth-child(3), .small th:nth-child(3), 200 | .small td:nth-child(4), .small th:nth-child(4) { 201 | font-size: 75%; 202 | } 203 | .legend { 204 | font-size: 75%; 205 | float: right; 206 | margin: 15px 25px 0 0; 207 | } 208 | dt { 209 | font-weight: bold; 210 | } 211 | dd { 212 | padding-left: 12px; 213 | } 214 | .sort-direction { 215 | font-size : 95%; 216 | } 217 | .sorted { 218 | font-weight: bold; 219 | } 220 | .merge { 221 | width : 3em; 222 | } 223 | 224 | .chart { 225 | margin-bottom : 30px; 226 | } -------------------------------------------------------------------------------- /test/results/code.js: -------------------------------------------------------------------------------- 1 | exports.results = { 2 | "base.js" : { 3 | total : 3, 4 | visited : 3, 5 | statementsPercentage : 100, 6 | conditions : 0, 7 | conditionsTrue : 0, 8 | conditionsFalse : 0, 9 | conditionsPercentage : 100, 10 | functions : 0, 11 | functionsCalled : 0, 12 | functionsPercentage : 100 13 | }, 14 | "longlines.js" : { 15 | total : 1, 16 | visited : 1, 17 | statementsPercentage : 100, 18 | conditions : 0, 19 | conditionsTrue : 0, 20 | conditionsFalse : 0, 21 | conditionsPercentage : 100, 22 | functions : 0, 23 | functionsCalled : 0, 24 | functionsPercentage : 100 25 | }, 26 | "function.js" : { 27 | total : 4, 28 | visited : 4, 29 | statementsPercentage : 100, 30 | conditions : 0, 31 | conditionsTrue : 0, 32 | conditionsFalse : 0, 33 | conditionsPercentage : 100, 34 | functions : 1, 35 | functionsCalled : 1, 36 | functionsPercentage : 100 37 | }, 38 | "if_no_block.js" : { 39 | total : 9, 40 | visited : 6, 41 | statementsPercentage : 100 * 6 / 9, 42 | conditions : 2, 43 | conditionsTrue : 1, 44 | conditionsFalse : 1, 45 | conditionsPercentage : 50, 46 | functions : 2, 47 | functionsCalled : 1, 48 | functionsPercentage : 50 49 | }, 50 | "no_block.js" : { 51 | total : 8, 52 | visited : 5, 53 | statementsPercentage : 100 * 5 / 8, 54 | conditions : 0, 55 | conditionsTrue : 0, 56 | conditionsFalse : 0, 57 | conditionsPercentage : 100, 58 | functions : 0, 59 | functionsCalled : 0, 60 | functionsPercentage : 100 61 | }, 62 | "label_continue.js" : { 63 | total : 2, 64 | visited : 2, 65 | statementsPercentage : 100, 66 | conditions : 0, 67 | conditionsTrue : 0, 68 | conditionsFalse : 0, 69 | conditionsPercentage : 100, 70 | functions : 0, 71 | functionsCalled : 0, 72 | functionsPercentage : 100 73 | }, 74 | "cond_simple_if.js" : { 75 | total : 22, 76 | visited : 22, 77 | statementsPercentage : 100, 78 | conditions : 9, 79 | conditionsTrue : 9, 80 | conditionsFalse : 0, 81 | conditionsPercentage : 50, 82 | functions : 3, 83 | functionsCalled : 3, 84 | functionsPercentage : 100 85 | }, 86 | "cond_simple_if_false.js" : { 87 | total : 24, 88 | visited : 15, 89 | statementsPercentage : 100 * 15 / 24, 90 | conditions : 10, 91 | conditionsTrue : 1, 92 | conditionsFalse : 9, 93 | conditionsPercentage : 50, 94 | functions : 3, 95 | functionsCalled : 3, 96 | functionsPercentage : 100 97 | }, 98 | "function_object.js" : { 99 | total : 7, 100 | visited : 7, 101 | statementsPercentage : 100, 102 | conditions : 0, 103 | conditionsTrue : 0, 104 | conditionsFalse : 0, 105 | conditionsPercentage : 100, 106 | functions : 4, 107 | functionsCalled : 4, 108 | functionsPercentage : 100 109 | }, 110 | "cond_decision_if.js" : { 111 | total : 17, 112 | visited : 15, 113 | statementsPercentage : 100 * 15 / 17, 114 | conditions : 7, 115 | conditionsTrue : 5, 116 | conditionsFalse : 2, 117 | conditionsPercentage : 50, 118 | functions : 1, 119 | functionsCalled : 1, 120 | functionsPercentage : 100 121 | }, 122 | "cond_multiple_if.js" : { 123 | total : 14, 124 | visited : 11, 125 | statementsPercentage : 100 * 11 / 14, 126 | conditions : 14, 127 | conditionsTrue : 7, 128 | conditionsFalse : 5, 129 | conditionsPercentage : 50 * (7 + 5) / 14, 130 | functions : 1, 131 | functionsCalled : 1, 132 | functionsPercentage : 100 133 | }, 134 | "cond_group_if.js" : { 135 | total : 4, 136 | visited : 2, 137 | statementsPercentage : 50, 138 | conditions : 7, 139 | conditionsTrue : 3, 140 | conditionsFalse : 2, 141 | conditionsPercentage : 50 * (3 + 2) / 7, 142 | functions : 0, 143 | functionsCalled : 0, 144 | functionsPercentage : 100 145 | }, 146 | "if_else.js" : { 147 | total : 10, 148 | visited : 7, 149 | statementsPercentage : 100 * 7 / 10, 150 | conditions : 6, 151 | conditionsTrue : 4, 152 | conditionsFalse : 1, 153 | conditionsPercentage : 50 * (4 + 1) / 6, 154 | functions : 1, 155 | functionsCalled : 1, 156 | functionsPercentage : 100 157 | }, 158 | "empty_function.js" : { 159 | total : 7, 160 | visited : 7, 161 | statementsPercentage : 100, 162 | conditions : 0, 163 | conditionsTrue : 0, 164 | conditionsFalse : 0, 165 | conditionsPercentage : 100, 166 | functions : 4, 167 | functionsCalled : 3, 168 | functionsPercentage : 100 * 3 / 4 169 | }, 170 | "for_in.js" : { 171 | total : 5, 172 | visited : 5, 173 | statementsPercentage : 100, 174 | conditions : 1, 175 | conditionsTrue : 1, 176 | conditionsFalse : 1, 177 | conditionsPercentage : 100, 178 | functions : 1, 179 | functionsCalled : 1, 180 | functionsPercentage : 100 181 | }, 182 | "minified.js" : { 183 | total : 17, 184 | visited : 13, 185 | statementsPercentage : 100 * 13 / 17, 186 | conditions : 3, 187 | conditionsTrue : 1, 188 | conditionsFalse : 2, 189 | conditionsPercentage : 50, 190 | functions : 6, 191 | functionsCalled : 2, 192 | functionsPercentage : 100 / 3 193 | }, 194 | "ternary.js" : { 195 | total : 12, 196 | visited : 7, 197 | statementsPercentage : 100 * 7 / 12, 198 | conditions : 8, 199 | conditionsTrue : 2, 200 | conditionsFalse : 6, 201 | conditionsPercentage : 50, 202 | functions : 6, 203 | functionsCalled : 1, 204 | functionsPercentage : 100 / 6 205 | } 206 | }; -------------------------------------------------------------------------------- /lib/clientCode.js: -------------------------------------------------------------------------------- 1 | var highlight = require("./highlight").highlight; 2 | var uglify = require("uglify-js").uglify; 3 | 4 | /** 5 | * Fist line of the generated file, it avoid instrumenting the same file twice 6 | */ 7 | var header = "//NODE-COVERAGE OFF\n"; 8 | 9 | function isInstrumented (code) { 10 | return (code.indexOf(header) === 0); 11 | } 12 | 13 | function generateWithHeaderOnly (code) { 14 | return { 15 | clientCode : header + code, 16 | error : false 17 | }; 18 | } 19 | 20 | function formatContent (content) { 21 | return { 22 | clientCode : content, 23 | error : false 24 | }; 25 | } 26 | 27 | function shouldBeExcluded (location, exclude) { 28 | if (!exclude) { 29 | return false; 30 | } else if (!exclude.forEach) { 31 | exclude = [exclude]; 32 | } 33 | 34 | var found = false; 35 | exclude.forEach(function (item) { 36 | if (location.indexOf(item) === 0) { 37 | found = true; 38 | } 39 | }); 40 | 41 | return found; 42 | } 43 | 44 | /** 45 | * These functions generate the additional code sent to the client. 46 | * This code contains the logic to send reports back to the server 47 | */ 48 | var composeFile = function (fileName, allLines, allConditions, allFunctions, code, options) { 49 | var highlighted = highlight(code); 50 | var clientCodeHeader = [ 51 | // header comment saying that this file is instrumented 52 | header, 53 | // closure that creates the global objects 54 | "(", createGlobalObjects.toString(), ")(", 55 | // argument for the closure call 56 | uglify.make_string(fileName), 57 | ");" 58 | ]; 59 | if (options.staticInfo !== false) { 60 | clientCodeHeader.push("(", storeStaticFileInfo.toString(), ")(", uglify.make_string(fileName), ",", JSON.stringify({ 61 | code : highlighted, 62 | lines : allLines, 63 | conditions : allConditions, 64 | functions : allFunctions 65 | }), ");"); 66 | } 67 | if (options.submit !== false) { 68 | clientCodeHeader.push("(", 69 | //second closure with the logic to send the report to the server 70 | submitData.toString(), ")();" 71 | ); 72 | } 73 | 74 | return { 75 | clientCode : clientCodeHeader.join("") + "\n" + code, 76 | staticInfo: { 77 | code : highlighted, 78 | lines : allLines, 79 | conditions : allConditions, 80 | functions : allFunctions 81 | }, 82 | error : false 83 | }; 84 | }; 85 | 86 | var createGlobalObjects = function (f) { 87 | var $$_l = this.$$_l; 88 | if (!this.$$_l) { 89 | $$_l = function (file, line) { 90 | var counter = $$_l.run[file].lines[line]++; 91 | if (!counter > 0) { 92 | $$_l.run[file].lines[line] = 1; 93 | } 94 | }; 95 | $$_c = function (file, line, condition) { 96 | var counter = $$_l.run[file].conditions[line + "=" + !!condition]++; 97 | if (!counter > 0) { 98 | $$_l.run[file].conditions[line + "=" + !!condition] = 1; 99 | } 100 | return condition; 101 | }; 102 | $$_f = function (file, line) { 103 | var counter = $$_l.run[file].functions[line]++; 104 | if (!counter > 0) { 105 | $$_l.run[file].functions[line] = 1; 106 | } 107 | }; 108 | $$_l.run = {}; 109 | this.$$_l = $$_l; 110 | } 111 | $$_l.run[f] = { 112 | lines: {}, 113 | conditions: {}, 114 | functions: {} 115 | }; 116 | }; 117 | 118 | var storeStaticFileInfo = function (f, args) { 119 | if (!this.$$_l.staticInfo) { 120 | $$_l.staticInfo = {}; 121 | } 122 | $$_l.staticInfo[f] = args; 123 | }; 124 | 125 | // The serialize in this function is much simplified, values are string/object/array/boolean/Number 126 | var submitData = function () { 127 | var serialize = function (object) { 128 | var properties = []; 129 | for (var key in object) { 130 | if (object.hasOwnProperty(key) && object[key] != null) { 131 | properties.push('"' + key.replace('\\', '\\\\') + '":' + getValue(object[key])); 132 | } 133 | } 134 | return "{" + properties.join(",") + "}"; 135 | }; 136 | var getValue = function (value) { 137 | if (typeof value === "string") { 138 | return quote(value); 139 | } else if (typeof value === "boolean") { 140 | return "" + value; 141 | } else if (value.join) { 142 | if (value.length == 0) { 143 | return "[]"; 144 | } else { 145 | var flat = []; 146 | for (var i = 0, len = value.length; i < len; i += 1) { 147 | flat.push(getValue(value[i])); 148 | } 149 | return '[' + flat.join(",") + ']'; 150 | } 151 | } else if (typeof value === "number") { 152 | return value; 153 | } else { 154 | return serialize(value); 155 | } 156 | }; 157 | var pad = function (s) { 158 | return '0000'.substr(s.length) + s; 159 | }; 160 | var replacer = function (c) { 161 | switch (c) { 162 | case '\b': return '\\b'; 163 | case '\f': return '\\f'; 164 | case '\n': return '\\n'; 165 | case '\r': return '\\r'; 166 | case '\t': return '\\t'; 167 | case '"': return '\\"'; 168 | case '\\': return '\\\\'; 169 | default: return '\\u' + pad(c.charCodeAt(0).toString(16)); 170 | } 171 | }; 172 | var quote = function (s) { 173 | return '"' + s.replace(/[\u0000-\u001f"\\\u007f-\uffff]/g, replacer) + '"'; 174 | }; 175 | if (!$$_l.__send) { 176 | $$_l.__send = function (data) { 177 | var xhr = (window.ActiveXObject) ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest(); 178 | xhr.open('POST', '/node-coverage-store', false); 179 | xhr.setRequestHeader('Content-Type', 'application/json'); 180 | xhr.send(data); 181 | }; 182 | } 183 | 184 | if (!$$_l.submit) { 185 | $$_l.submit = function (name) { 186 | $$_l.__send(serialize({ 187 | name : name || "", 188 | staticInfo : $$_l.staticInfo, 189 | run : $$_l.run 190 | })); 191 | }; 192 | } 193 | }; 194 | 195 | exports.generate = composeFile; 196 | exports.isInstrumented = isInstrumented; 197 | exports.generateWithHeaderOnly = generateWithHeaderOnly; 198 | exports.shouldBeExcluded = shouldBeExcluded; 199 | exports.formatContent = formatContent; 200 | -------------------------------------------------------------------------------- /lib/server/administration.js: -------------------------------------------------------------------------------- 1 | var express = require("express"), fs = require("fs"); 2 | 3 | var report = require("../report"); 4 | var common = require("./common"); 5 | 6 | exports.createApp = function (options) { 7 | var adminRoot = options.adminRoot || ""; 8 | var docRoot = options.docRoot || ""; 9 | 10 | function defaultReadReport (report, callback, req) { 11 | fs.readFile(adminRoot + "/" + report, function (err, data) { 12 | if (err) { 13 | console.error(err); 14 | callback.call(null, err); 15 | } else { 16 | try { 17 | var result = JSON.parse(data); 18 | callback.call(null, false, result); 19 | } catch (ex) { 20 | console.error(ex); 21 | callback.call(null, ex); 22 | } 23 | } 24 | }); 25 | }; 26 | 27 | function defaultReportsList (callback, req) { 28 | fs.readdir(adminRoot, function (err, files) { 29 | if (err) { 30 | callback.call(null, err); 31 | } else { 32 | var reports = []; 33 | files.forEach(function (file) { 34 | var time = parseInt(file.substring(file.lastIndexOf("_") + 1, file.lastIndexOf(".json")), 10); 35 | reports.push({ 36 | id : file, 37 | time : time, 38 | date : (new Date(time)).toString() 39 | }); 40 | }); 41 | callback.call(null, null, reports); 42 | } 43 | }); 44 | }; 45 | 46 | var app = express.createServer(); 47 | var reportsList = options.reportsList || defaultReportsList; 48 | var readReport = options.readReport || defaultReadReport; 49 | var canMerge = (options.canMerge !== false); 50 | 51 | var serverRoot = options.serverRoot || ""; 52 | 53 | app.set("view engine", "jade"); 54 | app.set("view options", { 55 | layout: false 56 | }); 57 | app.set("jsonp callback", true); 58 | 59 | app.set("views", __dirname + "/../../views"); 60 | app.use(express.static(__dirname + "/../../views/statics")); 61 | 62 | app.get("/", function (req, res) { 63 | reportsList(function (err, reports) { 64 | if (err) { 65 | res.send("Error while reading reports list", 404); 66 | } else { 67 | reports.sort(function (first, second) { 68 | var timeOne = first.time; 69 | var timeTwo = second.time; 70 | if (timeOne == timeTwo) { 71 | // rly ? 72 | return 0; 73 | } 74 | return timeOne < timeTwo ? 1 : -1; 75 | }); 76 | if (req.param("callback", false)) { 77 | res.json(reports); 78 | } else { 79 | res.render("admin", { 80 | canMerge: canMerge, 81 | serverRoot : serverRoot, 82 | reports : reports, 83 | conf : { 84 | htdoc : docRoot, 85 | report : adminRoot 86 | } 87 | }); 88 | } 89 | } 90 | }, req); 91 | }); 92 | 93 | app.get("/r/:report", function (req, res) { 94 | readReport(req.params.report, sendReport.bind(this, req, res), req); 95 | }); 96 | 97 | app.get("/r/:report/sort/:what/:how", function (req, res) { 98 | readReport(req.params.report, sendReport.bind(this, req, res), req); 99 | }); 100 | 101 | app.get("/r/:report/file/:fileName", function (req, res) { 102 | var fileName = req.params.fileName; 103 | readReport(req.params.report, function (err, data) { 104 | if (err) { 105 | res.send(500); 106 | } else { 107 | if (req.param("callback", false)) { 108 | res.json(data.files[fileName]); 109 | } else { 110 | res.render("file", { 111 | serverRoot : serverRoot, 112 | name : req.params.report, 113 | file : fileName, 114 | report : data.files[fileName] 115 | }); 116 | } 117 | } 118 | }, req); 119 | }); 120 | 121 | if (canMerge) { 122 | app.get("/merge", function (req, res) { 123 | var toBeMerged = req.param("report"); 124 | readMultipleReports(toBeMerged, req, function (err, reports) { 125 | if (err) { 126 | res.send(404); 127 | } else { 128 | var mergedReport = report.mergeReports(reports); 129 | 130 | if (req.param("callback", false)) { 131 | res.json(mergedReport); 132 | } else { 133 | var shorterNames = toBeMerged.map(function (name) { 134 | return name.substring(0, name.lastIndexOf("_")); 135 | }); 136 | var reportName = common.createReportName("merge_" + shorterNames.join("_")); 137 | var fileName = adminRoot + "/" + reportName; 138 | 139 | console.log("Saving merged report", fileName); 140 | fs.writeFile(fileName, JSON.stringify(mergedReport), "utf-8", function (err) { 141 | if (err) { 142 | msg = "Error while saving coverage report to " + fileName; 143 | console.error(msg, err); 144 | res.send(msg, 500); 145 | } else { 146 | res.redirect("/r/" + reportName); 147 | } 148 | }); 149 | } 150 | } 151 | }); 152 | }); 153 | } 154 | 155 | app.get("/stat/:report", function (req, res) { 156 | readReport(req.params.report, function (err, data) { 157 | if (err) { 158 | res.send(500); 159 | } else { 160 | var stats = report.stats(data); 161 | if (req.param("callback", false)) { 162 | res.json(stats); 163 | } else { 164 | res.render("stats", { 165 | serverRoot : serverRoot, 166 | name : req.params.report, 167 | report : stats 168 | }); 169 | } 170 | } 171 | }, req); 172 | }); 173 | 174 | function sendReport(req, res, err, report, name) { 175 | if (err) { 176 | res.send(500); 177 | } else { 178 | var what = req.params.what || "file"; 179 | var how = req.params.how || "desc"; 180 | 181 | if (req.param("callback", false)) { 182 | res.json(sortReport(report, what, how)); 183 | } else { 184 | res.render("report", { 185 | serverRoot : serverRoot, 186 | name : name || req.params.report, 187 | report : sortReport(report, what, how), 188 | sort : { 189 | what : what, 190 | how : how 191 | } 192 | }); 193 | } 194 | } 195 | }; 196 | 197 | function readMultipleReports (reports, req, callback) { 198 | var howMany = reports.length; 199 | var result = []; 200 | if (howMany == 0) { 201 | return callback.call(this, "No reports to read"); 202 | } 203 | reports.forEach(function (report) { 204 | readReport(report, function (err, data) { 205 | if (err) { 206 | callback.call(this, err); 207 | } else { 208 | result.push(data); 209 | howMany -= 1; 210 | if (howMany < 1) { 211 | callback.call(this, null, result); 212 | } 213 | } 214 | }, req); 215 | }); 216 | } 217 | return app; 218 | }; 219 | 220 | exports.start = function (docRoot, port, adminRoot, adminPort) { 221 | var app = exports.createApp({ 222 | docRoot : docRoot, 223 | adminRoot: adminRoot 224 | }); 225 | return app.listen(adminPort); 226 | }; 227 | 228 | function sortReport (reports, what, how) { 229 | var howToSort; 230 | var fileReports = []; 231 | for (var file in reports.files) { 232 | if (reports.files.hasOwnProperty(file)) { 233 | fileReports.push({ 234 | file : file, 235 | report : reports.files[file] 236 | }); 237 | } 238 | } 239 | 240 | var numericalSort = function (attribute, direction) { 241 | return function (first, second) { 242 | var firstValue = first.report[attribute].percentage; 243 | var secondValue = second.report[attribute].percentage; 244 | if (firstValue == secondValue) { 245 | return 0; 246 | } 247 | var compare = (firstValue > secondValue) ? 1 : -1; 248 | return direction == "asc" ? compare : -compare; 249 | }; 250 | }; 251 | 252 | if (what == "statement") { 253 | howToSort = numericalSort("statements", how); 254 | } else if (what == "condition") { 255 | howToSort = numericalSort("conditions", how); 256 | } else if (what == "function") { 257 | howToSort = numericalSort("functions", how); 258 | } else { 259 | // file sort, not a numerical sort 260 | howToSort = function (first, second) { 261 | var firstValue = first.file; 262 | var secondValue = second.file; 263 | if (firstValue == secondValue) { 264 | return 0; 265 | } 266 | var compare = firstValue.localeCompare(secondValue); 267 | return how == "asc" ? -compare : compare; 268 | }; 269 | } 270 | 271 | return { 272 | global : reports.global, 273 | files : fileReports.sort(howToSort) 274 | }; 275 | }; -------------------------------------------------------------------------------- /test/results/details.js: -------------------------------------------------------------------------------- 1 | /** 2 | * I don't want to test exactly the lines/conditions/functions ids. 3 | * So the key is just the line number 4 | * The expected objects looks like this 5 | * 6 | * statements : { 7 | * number : how many statements, 8 | * total : how many times they are called (total) 9 | * }, 10 | * conditions : { 11 | * true 12 | * number : how many conditions are evaluated true 13 | * total : how many times all statement are evaluated true 14 | * false 15 | * number : how many conditions are evaluated false 16 | * total : how many times all statement are evaluated false 17 | * all : how many conditions 18 | * }, 19 | * functions : { 20 | * number : how many functions are defined, 21 | * total : how many times they are called 22 | * } 23 | */ 24 | exports.results = { 25 | code : { 26 | "base.js" : { 27 | statements : { 28 | number : 3, 29 | total : 3 30 | }, 31 | conditions : { 32 | "true" : { 33 | number : 0, 34 | total : 0 35 | }, 36 | "false" : { 37 | number : 0, 38 | total : 0 39 | }, 40 | "all" : 0 41 | }, 42 | functions : { 43 | number : 0, 44 | total : 0 45 | } 46 | }, 47 | "longlines.js" : { 48 | statements : { 49 | number : 1, 50 | total : 1 51 | }, 52 | conditions : { 53 | "true" : { 54 | number : 0, 55 | total : 0 56 | }, 57 | "false" : { 58 | number : 0, 59 | total : 0 60 | }, 61 | "all" : 0 62 | }, 63 | functions : { 64 | number : 0, 65 | total : 0 66 | } 67 | }, 68 | "function.js" : { 69 | statements : { 70 | number : 4, 71 | total : 4 72 | }, 73 | conditions : { 74 | "true" : { 75 | number : 0, 76 | total : 0 77 | }, 78 | "false" : { 79 | number : 0, 80 | total : 0 81 | }, 82 | "all" : 0 83 | }, 84 | functions : { 85 | number : 1, 86 | total : 1 87 | } 88 | }, 89 | "if_no_block.js" : { 90 | statements : { 91 | number : 9, 92 | total : 6 93 | }, 94 | conditions : { 95 | "true" : { 96 | number : 1, 97 | total : 1 98 | }, 99 | "false" : { 100 | number : 1, 101 | total : 1 102 | }, 103 | "all" : 2 104 | }, 105 | functions : { 106 | number : 2, 107 | total : 1 108 | } 109 | }, 110 | "no_block.js" : { 111 | statements : { 112 | number : 8, 113 | total : 5 114 | }, 115 | conditions : { 116 | "true" : { 117 | number : 0, 118 | total : 0 119 | }, 120 | "false" : { 121 | number : 0, 122 | total : 0 123 | }, 124 | "all" : 0 125 | }, 126 | functions : { 127 | number : 0, 128 | total : 0 129 | } 130 | }, 131 | "label_continue.js" : { 132 | statements : { 133 | number : 2, 134 | total : 2 135 | }, 136 | conditions : { 137 | "true" : { 138 | number : 0, 139 | total : 0 140 | }, 141 | "false" : { 142 | number : 0, 143 | total : 0 144 | }, 145 | "all" : 0 146 | }, 147 | functions : { 148 | number : 0, 149 | total : 0 150 | } 151 | }, 152 | "cond_simple_if.js" : { 153 | statements : { 154 | number : 22, 155 | total : 22 156 | }, 157 | conditions : { 158 | "true" : { 159 | number : 9, 160 | total : 9 161 | }, 162 | "false" : { 163 | number : 0, 164 | total : 0 165 | }, 166 | "all" : 9 167 | }, 168 | functions : { 169 | number : 3, 170 | total : 3 171 | } 172 | }, 173 | "cond_simple_if_false.js" : { 174 | statements : { 175 | number : 24, 176 | total : 15 177 | }, 178 | conditions : { 179 | "true" : { 180 | number : 1, 181 | total : 1 182 | }, 183 | "false" : { 184 | number : 9, 185 | total : 9 186 | }, 187 | "all" : 10 188 | }, 189 | functions : { 190 | number : 3, 191 | total : 3 192 | } 193 | }, 194 | "function_object.js" : { 195 | statements : { 196 | number : 7, 197 | total : 7 198 | }, 199 | conditions : { 200 | "true" : { 201 | number : 0, 202 | total : 0 203 | }, 204 | "false" : { 205 | number : 0, 206 | total : 0 207 | }, 208 | "all" : 0 209 | }, 210 | functions : { 211 | number : 4, 212 | total : 4 213 | } 214 | }, 215 | "cond_decision_if.js" : { 216 | statements : { 217 | number : 17, 218 | total : 17 219 | }, 220 | conditions : { 221 | "true" : { 222 | number : 5, 223 | total : 5 224 | }, 225 | "false" : { 226 | number : 2, 227 | total : 2 228 | }, 229 | "all" : 7 230 | }, 231 | functions : { 232 | number : 1, 233 | total : 3 234 | } 235 | }, 236 | "cond_multiple_if.js" : { 237 | statements : { 238 | number : 14, 239 | total : 13 240 | }, 241 | conditions : { 242 | "true" : { 243 | number : 7, 244 | total : 7 245 | }, 246 | "false" : { 247 | number : 5, 248 | total : 5 249 | }, 250 | "all" : 14 251 | }, 252 | functions : { 253 | number : 1, 254 | total : 3 255 | } 256 | }, 257 | "cond_group_if.js" : { 258 | statements : { 259 | number : 4, 260 | total : 2 261 | }, 262 | conditions : { 263 | "true" : { 264 | number : 3, 265 | total : 3 266 | }, 267 | "false" : { 268 | number : 2, 269 | total : 2 270 | }, 271 | "all" : 7 272 | }, 273 | functions : { 274 | number : 0, 275 | total : 0 276 | } 277 | }, 278 | "if_else.js" : { 279 | statements : { 280 | number : 10, 281 | total : 8 282 | }, 283 | conditions : { 284 | "true" : { 285 | number : 4, 286 | total : 5 287 | }, 288 | "false" : { 289 | number : 1, 290 | total : 1 291 | }, 292 | "all" : 6 293 | }, 294 | functions : { 295 | number : 1, 296 | total : 2 297 | } 298 | }, 299 | "empty_function.js" : { 300 | statements : { 301 | number : 7, 302 | total : 7 303 | }, 304 | conditions : { 305 | "true" : { 306 | number : 0, 307 | total : 0 308 | }, 309 | "false" : { 310 | number : 0, 311 | total : 0 312 | }, 313 | "all" : 0 314 | }, 315 | functions : { 316 | number : 4, 317 | total : 3 318 | } 319 | }, 320 | "for_in.js" : { 321 | statements : { 322 | number : 5, 323 | total : 7 324 | }, 325 | conditions : { 326 | "true" : { 327 | number : 1, 328 | total : 1 329 | }, 330 | "false" : { 331 | number : 1, 332 | total : 2 333 | }, 334 | "all" : 1 335 | }, 336 | functions : { 337 | number : 1, 338 | total : 1 339 | } 340 | }, 341 | "minified.js" : { 342 | statements : { 343 | number : 17, 344 | total : 14 345 | }, 346 | conditions : { 347 | "true" : { 348 | number : 1, 349 | total : 1 350 | }, 351 | "false" : { 352 | number : 2, 353 | total : 2 354 | }, 355 | "all" : 3 356 | }, 357 | functions : { 358 | number : 6, 359 | total : 3 360 | } 361 | }, 362 | "ternary.js" : { 363 | statements : { 364 | number : 12, 365 | total : 8 366 | }, 367 | conditions : { 368 | "true" : { 369 | number : 2, 370 | total : 2 371 | }, 372 | "false" : { 373 | number : 6, 374 | total : 6 375 | }, 376 | "all" : 8 377 | }, 378 | functions : { 379 | number : 6, 380 | total : 2 381 | } 382 | } 383 | }, 384 | 385 | 386 | merge : { 387 | "file1.js" : { 388 | statements : { 389 | number : 10, 390 | total : 12 391 | }, 392 | conditions : { 393 | "true" : { 394 | number : 0, 395 | total : 0 396 | }, 397 | "false" : { 398 | number : 0, 399 | total : 0 400 | }, 401 | "all" : 0 402 | }, 403 | functions : { 404 | number : 1, 405 | total : 1 406 | } 407 | }, 408 | "file2.js" : { 409 | statements : { 410 | number : 15, 411 | total : 16 412 | }, 413 | conditions : { 414 | "true" : { 415 | number : 4, 416 | total : 5 417 | }, 418 | "false" : { 419 | number : 1, 420 | total : 1 421 | }, 422 | "all" : 4 423 | }, 424 | functions : { 425 | number : 4, 426 | total : 6 427 | } 428 | }, 429 | "file3.js" : { 430 | statements : { 431 | number : 9, 432 | total : 14 433 | }, 434 | conditions : { 435 | "true" : { 436 | number : 0, 437 | total : 0 438 | }, 439 | "false" : { 440 | number : 2, 441 | total : 2 442 | }, 443 | "all" : 2 444 | }, 445 | functions : { 446 | number : 3, 447 | total : 0 448 | } 449 | } 450 | }, 451 | 452 | 453 | mergeSelf : { 454 | "file1.js" : { 455 | statements : { 456 | number : 10, 457 | total : 24 458 | }, 459 | conditions : { 460 | "true" : { 461 | number : 0, 462 | total : 0 463 | }, 464 | "false" : { 465 | number : 0, 466 | total : 0 467 | }, 468 | "all" : 0 469 | }, 470 | functions : { 471 | number : 1, 472 | total : 2 473 | } 474 | }, 475 | "file2.js" : { 476 | statements : { 477 | number : 15, 478 | total : 32 479 | }, 480 | conditions : { 481 | "true" : { 482 | number : 4, 483 | total : 10 484 | }, 485 | "false" : { 486 | number : 1, 487 | total : 2 488 | }, 489 | "all" : 4 490 | }, 491 | functions : { 492 | number : 4, 493 | total : 12 494 | } 495 | }, 496 | "file3.js" : { 497 | statements : { 498 | number : 9, 499 | total : 28 500 | }, 501 | conditions : { 502 | "true" : { 503 | number : 0, 504 | total : 0 505 | }, 506 | "false" : { 507 | number : 2, 508 | total : 4 509 | }, 510 | "all" : 2 511 | }, 512 | functions : { 513 | number : 3, 514 | total : 0 515 | } 516 | } 517 | }, 518 | 519 | mergeSpecial : { 520 | coverage : { 521 | total : 9, 522 | visited : 9, 523 | statementsPercentage : 100, 524 | conditions : 2, 525 | conditionsTrue : 1, 526 | conditionsFalse : 2, 527 | conditionsPercentage : 75, 528 | functions : 3, 529 | functionsCalled : 3, 530 | functionsPercentage : 100 531 | }, 532 | details : { 533 | statements : { 534 | number : 9, 535 | total : 21 536 | }, 537 | conditions : { 538 | "true" : { 539 | number : 1, 540 | total : 1 541 | }, 542 | "false" : { 543 | number : 2, 544 | total : 3 545 | }, 546 | "all" : 2 547 | }, 548 | functions : { 549 | number : 3, 550 | total : 3 551 | } 552 | } 553 | } 554 | }; -------------------------------------------------------------------------------- /lib/report.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a coverage report for a list of file. 3 | * 4 | * The report contains: 5 | * 6 | * global : summary of the global coverage 7 | * statements 8 | * total : total number of lines, 9 | * covered : number of exectuded statement, 10 | * percentage : percentage of covered statements, float 0<>100, 11 | * conditions 12 | * total : total number of conditions, 13 | * coveredTrue : number of conditions evaluated to true, 14 | * coveredFalse : number of conditions evaluated to false, 15 | * percentage : percentage of conditions evaluated both true and false, 16 | * functions 17 | * total : total number of functions, 18 | * covered : number of functions that have been called (including empty functions), 19 | * percentage : percentage of functions called 20 | * files : map of single reports for every file @see generate 21 | */ 22 | function generateAll (descriptor, staticInfo) { 23 | var globalReport = { 24 | statements : { 25 | total : 0, 26 | covered : 0, 27 | percentage : 100 28 | }, 29 | conditions : { 30 | total : 0, 31 | coveredTrue : 0, 32 | coveredFalse : 0, 33 | percentage : 100 34 | }, 35 | functions : { 36 | total : 0, 37 | covered : 0, 38 | percentage : 100 39 | } 40 | }; 41 | var filesReport = {}; 42 | 43 | if (!staticInfo) { 44 | staticInfo = descriptor.staticInfo; 45 | } else if (descriptor.staticInfo) { 46 | mergeStaticInfo(staticInfo, descriptor.staticInfo); 47 | } 48 | 49 | for (var file in staticInfo) { 50 | if (staticInfo.hasOwnProperty(file)) { 51 | var fileReport = generate(file, descriptor, staticInfo); 52 | filesReport[file] = fileReport; 53 | 54 | globalReport.statements.total += fileReport.statements.total; 55 | globalReport.statements.covered += fileReport.statements.covered; 56 | 57 | globalReport.conditions.total += fileReport.conditions.total; 58 | globalReport.conditions.coveredTrue += fileReport.conditions.coveredTrue; 59 | globalReport.conditions.coveredFalse += fileReport.conditions.coveredFalse; 60 | 61 | globalReport.functions.total += fileReport.functions.total; 62 | globalReport.functions.covered += fileReport.functions.covered; 63 | } 64 | } 65 | if (globalReport.statements.total) { 66 | globalReport.statements.percentage = 100.0 / globalReport.statements.total 67 | * globalReport.statements.covered; 68 | } 69 | if (globalReport.conditions.total) { 70 | globalReport.conditions.percentage = 50.0 / globalReport.conditions.total 71 | * (globalReport.conditions.coveredTrue + globalReport.conditions.coveredFalse); 72 | } 73 | if (globalReport.functions.total) { 74 | globalReport.functions.percentage = 100.0 / globalReport.functions.total 75 | * globalReport.functions.covered; 76 | } 77 | 78 | return { 79 | global : globalReport, 80 | files : filesReport 81 | }; 82 | }; 83 | 84 | function mergeStaticInfo(destination, extraStaticInfo) { 85 | for (var file in extraStaticInfo) { 86 | if (extraStaticInfo.hasOwnProperty(file) && extraStaticInfo[file] && !destination[file]) { 87 | destination[file] = extraStaticInfo[file]; 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * Generate a coverage report for a single file. 94 | * 95 | * The report contains: 96 | * 97 | * code : 'highlighted' code it's an array of lines of code @see highlight module, 98 | * statements 99 | * total : total number of lines, 100 | * covered : number of exectuded statement, 101 | * detail : coverage detail for every line, how many times that statement was called, 102 | * percentage : percentage of covered statements, float 0<>100, 103 | * conditions 104 | * total : total number of conditions, 105 | * coveredTrue : number of conditions evaluated to true, 106 | * coveredFalse : number of conditions evaluated to false, 107 | * detail : list of conditions that evaluated 'true' or 'false' and 'all' 108 | * percentage : percentage of conditions evaluated both true and false (100 if no conditions), 109 | * functions 110 | * total : total number of functions, 111 | * covered : number of functions that have been called (including empty functions), 112 | * percentage : percentage of functions called, 113 | * detail : coverage detail of functions, how many times the function was called 114 | */ 115 | function generate (file, descriptor, staticInfo) { 116 | var fileStaticInfo = staticInfo[file]; 117 | var fileRunInfo = descriptor.run[file] || { 118 | lines: {}, 119 | conditions: {}, 120 | functions: {} 121 | }; 122 | return { 123 | code : fileStaticInfo.code, 124 | statements : statementCoverage(fileStaticInfo.lines, fileRunInfo.lines), 125 | conditions : conditionCoverage(fileStaticInfo.conditions, fileRunInfo.conditions), 126 | functions : functionCoverage(fileStaticInfo.functions, fileRunInfo.functions) 127 | }; 128 | }; 129 | 130 | function statementCoverage (allLines, coveredLines) { 131 | var covered = 0; 132 | allLines.forEach(function (line) { 133 | if (coveredLines[line] > 0) { 134 | covered += 1; 135 | } else { 136 | coveredLines[line] = 0; 137 | } 138 | }); 139 | 140 | return { 141 | total : allLines.length, 142 | covered : covered, 143 | detail : coveredLines, 144 | percentage : allLines.length == 0 ? 100 : 100.0 * covered / allLines.length 145 | }; 146 | }; 147 | 148 | function conditionCoverage (allConditions, coveredConditions) { 149 | var met = {}, metTrue = [], metFalse = []; 150 | allConditions.forEach(function (condition) { 151 | var numberTrue = coveredConditions[condition + "=true"] || 0; 152 | var numberFalse = coveredConditions[condition + "=false"] || 0; 153 | met[condition] = { 154 | "true" : numberTrue, 155 | "false" : numberFalse 156 | }; 157 | if (numberTrue > 0) { 158 | metTrue.push(condition); 159 | } 160 | if (numberFalse > 0) { 161 | metFalse.push(condition); 162 | } 163 | }); 164 | 165 | return { 166 | total : allConditions.length, 167 | coveredTrue : metTrue.length, 168 | coveredFalse : metFalse.length, 169 | detail : { 170 | "true" : metTrue, 171 | "false" : metFalse, 172 | "all" : met 173 | }, 174 | percentage : allConditions.length == 0 ? 175 | 100 : // no conditions means we covered them all 176 | 50.0 * (metTrue.length + metFalse.length) / allConditions.length 177 | // 50.0 because every condition counts for 2 178 | }; 179 | }; 180 | 181 | function functionCoverage(allFunctions, coveredFunctions) { 182 | // coveredFunctions has only the functions that were called 183 | detail = coveredFunctions || {}; 184 | var fnTotal = allFunctions.length, fnCalled = 0; 185 | allFunctions.forEach(function (fnName) { 186 | if ((fnName in detail) && detail[fnName] > 0) { 187 | fnCalled += 1; 188 | } else { 189 | detail[fnName] = 0; 190 | } 191 | }); 192 | 193 | return { 194 | total : fnTotal, 195 | covered : fnCalled, 196 | percentage : fnTotal == 0 ? 100 : (100.0 * fnCalled / fnTotal), 197 | detail : detail 198 | }; 199 | }; 200 | 201 | 202 | function mergeReports (reports) { 203 | var merged = { 204 | global : { 205 | statements : { 206 | total : 0, 207 | covered : 0, 208 | percentage : 100 209 | }, 210 | conditions : { 211 | total : 0, 212 | coveredTrue : 0, 213 | coveredFalse : 0, 214 | percentage : 100 215 | }, 216 | functions : { 217 | total : 0, 218 | covered : 0, 219 | percentage : 100 220 | } 221 | }, 222 | files : {} 223 | }; 224 | 225 | reports.forEach(function (report) { 226 | for (var fileName in report.files) { 227 | if (!merged.files[fileName]) { 228 | merged.files[fileName] = report.files[fileName]; 229 | } else { 230 | merged.files[fileName].statements = mergeStatementsOrFunctions( 231 | merged.files[fileName].statements, report.files[fileName].statements 232 | ); 233 | merged.files[fileName].conditions = mergeConditions( 234 | merged.files[fileName].conditions, report.files[fileName].conditions 235 | ); 236 | merged.files[fileName].functions = mergeStatementsOrFunctions( 237 | merged.files[fileName].functions, report.files[fileName].functions 238 | ); 239 | } 240 | } 241 | }); 242 | 243 | for (var fileReport in merged.files) { 244 | var report = merged.files[fileReport]; 245 | 246 | merged.global.statements.total += report.statements.total; 247 | merged.global.statements.covered += report.statements.covered; 248 | 249 | merged.global.conditions.total += report.conditions.total; 250 | merged.global.conditions.coveredTrue += report.conditions.coveredTrue; 251 | merged.global.conditions.coveredFalse += report.conditions.coveredFalse; 252 | 253 | merged.global.functions.total += report.functions.total; 254 | merged.global.functions.covered += report.functions.covered; 255 | } 256 | if (merged.global.statements.total) { 257 | merged.global.statements.percentage = 100.0 / merged.global.statements.total 258 | * merged.global.statements.covered; 259 | } 260 | if (merged.global.conditions.total) { 261 | merged.global.conditions.percentage = 50.0 / merged.global.conditions.total 262 | * (merged.global.conditions.coveredTrue + merged.global.conditions.coveredFalse); 263 | } 264 | if (merged.global.functions.total) { 265 | merged.global.functions.percentage = 100.0 / merged.global.functions.total 266 | * merged.global.functions.covered; 267 | } 268 | 269 | return merged; 270 | }; 271 | 272 | function mergeStatementsOrFunctions (one, two) { 273 | var merged = { 274 | total : 0, 275 | covered : 0, 276 | detail : {}, 277 | percentage : 0 278 | }; 279 | for (var lineId in one.detail) { 280 | merged.detail[lineId] = one.detail[lineId] + two.detail[lineId]; 281 | 282 | merged.total += 1; 283 | if (merged.detail[lineId] > 0) { 284 | merged.covered += 1; 285 | } 286 | } 287 | 288 | merged.percentage = merged.total ? 100.0 * merged.covered / merged.total : 100.0; 289 | 290 | return merged; 291 | }; 292 | 293 | function mergeConditions (one, two) { 294 | var merged = { 295 | total : one.total, 296 | coveredTrue : 0, 297 | coveredFalse : 0, 298 | detail : { 299 | "true" : [], 300 | "false" : [], 301 | "all" : {} 302 | }, 303 | percentage : 0 304 | }; 305 | 306 | ["true", "false"].forEach(function (condType) { 307 | merged.detail[condType] = one.detail[condType].slice(0); 308 | 309 | two.detail[condType].forEach(function (condId) { 310 | if (merged.detail[condType].indexOf(condId) === -1) { 311 | merged.detail[condType].push(condId); 312 | } 313 | }); 314 | 315 | merged[condType == "true" ? "coveredTrue" : "coveredFalse"] = merged.detail[condType].length; 316 | }); 317 | 318 | for (var condId in one.detail.all) { 319 | merged.detail.all[condId] = { 320 | "true" : one.detail.all[condId]["true"] + two.detail.all[condId]["true"], 321 | "false" : one.detail.all[condId]["false"] + two.detail.all[condId]["false"] 322 | }; 323 | } 324 | 325 | 326 | merged.percentage = merged.total ? 327 | 50.0 * (merged.coveredTrue + merged.coveredFalse) / merged.total : 100; 328 | 329 | return merged; 330 | }; 331 | 332 | exports.generate = generate; 333 | exports.generateAll = generateAll; 334 | exports.mergeReports = mergeReports; 335 | exports.stats = require("./statistics"); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-coverage 2 | _node-coverage_ is a tool that measures code coverage of JavaScript application. 3 | 4 | Code coverage is a measure typically used in software testing to describe the degree to which the source code has been tested. This is an indirect measure of quality of your tests. 5 | 6 | node-coverage can be used not only to extract measures on how well the application is covered by a test suite, but also to understand how much code is actually needed to load your application. 7 | 8 | ## Coverage criteria 9 | There are a large variety of coverage criteria. node-coverage measures 10 | 11 | * __statement coverage__. Whether or not each statement has been executed. 12 | * __condition coverage__. Whether or not each boolean sub-expression evaluated both to `true` and `false`. 13 | * __decision coverage__. For Javascript this implies from condition coverage. 14 | * __function coverage__. Whether or not each functions has been called. Full statement coverage doesn't imply full function coverage when empty functions are used. An empty function has full statement coverage even when it's not called. 15 | 16 | Why statement coverage is not enough? 17 | Consider the following code: 18 | 19 | var dangerous = createAnObject(); 20 | if (dangerous != null) { 21 | dangerous.doSomething(); 22 | } 23 | dangerous.doSomethingElse(); 24 | 25 | A test suite where `dangerous` is always different from `null` runs fine and achieve 100% statement coverage, however the program fails when `dangerous` is `null`. 26 | 27 | Such test suite has only 50% of condition coverage because the condition `dangerous != null` is never evaluated `false`. 28 | 29 | Note that for languages where boolean operators are not short-circuited, condition coverage does not necessarly imply decision coverage. This is __not__ the case in JavaScript. 30 | 31 | if (a && b) { 32 | //... 33 | } 34 | When `a` is `false`, `b` is not evaluated at all. 35 | 36 | a = true, b = true 37 | a = false, b = true 38 | has 100% decision coverage because the `if` evaluates both to `true` and `false` but only 75% condition coverage because `b` never evaluates `false`. 39 | 40 | Adding a test where 41 | 42 | a = false, b = false 43 | won't increase condition coverage because the second condition (wheter `b` is `true` or not) is never checked by the language. 44 | 45 | 46 | ## Prerequisites 47 | node-coverage works instrumenting your JavaScript code and serving those instrumented files to your browser from a web server. Therefore it depends on 48 | 49 | * [Optimist](https://github.com/substack/node-optimist) library to parse command line arguments. 50 | * [UglifyJS](https://github.com/mishoo/UglifyJS) to parse and instrument your files. 51 | * [Express](https://github.com/visionmedia/express) to serve instrumented files. 52 | * [Jade](https://github.com/visionmedia/jade) a templating engine to display coverage reports. 53 | * [mkdirp](https://github.com/substack/node-mkdirp) utility for recursively create directories. 54 | * [Connect](https://github.com/senchalabs/connect) middleware layer 55 | * [node-http-proxy](https://github.com/nodejitsu/node-http-proxy) http proxy for node.js 56 | 57 | Those dependencies can be installed (from the node-coverage directory) with: 58 | 59 | npm install 60 | 61 | Unit tests run on [Nodeunit](https://github.com/caolan/nodeunit). 62 | 63 | The administrative interface uses for "Stats & Graph" page 64 | 65 | * [jQuery](http://jquery.com) 66 | * [Highcharts](http://www.highcharts.com/) charting library written in JavaScript 67 | 68 | ## Usage 69 | node server.js -d "/var/www" -r "/var/log/reports" 70 | This creates a server listenig on port `8080` serving the content of your folder `/var/www` and saving coverage reports inside `/var/log/reports` 71 | 72 | Go to 73 | 74 | http://localhost:8080 75 | and run your test suite. When complete you must call from your scripts the function 76 | 77 | $$_l.submit() 78 | to submit the coverage report. The report is saved inside `/var/log/reports` as a JSON file. 79 | 80 | To see the report go to the administrative interface on 81 | 82 | http://localhost:8787 83 | 84 | 85 | It's also possible to specify a report name from the `submit` function 86 | 87 | $$_l.submit("myTestCaseReport") 88 | 89 | 90 | ### Supported options 91 | 92 | * `-h` or `--help` list of options 93 | * `-d` or `--doc-root` document root of the web server. All JS files in this folder will be instrumented. Default `/var/www` 94 | * `-p` or `--port` web server port. Default `8080` 95 | * `-r` or `--report-dir` directory where reports are stored. Default `/var/log/node-coverage` 96 | * `-a` or `--admin-port` administrative server port. Default `8787` 97 | * `--condition`, `--no-condition` Enable or disable condition coverage. By default it's enabled. 98 | * `--function`, `--no-function` Enable or disable function coverage. By default it's disabled. 99 | * `--static-info` In case files are pre-instrumented, path to the JSON file containing static information about instrumented files. 100 | * `--session`, `--no-session` Enable or disable storage of information not strictly needed by the browser. By default it's enabled. Disabling this means that more code is sent to and from the client. 101 | * `-i` or `--ignore` Ignore file or folder. This file/folder won't be instrumented. Path is relative to document root. 102 | * `--proxy` Proxy mode. You can use node-coverage to instrument files on a differnt host. 103 | * `--exit-on-submit` The default behavior is to keep the server running in order to collect multiple reports. By enabling this options the server will automatically shut down when a coverage report is received. This is useful for some continuous integration environment. If you want to collect more coverage reports but still be able to shut down the server when tests are done you can submit a request to '/node-coverage-please-exit'. 104 | * `-v` or `--verbose` Enable more verbose logging information. Default `false`. 105 | 106 | By default function coverage is disabled, to enable it you can run 107 | 108 | node server.js --function 109 | 110 | or 111 | 112 | node server.js --no-condition 113 | 114 | to disable condition coverage. 115 | 116 | You can exclude some files or folders using 117 | 118 | node server.js -i lib/minified -i lib/jquery.js 119 | 120 | 121 | 122 | ## Instrumenting offline 123 | 124 | The server instruments JavaScript files on each request. It's possible to instrument offline your files running 125 | 126 | node instrument.js /var/www/myApp /var/www/myInstrumentedApp 127 | 128 | You can then run the server with 129 | 130 | node server.js -d /var/www/myInstrumentedApp 131 | 132 | 133 | ### Supported options 134 | 135 | * `-h` or `--help` list of options 136 | * `-t` ot `test` run unit tests 137 | * `--condition`, `--no-condition` enable or disable condition coverage. By default it's enabled. 138 | * `--function`, `--no-function` enable or disable function coverage. By default it's disabled. 139 | * `--static-info` Path to a JSON output file which will contain static information about instrumented files. Using this option reduces the size of instrumented files. 140 | * `-i` or `--ignore` Ignore file or folder. This file/folder is copied in target folder but not instrumented. Path relative to the source folder. 141 | * `-x` or `--exclude` Exclude file or folder. This file/folder won't be copied in target folder. Path relative to the source folder. 142 | 143 | By default function coverage is disabled, to enable it you can run 144 | 145 | node instrument.js --function /var/www/myApp /var/www/myInstrumentedApp 146 | 147 | or 148 | 149 | node instrument.js --no-condition /var/www/myApp /var/www/myInstrumentedApp 150 | 151 | to disable condition coverage. 152 | 153 | The code generated offline is equal to the one generated by the server when storage is disabled with `--no-session`, unless `--static-info` is used. 154 | 155 | You can also instrument a single file launching 156 | 157 | node instrument.js myScript.js 158 | 159 | The output is sent to standard input. 160 | 161 | The command 162 | 163 | node instrument /var/www/myApp /var/www/myInstrumentedApp -x .git -i lib/minified 164 | 165 | copies and instrument all files inside `myApp` excluding `.git` which is not copied at all and `lib/minified` which is copied but won't be instrumented for coverage. 166 | 167 | ### Collecting Coverage 168 | 169 | When instrumented offline, files can be served 170 | 171 | * by node-coverage using as document root the instrumented path 172 | 173 | * by any other web server. Reports however should still be sent back to node-coverage either through XHR or form submit. 174 | 175 | By default `$$_l.submit` sends an XHR POST request to `/node-coverage-store` containing the JSON report. 176 | 177 | You can set up your server to redirect this request to node coverage or override the private method `$$_l.__send`. This method receives the coverage report as string. 178 | 179 | node-coverage server accepts two types of POST request: 180 | 181 | * XHR with `Content-type: application/json` and coverage report as request body. 182 | * Form submit with `Content-type: application/x-www-form-urlencoded` and coverage report as a string inside the field `coverage`. 183 | 184 | #### Unit Test 185 | 186 | In order to run unit tests after cloning this repository you need to run 187 | 188 | node instrument.js -t 189 | 190 | 191 | ## JSONP API 192 | 193 | Once the server is started you can access the built-in adminitrative interface or use it's JSONP API to get reports as JSON objects and use them in your own tools. 194 | 195 | You can target any page in the administrative interface adding a `?callback=myJsonPCallback` GET parameter. 196 | Empty space characters should be converted in `%20`. 197 | 198 | ### Get the list of reports 199 | 200 | http://localhost:8787/?callback=myCallback 201 | 202 | The returned JSON is an Array of objects containing 203 | 204 | * `id` : report name 205 | * `time` : creation timestamp 206 | * `date` : creation date 207 | 208 | ### Get the details of a report 209 | 210 | http://localhost:8787/r/[id]?callback=myCallback 211 | 212 | Replace `[id]` with the actual report's id. 213 | 214 | The returned JSON has the following structure 215 | 216 | * `global` 217 | * `statements` 218 | * `total` : total number of lines, 219 | * `covered` : number of exectuded statement, 220 | * `percentage` : percentage of covered statements, float 0<>100, 221 | * `conditions` 222 | * `total` : total number of conditions, 223 | * `coveredTrue` : number of conditions evaluated to true, 224 | * `coveredFalse` : number of conditions evaluated to false, 225 | * `percentage` : percentage of conditions evaluated both true and false, 226 | * `functions` 227 | * `total` : total number of functions, 228 | * `covered` : number of functions that have been called (including empty functions), 229 | * `percentage` : percentage of functions called 230 | * `files` : map of single reports for every file. The key being the file name and the value being the file report 231 | * `functions` : history of all covered functions 232 | 233 | By default files reports are sorted alphabetically by file name. 234 | 235 | You can change the sorting criteria targeting 236 | 237 | http://localhost:8787/r/[id]/sort/[what]/[how]?callback=myCallback 238 | 239 | Where 240 | 241 | * `what` is either `file` for alphabetical sort or `statement`, `condition` or `function` to sort according to the desired metric. 242 | * `how` is either `asc` or `desc` 243 | 244 | ### Get the statistics of a report 245 | 246 | http://localhost:8787/stat/[id]?callback=myCallback 247 | 248 | Replace `[id]` with the actual report's id. 249 | 250 | The returned JSON has the following structure 251 | 252 | * `unused` : number of unused statements 253 | * `byFile` : object where the key is a file name and the value is the number of unused statements 254 | * `byPackage` : group unused statements by "package" or folder. 255 | 256 | ### Get a file report 257 | 258 | http://localhost:8787/r/[id]/file/[fileName]?callback=myCallback 259 | 260 | Slashes in `fileName` must be converted into `+` 261 | 262 | The returned JSON contains 263 | 264 | * `code` : _highlighted_ code 265 | * `src` : array (one entry per line of code) where value are object with 266 | * `s` : source line 267 | * `l` : lineid of the instrumented function 268 | * `c` : list of conditions (array) 269 | * `fns` : object mapping a function id to the generated line of code 270 | * `statements` 271 | * `total` : total number of lines, 272 | * `covered` : number of exectuded statement, 273 | * `detail` : coverage detail for every line, how many times that statement was called, 274 | * `percentage` : percentage of covered statements, float 0<>100, 275 | * `conditions` 276 | * `total` : total number of conditions, 277 | * `coveredTrue` : number of conditions evaluated to true, 278 | * `coveredFalse` : number of conditions evaluated to false, 279 | * `detail` : list of conditions that evaluated 'true' or 'false' and 'all' for both 280 | * `percentage` : percentage of conditions evaluated both true and false (100 if no conditions), 281 | * `functions` 282 | * `total` : total number of functions, 283 | * `covered` : number of functions that have been called (including empty functions), 284 | * `percentage` : percentage of functions called, 285 | * `detail` : coverage detail of functions, how many times the function was called 286 | 287 | ### Merge multiple reports 288 | 289 | http://localhost:8787/merge/?report=[id]&report=[id]?callback=myCallback 290 | 291 | Where `id` is the report name. It's possible to merge more than two reports adding extra `&report=[id]` 292 | 293 | The returned JSON has the same structure of a single report. 294 | 295 | It's also possible to merge multiple reports from the command line 296 | 297 | node merge.js -o destination_report.json report1.json report2.json [... reportN.json] 298 | 299 | 300 | ## Interpreters 301 | 302 | node-coverage has a modular system for interpreting and instrumenting JavaScript files. This allows you to create an interpreter for any type of file. 303 | 304 | The base interpreter is able to instrument standard JavaScript files, but you can create your own adding a module inside `lib/interpreters` with the following structure 305 | 306 | exports.filter = { 307 | files : /.*/, // a regular expression matching file names 308 | content : /\/\!/ // a regular expression matching file content 309 | }; 310 | 311 | exports.interpret = function (file, content, options) {} 312 | 313 | Filter object specifies which files are handled by the module. 314 | 315 | * `files` is mandatory, it's a regular expression matching the file name, examples are `/.*/` for any file, `/\.js$/` for JavaScript files 316 | * `content` is optional, it's a regular expression matching the file content. File content are checked against this expression only if their file name matches `filter.files`. 317 | 318 | `interpret` is the function that instruments the code. It takes 3 parameters 319 | 320 | * `file` File name 321 | * `content` File content 322 | * `options` Coverage options 323 | * `function` boolean, enable function coverage 324 | * `condition` boolean, enable condition coverage 325 | * `staticInfo` boolean, whether to include static information inside the instrumented code 326 | * `submit` boolean, whether to include the submit function inside the instrumented code 327 | 328 | this function must return an object containing 329 | 330 | * `clientCode` the instrumented code, this is sent to the client 331 | * `staticInfo` an object describing static information about the file 332 | 333 | ## Proxy 334 | 335 | node-coverage can also be used as an http proxy to instrument files hosted on a different machine. 336 | 337 | node server.js --proxy -p 8000 338 | 339 | Start the instrumentation server in proxy mode. You can configure your browser to use an http proxy targeting `localhost` on port `8000` 340 | 341 | You can also enable or disable condition or function coverage using the same options of a standalone server or specify a differnt path where to store coverage reports. 342 | 343 | node server.js --proxy --no-condition -r ~/reports 344 | 345 | At the moment it only support http, not https. 346 | 347 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/piuccio/node-coverage/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 348 | 349 | -------------------------------------------------------------------------------- /lib/interpreters/basic javascript.js: -------------------------------------------------------------------------------- 1 | var parser = require("uglify-js").parser; 2 | var uglify = require("uglify-js").uglify; 3 | var clientCode = require("../clientCode"); 4 | 5 | // This interpreter applies to any JS file regardless of the content 6 | exports.filter = { 7 | files : /\.js$/ 8 | }; 9 | 10 | /** 11 | * This is the main function of this module (it's the only one exported) 12 | * Given a file path and content returns the instrumented file 13 | * It won't instrument files that are already instrumented 14 | * 15 | * options allow to enable/disable coverage metrics 16 | * "function" enable function coverage 17 | * "condition" enable condition coverage 18 | */ 19 | exports.interpret = function (file, content, options) { 20 | options = options || { 21 | "function" : true, 22 | "condition" : true, 23 | "staticInfo": true, 24 | "submit": true, 25 | "verbose": false 26 | }; 27 | 28 | if (clientCode.isInstrumented(content)) { 29 | if (options.verbose) { 30 | console.log("\t", file, "already instrumented"); 31 | } 32 | return clientCode.formatContent(content); 33 | } else if (clientCode.shouldBeExcluded(file, options.ignore)) { 34 | if (options.verbose) { 35 | console.log("\t", file, "ignored"); 36 | } 37 | return clientCode.generateWithHeaderOnly(content); 38 | } 39 | 40 | try { 41 | var tree = parser.parse(content,false, true); 42 | } catch (ex) { 43 | console.error("Error instrumentig file", file, options.verbose ? ex : ex.message); 44 | var code = clientCode.formatContent(content); 45 | code.error = true; 46 | return code; 47 | } 48 | 49 | var walker = uglify.ast_walker(); 50 | // this is the list of nodes being analyzed by the walker 51 | // without this, w.walk(this) would re-enter the newly generated code with infinite recursion 52 | var analyzing = []; 53 | // list of all lines' id encounterd in this file 54 | var lines = []; 55 | // list of all conditions' id encounterd in this file 56 | var allConditions = []; 57 | // list of all functions' id encounterd in this file 58 | var allFunctions = []; 59 | 60 | /** 61 | * A statement was found in the file, remember its id. 62 | */ 63 | function rememberStatement (id) { 64 | lines.push(id); 65 | }; 66 | 67 | /** 68 | * A function was found in the file, remember its id. 69 | */ 70 | function rememberFunction (id) { 71 | allFunctions.push(id); 72 | }; 73 | 74 | /** 75 | * Generic function for counting a line. 76 | * It generates a lineId from the line number and the block name (in minified files there 77 | * are more logical lines on the same file line) and adds a function call before the actual 78 | * line of code. 79 | * 80 | * 'this' is any node in the AST 81 | */ 82 | function countLine() { 83 | var ret; 84 | if (this[0].start && analyzing.indexOf(this) < 0) { 85 | giveNameToAnonymousFunction.call(this); 86 | var lineId = this[0].name + this[0].start.line + "_" + this[0].start.pos; 87 | rememberStatement(lineId); 88 | 89 | analyzing.push(this); 90 | ret = [ "splice", 91 | [ 92 | [ "stat", 93 | [ "call", [ "name", "$$_l" ], 94 | [ 95 | [ "string", file], 96 | [ "string", lineId] 97 | ] 98 | ] 99 | ], 100 | walker.walk(this) 101 | ] 102 | ]; 103 | analyzing.pop(this); 104 | } 105 | return ret; 106 | }; 107 | 108 | /** 109 | * Walker for 'if' nodes. It overrides countLine because we want to instrument conditions. 110 | * 111 | * 'this' is an if node, so 112 | * 'this[0]' is the node descriptor 113 | * 'this[1]' is the decision block 114 | * 'this[2]' is the 'then' code block 115 | * 'this[3]' is the 'else' code block 116 | * 117 | * Note that if/else if/else in AST are represented as nested if/else 118 | */ 119 | function countIf() { 120 | var self = this, ret; 121 | if (self[0].start && analyzing.indexOf(self) < 0) { 122 | var decision = self[1]; 123 | var lineId = self[0].name + self[0].start.line; 124 | 125 | self[1] = wrapCondition(decision, lineId); 126 | 127 | // We are adding new lines, make sure code blocks are actual blocks 128 | if (self[2] && self[2][0].start && self[2][0].start.value != "{") { 129 | self[2] = [ "block", [self[2]]]; 130 | } 131 | 132 | if (self[3] && self[3][0].start && self[3][0].start.value != "{") { 133 | self[3] = [ "block", [self[3]]]; 134 | } 135 | } 136 | 137 | ret = countLine.call(self); 138 | 139 | if (decision) { 140 | analyzing.pop(decision); 141 | } 142 | 143 | return ret; 144 | }; 145 | 146 | /** 147 | * This is the key function for condition coverage as it wraps every condition in 148 | * a function call. 149 | * The condition id is generated fron the lineId (@see countLine) plus the character 150 | * position of the condition. 151 | */ 152 | function wrapCondition(decision, lineId, parentPos) { 153 | if (options.condition === false) { 154 | // condition coverage is disabled 155 | return decision; 156 | } 157 | 158 | if (isSingleCondition(decision)) { 159 | var pos = getPositionStart(decision, parentPos); 160 | var condId = lineId + "_" + pos; 161 | 162 | analyzing.push(decision); 163 | allConditions.push(condId); 164 | return ["call", 165 | ["name", "$$_c"], 166 | [ 167 | [ "string", file ], 168 | [ "string", condId], 169 | decision 170 | ] 171 | ]; 172 | } else { 173 | decision[2] = wrapCondition(decision[2], lineId, getPositionStart(decision, parentPos)); 174 | decision[3] = wrapCondition(decision[3], lineId, getPositionEnd(decision, parentPos)); 175 | 176 | return decision; 177 | } 178 | }; 179 | 180 | /** 181 | * Wheter or not the if decision has only one boolean condition 182 | */ 183 | function isSingleCondition(decision) { 184 | if (decision[0].start && decision[0].name != "binary") { 185 | return true; 186 | } else if (decision[1] == "&&" || decision[1] == "||") { 187 | return false; 188 | } else { 189 | return true; 190 | } 191 | }; 192 | 193 | /** 194 | * Get the start position of a given condition, if it has a start it's a true condition 195 | * so get the value, otherwise use a default value that is coming from an upper decision 196 | */ 197 | function getPositionStart (decision, defaultValue) { 198 | if (decision[0].start) { 199 | return decision[0].start.pos; 200 | } else { 201 | return defaultValue || "s"; 202 | } 203 | }; 204 | 205 | /** 206 | * As for getPositionStart but returns end position. It allows to give different ids to 207 | * math and binary operations in multiple conditions ifs. 208 | */ 209 | function getPositionEnd (decision, defaultValue) { 210 | if (decision[0].end) { 211 | return decision[0].end.pos; 212 | } else { 213 | return defaultValue || "e"; 214 | } 215 | }; 216 | 217 | /** 218 | * Generic function for every node that needs to be wrapped in a block. 219 | * For instance, the following code 220 | * 221 | * for (a in b) doSomething(a) 222 | * 223 | * once converted in AST does not have a block but only a function call. 224 | * Instrumentig this code would return 225 | * 226 | * for (a in b) instrumentation() 227 | * doSomething(a) 228 | * 229 | * which clearly does not have the same behavior as the non instrumented code. 230 | * 231 | * This function generates a function that can be used by the walker to add 232 | * blocks when they are missing depending on where the block is supposed to be 233 | */ 234 | function wrapBlock(position) { 235 | return function countFor() { 236 | var self = this; 237 | 238 | if (self[0].start && analyzing.indexOf(self) < 0) { 239 | if (self[0].start && analyzing.indexOf(self) < 0) { 240 | if (self[position] && self[position][0].name != "block") { 241 | self[position] = [ "block", [self[position]]]; 242 | } 243 | } 244 | } 245 | 246 | return countLine.call(self); 247 | }; 248 | }; 249 | 250 | /** 251 | * Label nodes need special treatment as well. 252 | * 253 | * myLabel : for (;;) { 254 | * //whateveer code here 255 | * continue myLabel 256 | * } 257 | * 258 | * Label can be wrapped by countLine, hovewer the subsequent for shouldn't be wrapped. 259 | * 260 | * instrumentation("label"); 261 | * mylabel : instrumentation("for") 262 | * for (;;) {} 263 | * 264 | * The above code would be wrong. 265 | * 266 | * This function makes sure that the 'for' after a label is not instrumented and that 267 | * the 'for' content is wrapped in a block. 268 | * 269 | * I'm don't think it's reasonable to use labels with something that is not a 'for' block. 270 | * In that case the instrumented code might easily break. 271 | */ 272 | function countLabel() { 273 | var ret; 274 | if (this[0].start && analyzing.indexOf(this) < 0) { 275 | var content = this[2]; 276 | 277 | if (content[0].name == "for" && content[4] && content[4].name != "block") { 278 | content[4] = [ "block", [content[4]]]; 279 | } 280 | analyzing.push(content); 281 | 282 | var ret = countLine.call(this); 283 | 284 | analyzing.pop(content); 285 | } 286 | return ret; 287 | }; 288 | 289 | /** 290 | * Instrumenting function strictly needed for statement coverage only in case of 'defun' 291 | * (function definition), however the block 'function' does not correspond to a new statement. 292 | * This method allows to track every function call (function coverage). 293 | * 294 | * As far as I can tell, 'function' is different from 'defun' for the fact that 'defun' 295 | * refers to the global definition of a function 296 | * function something () {} -> defun 297 | * something = function () {} -> function 298 | * 'function' doesn't need to be counted because the line is covered by 'name' or whatever 299 | * other block. 300 | * 301 | * Strictly speaking full statement coverage does not imply function coverage only if there 302 | * are empty function, which however are empty! 303 | * 304 | * The tracking for functions is also a bit different from countLine (except 'defun'). This 305 | * method assigns every function a name and tracks the history of every call throughout the 306 | * whole lifetime of the application, It's a sort of profiler. 307 | * 308 | * 309 | * The structure of 'this' is 310 | * 'this[0]' node descriptor 311 | * 'this[1]' string, name of the function or null 312 | * 'this[2]' array of arguments names (string) 313 | * 'this[3]' block with the function's body 314 | * 315 | * As 'function' happens in the middle of a line, the instrumentation should be in the body. 316 | */ 317 | function countFunction () { 318 | var ret; 319 | if (this[0].start && analyzing.indexOf(this) < 0) { 320 | var defun = this[0].name === "defun"; 321 | var lineId = this[0].name + this[0].start.line + "_" + this[0].start.pos; 322 | var fnName = this[1] || this[0].anonymousName || "(?)"; 323 | var body = this[3]; 324 | 325 | analyzing.push(this); 326 | 327 | // put a new function call inside the body, works also on empty functions 328 | if (options["function"]) { 329 | body.splice(0, 0, [ "stat", 330 | [ "call", 331 | ["name", "$$_f"], 332 | [ 333 | ["string", file], 334 | ["string", fnName + "_" + this[0].start.line + "_" + this[0].start.pos] 335 | ] 336 | ] 337 | ]); 338 | // It would be great to instrument the 'exit' from a function 339 | // but it means tracking all return statements, maybe in the future... 340 | 341 | rememberFunction(fnName + "_" + this[0].start.line + "_" + this[0].start.pos); 342 | } 343 | 344 | 345 | if (defun) { 346 | // 'defun' should also be remembered as statements 347 | rememberStatement(lineId); 348 | 349 | ret = [ "splice", 350 | [ 351 | [ "stat", 352 | [ "call", [ "name", "$$_l" ], 353 | [ 354 | [ "string", file], 355 | [ "string", lineId] 356 | ] 357 | ] 358 | ], 359 | walker.walk(this) 360 | ] 361 | ]; 362 | } else { 363 | ret = walker.walk(this); 364 | } 365 | 366 | analyzing.pop(this); 367 | 368 | } 369 | return ret; 370 | }; 371 | 372 | /** 373 | * This function tries to extract the name of anonymous functions depending on where they are 374 | * defined. 375 | * 376 | * For instance 377 | * var something = function () {} 378 | * the function itself is anonymous but we can use 'something' as its name 379 | * 380 | * 'node' is anything that gets counted, function are extracted from 381 | * 382 | * var 383 | * node[0] : node description 384 | * node[1] : array of assignments 385 | * node[x][0] : variable name 386 | * node[x][1] : value, node 387 | * 388 | * object (when functions are properties of an object) 389 | * node[0] : node description 390 | * node[1] : array of attributes 391 | * node[x][0] : attribute name 392 | * node[x][1] : value, node 393 | * 394 | * assign (things like object.name = function () {}) 395 | * node[0] : node description 396 | * node[1] : type of assignment, 'true' if '=' or operand (like += |= and others) 397 | * node[2] : left value, object property or variable 398 | * node[3] : right value, node 399 | * 400 | * in case of assign, node[2] can be 401 | * 'name' if we assign to a variable 402 | * name[0] : node description 403 | * name[1] : variable's name 404 | * 'dot' when we assign to an object's property 405 | * dot[0] : node description 406 | * dot[1] : container object 407 | * dot[2] : property 408 | */ 409 | function giveNameToAnonymousFunction () { 410 | node = this; 411 | 412 | if (node[0].name == "var" || node[0].name == "object") { 413 | node[1].forEach(function (assignemt) { 414 | if (assignemt[1]) { 415 | if (assignemt[1][0].name === "function") { 416 | assignemt[1][0].anonymousName = assignemt[0]; 417 | } else if (assignemt[1][0].name === "conditional") { 418 | if (assignemt[1][2][0] && assignemt[1][2][0].name === "function") { 419 | assignemt[1][2][0].anonymousName = assignemt[0]; 420 | } 421 | if (assignemt[1][3][0] && assignemt[1][3][0].name === "function") { 422 | assignemt[1][3][0].anonymousName = assignemt[0]; 423 | } 424 | } 425 | } 426 | }); 427 | } else if (node[0].name == "assign" && node[1] === true) { 428 | if (node[3][0].name === "function") { 429 | node[3][0].anonymousName = getNameFromAssign(node); 430 | } else if (node[3][0] === "conditional") { 431 | if (node[3][2][0] && node[3][2][0].name === "function") { 432 | node[3][2][0].anonymousName = getNameFromAssign(node); 433 | } 434 | if (node[3][3][0] && node[3][3][0].name === "function") { 435 | node[3][3][0].anonymousName = getNameFromAssign(node); 436 | } 437 | } 438 | } 439 | }; 440 | 441 | function getNameFromAssign (node) { 442 | if (node[2][0].name === "name") { 443 | return node[2][1]; 444 | } else if (node[2][0].name === "dot") { 445 | return node[2][2]; 446 | } 447 | } 448 | 449 | /** 450 | * This function wraps ternary conditionals in order to have condition coverage 451 | * 452 | * 'this' is a node containing 453 | * 'this[0]' node descriptor 454 | * 'this[1]' decision block 455 | * 'this[2]' first statement 456 | * 'this[3]' second statement 457 | */ 458 | function wrapConditionals () { 459 | if (options.condition === false) { 460 | // condition coverage is disabled 461 | return; 462 | } 463 | 464 | var self = this; 465 | if (self[0].start && analyzing.indexOf(self) < 0) { 466 | analyzing.push(self); 467 | 468 | var lineId = self[0].name + self[0].start.line; 469 | 470 | self[1] = wrapCondition(self[1], lineId); 471 | 472 | self[2] = walker.walk(self[2]); 473 | self[3] = walker.walk(self[3]); 474 | 475 | analyzing.pop(self); 476 | 477 | return self; 478 | } else if (self[1]) { 479 | self[1] = wrapCondition(self[1], lineId); 480 | } 481 | }; 482 | 483 | var instrumentedTree = walker.with_walkers({ 484 | "stat" : countLine, 485 | "label" : countLabel, 486 | "break" : countLine, 487 | "continue" : countLine, 488 | "debugger" : countLine, 489 | "var" : countLine, 490 | "const" : countLine, 491 | "return" : countLine, 492 | "throw" : countLine, 493 | "try" : countLine, 494 | "defun" : countFunction, 495 | "if" : countIf, 496 | "while" : wrapBlock(2), 497 | "do" : wrapBlock(2), 498 | "for" : wrapBlock(4), 499 | "for-in" : wrapBlock(4), 500 | "switch" : countLine, 501 | "with" : countLine, 502 | "function" : countFunction, 503 | "assign" : giveNameToAnonymousFunction, 504 | "object" : giveNameToAnonymousFunction, 505 | "conditional": wrapConditionals 506 | }, function () { 507 | return walker.walk(tree); 508 | }); 509 | 510 | var code = generateCode(instrumentedTree); 511 | return clientCode.generate(file, lines, allConditions, allFunctions, code, options); 512 | }; 513 | 514 | function generateCode (tree) { 515 | return uglify.gen_code(tree, {beautify : true}); 516 | }; --------------------------------------------------------------------------------