Permalink
Browse files

Code coverage done right

* Moved code coverage from srunner to stest
* Updated README
* Updated deps

Rather than evaluate the entire code block at once,
this new methods continues to evaluate the code as
you call methods, ensuring that code inside timeouts
and stuff still gets evaluated.
  • Loading branch information...
1 parent 7c36a9c commit 0462253efb7bd6135e1c0306c819d0d4eff2728f Siddharth Mahendraker committed Dec 19, 2011
Showing with 92 additions and 178 deletions.
  1. +4 −5 README.md
  2. +0 −131 bin/coverage.js
  3. +1 −36 bin/srunner.js
  4. +64 −4 lib/stest.js
  5. +2 −2 package.json
  6. +6 −0 test/fixtures/coverage.js
  7. +15 −0 test/test-coverage.js
View
@@ -55,16 +55,15 @@ See the source for more details and documentation.
Tests can be run en masse using `srunner`:
- Usage: srunner [-s] [-c] [modules] -r [regexp]
+ Usage: srunner [-s] [modules] -r [regexp]
Options:
- -s, --silent supress output [boolean]
- -c, --cover files to output code coverage for [string]
- -r, --regexp regexp of files to test [string] [required]
+ -s, --silent supress output [boolean]
+ -r, --regexp regexp of files to test [string] [required]
Which looks like this in the command line:
- srunner -c lib/foobar.js -r test/test-.*\.js
+ srunner -r test/test-.*\.js
If you prefer not to use `srunner`, you can
still run tests like this:
View
@@ -1,131 +0,0 @@
-/*
- * Copyright (c) 2011, Chris Dickinson
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- * * Neither the name of Chris Dickinson nor the
- * names of its contributors may be used to endorse or promote products
- * derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL Chris Dickinson BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-var bunker = require('bunker'),
- Module = require('module').Module,
- path = require('path'),
- fs = require('fs'),
- vm = require('vm');
-
-function CoverageData (filename, bunker, data) {
- this.bunker = bunker;
- this.filename = filename;
- this.filedata = data;
- this.nodes = {};
-}
-
-CoverageData.prototype.visit = function(node) {
- (this.nodes[node.id] = this.nodes[node.id] || {node:node, count:0}).count++;
-};
-
-CoverageData.prototype.missing = function() {
-
- var nodes = this.nodes;
- return this.bunker.nodes.filter(function(node) { return !nodes[node.id] });
-};
-
-CoverageData.prototype.stats = function() {
- var missing = this.missing(),
- filedata = this.filedata.split('\n');
-
- var seenLines = [],
- lines =
- missing.sort(function(lhs, rhs) {
- return lhs.node[0].start.line < rhs.node[0].start.line ? -1 :
- lhs.node[0].start.line > rhs.node[0].start.line ? 1 :
- 0;
- }).filter(function(node) {
-
- var okay = (seenLines.indexOf(node.node[0].start.line) < 0);
- if(okay)
- seenLines.push(node.node[0].start.line);
- return okay;
-
- }).map(function(node, idx, all) {
- return {
- lineno:node.node[0].start.line + 1,
- source:function() { return filedata[node.node[0].start.line]; }
- };
- });
-
- return {
- percentage:(filedata.length-seenLines.length)/filedata.length,
- lines:lines,
- missing:seenLines.length,
- seen:(filedata.length-seenLines.length)
- };
-};
-
-var createEnvironment = function(module, filename) {
-
- var ctxt = {};
- for(var k in global)
- ctxt[k] = global[k];
-
- ctxt.require = require;
- ctxt.exports = module.exports;
- ctxt.__filename = filename;
- ctxt.__dirname = path.dirname(filename);
- ctxt.process = process;
- ctxt.console = console;
- ctxt.module = module;
- ctxt.global = ctxt;
-
- return ctxt;
-};
-
-module.exports = function(fileRegex) {
- var originalRequire = require.extensions['.js'],
- coverageData = {},
- match = fileRegex instanceof RegExp ? fileRegex :
- new RegExp(fileRegex ? fileRegex.replace(/\//g, '\\/').replace(/\./g, '\\.') : '.*');
-
- require.extensions['.js'] = function(module, filename) {
- if(!match.test(filename)) return originalRequire(module, filename);
-
- var context = createEnvironment(module, filename),
- data = fs.readFileSync(filename, 'utf8'),
- bunkerized = bunker(data),
- coverage = coverageData[filename] = new CoverageData(filename, bunkerized, data);
-
- bunkerized.on('node', coverage.visit.bind(coverage));
- bunkerized.assign(context);
-
- var wrapper = '(function(ctxt) { with(ctxt) { return '+Module.wrap(bunkerized.compile())+'; } })',
- compiledWrapper = vm.runInNewContext(wrapper, filename, context);
-
- var args = [context.exports, context.require, module, filename, context.__dirname];
- return compiledWrapper.apply(module.exports, args);
- };
-
- var retval = function(ready) { ready(coverageData) };
- retval.release = function() { require.extensions['.js'] = originalRequire };
-
- return retval;
-};
-
-module.exports.cover = module.exports;
View
@@ -6,17 +6,12 @@ var path = require("path"),
color = require("colors"),
optimist = require("optimist"),
argv = optimist
- .usage("Usage: $0 [-s] [-c] [modules] -r [regexp]")
+ .usage("Usage: $0 [-s] [modules] -r [regexp]")
.options("s",{
description: "supress output",
alias: "silent",
boolean: true
})
- .options("c",{
- description: "files to output code coverage for",
- alias: "cover",
- string: true
- })
.options("r",{
description: "regexp of files to test",
alias: "regexp",
@@ -29,12 +24,10 @@ var path = require("path"),
*
* -r: regexp of files to test
* -s: silent
- * -c: code coverage
*
*/
var silent = argv.s;
-var coverage = argv.c ? argv.c.split(",") : [];
var r = path.join(process.cwd(), argv.r);
var info = function(buffer, colr){
@@ -75,31 +68,3 @@ filtered.forEach(function(file){
info(stdout);
});
});
-
-if(coverage.length){
- var codecover = require("./coverage").cover(/.*/);
-
- coverage = coverage.map(function(file){ return path.join(process.cwd(), file); });
- coverage.forEach(function(file){ require(file); });
-
- filtered = filtered.map(function(file){ return dir+"/"+file; });
- filtered.forEach(function(file){ require(file); });
-
- var counter = 0;
- coverage.forEach(function(obj, i){
- counter++;
- codecover(function(cd){
- var stats = cd[obj].stats();
-
- var col = "magenta";
- info("Coverage for: "+path.basename(obj)+"\n", col);
- info("Lines seen: "+stats.seen+"\n", col);
- info("Lines missing: "+stats.missing+"\n", col);
- info("Percentage seen: "+Math.floor(stats.percentage*100)+"%\n", col);
- info("\n");
- counter--;
-
- if(counter === 0) codecover.release();
- });
- });
-}
View
@@ -1,5 +1,8 @@
var events = require("events"),
- color = require("colors");
+ fs = require("fs"),
+ path = require("path"),
+ color = require("colors"),
+ coverage = require("runforcover").cover(/.*/);
/*
* @class stest
@@ -17,6 +20,15 @@ function stest(){
this.promise = new events.EventEmitter();
this._queue = [];
+ this._coverqueue = [];
+
+ this._out = function(col, str){
+ if(!str)
+ process.stdout.write(col);
+ else
+ process.stdout.write(color[col](str));
+ }
+
this._addTime = function(func){
var now = Date.now();
func();
@@ -38,12 +50,12 @@ function stest(){
}
// write results
- process.stdout.write(color[c](result+" "+name+" - "+time+"ms\n"));
+ self._out(c, result+" "+name+" - "+time+"ms\n");
// print errs
errors.forEach(function(err){
- process.stdout.write(color.red(err.message+"\n"));
+ self._out("red", err.message+"\n");
if(err.type !== "stest")
- process.stdout.write(color.red(err.stack+"\n"));
+ self._out("red", err.stack+"\n");
});
}
}
@@ -120,6 +132,30 @@ stest.prototype.addCase = function(name, opts, test){
}
/*
+ * Allows stest to track function calls for
+ * code coverage
+ *
+ * @param {String} lib
+ *
+ * This method returns the same object as
+ * require(lib) except it allows stest to track
+ * which functions have been called and which
+ * haven't.
+ *
+ * @api public
+ */
+
+
+stest.prototype.cover = function(lib){
+
+ var realroot = path.dirname(module.parent.filename);
+ lib = path.resolve(realroot, lib);
+ this._coverqueue.push(lib);
+
+ return require(lib);
+}
+
+/*
* Runs the test cases synchronously
*
* All output is written to stdout.
@@ -190,6 +226,30 @@ stest.prototype.run = function(){
totalTime += self._addTime(function(){ teardown.apply(self.ctx,[errors]); });
//give a report
self._report(name, errors, totalTime);
+
+ coverage(function(cd){
+ self._coverqueue.forEach(function(lib){
+ var file = fs.readFileSync(lib).toString().split("\n");
+ var stats = cd[lib].stats();
+ var c = "magenta", err = "red", g = "green";
+
+ self._out("\n");
+ self._out(c, "Module: "+path.basename(lib)+"\n");
+ self._out(c, "Percentage seen: "+Math.floor(stats.percentage*100)+"%\n");
+ self._out(c, "Lines seen: "+stats.seen+"\n");
+ self._out(c, "Lines missed: "+stats.missing+"\n");
+
+ stats.lines.forEach(function(line){
+ self._out("\n");
+ self._out(g, line.lineno-1+": "+file[line.lineno-2]+"\n");
+ self._out(err, line.lineno+": "+line.source()+"\n");
+ self._out(g, line.lineno+1+": "+file[line.lineno]+"\n");
+ });
+
+ self._out("\n");
+ });
+ coverage.release();
+ });
}, (opts.timeout > 0 ? opts.timeout : 250));
});
}
View
@@ -16,7 +16,7 @@
},
"main": "lib/stest.js",
"scripts": {
- "test": "srunner -c lib/stest.js -r test/test-.*\\.js"
+ "test": "srunner -r test/test-.*\\.js"
},
"bin": {
"srunner": "bin/srunner.js"
@@ -27,7 +27,7 @@
"dependencies": {
"optimist": ">=0.2.8",
"colors": ">=0.6.0",
- "bunker": ">=0.1.1"
+ "runforcover": ">=0.0.1"
},
"devDependencies": {}
}
@@ -0,0 +1,6 @@
+exports.something = function(a){
+ if(a > 0)
+ a = 5;
+ else
+ a = -5;
+}
View
@@ -0,0 +1,15 @@
+var assert = require("assert"),
+ stest = require("../lib/stest.js"),
+ cov = stest.cover("./fixtures/coverage.js");
+
+// defaults to 250 ms
+var opts = { timeout: 0 };
+
+stest
+.addCase("stest - code coverage", opts, {
+ setup: function(promise){
+ cov.something(1);
+ cov.something(-1);
+ },
+})
+.run();

0 comments on commit 0462253

Please sign in to comment.