├── .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 | [](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 | };
--------------------------------------------------------------------------------