Comparing version 0.1.0 to 0.1.1
@@ -15,3 +15,3 @@ #!/usr/bin/env node | ||
, n = details.pass + "/" + details.testsTotal | ||
, dots = new Array(Math.max(1, 40 - s.length - n.length)).join(".") | ||
, dots = new Array(Math.max(1, 60 - s.length - n.length)).join(".") | ||
console.log("%s %s %s", s, dots, n) | ||
@@ -33,5 +33,9 @@ if (details.ok) { | ||
, n = r.results.pass + "/" + r.results.testsTotal | ||
, dots = new Array(40 - s.length - n.length).join(".") | ||
, dots = new Array(60 - s.length - n.length).join(".") | ||
, ok = r.results.ok ? "ok" : "not ok" | ||
console.log("%s %s %s\n\n%s", s, dots, n, ok) | ||
if (r.doCoverage) { | ||
console.error( "\nCoverage: %s\n" | ||
, path.resolve(r.coverageOutDir, "index.html") ) | ||
} | ||
// process.stdout.flush() | ||
@@ -38,0 +42,0 @@ }) |
@@ -91,3 +91,7 @@ // a thing that runs tests. | ||
//console.error("Harness process: no more left. ending") | ||
this.end() | ||
if (this._endNice) { | ||
this._endNice() | ||
} else { | ||
this.end() | ||
} | ||
} | ||
@@ -97,2 +101,5 @@ } | ||
Harness.prototype.end = function () { | ||
if (this._children.length) { | ||
return this.process() | ||
} | ||
//console.error("harness end", this.constructor.name) | ||
@@ -147,2 +154,8 @@ if (this._bailedOut) return | ||
function copyObj(o) { | ||
var copied = {} | ||
Object.keys(o).forEach(function (k) { copied[k] = o[k] }) | ||
return copied | ||
} | ||
Harness.prototype.test = function test (name, conf, cb) { | ||
@@ -155,3 +168,3 @@ if (this._bailedOut) return | ||
conf = conf || {} | ||
conf = (conf ? copyObj(conf) : {}) | ||
name = name || "" | ||
@@ -158,0 +171,0 @@ |
@@ -1,3 +0,1 @@ | ||
module.exports = Runner | ||
var fs = require("fs") | ||
@@ -7,23 +5,103 @@ , child_process = require("child_process") | ||
, chain = require("slide").chain | ||
, TapProducer = require("./tap-producer") | ||
, TapConsumer = require("./tap-consumer") | ||
, assert = require("./tap-assert") | ||
, asyncMap = require("slide").asyncMap | ||
, TapProducer = require("./tap-producer.js") | ||
, TapConsumer = require("./tap-consumer.js") | ||
, assert = require("./tap-assert.js") | ||
, inherits = require("inherits") | ||
, util = require("util") | ||
, CovHtml = require("./tap-cov-html.js") | ||
, doCoverage = process.env.TAP_COV | ||
|| process.env.npm_package_config_coverage | ||
|| process.env.npm_config_coverage | ||
inherits(Runner, TapProducer) | ||
function Runner (dir, diag, cb) { | ||
var Runner = module.exports = function (dir, diag, cb) { | ||
Runner.super.call(this, diag) | ||
if (dir) this.run(dir, cb) | ||
this.doCoverage = doCoverage | ||
// An array of full paths to files to obtain coverage | ||
this.coverageFiles = [] | ||
// The source of these files | ||
this.coverageFilesSource = {} | ||
// Where to write coverage information | ||
this.coverageOutDir = "./coverage" | ||
// Temporary test files bunkerified we'll remove later | ||
this.f2delete = [] | ||
// Raw coverage stats, as read from JSON files | ||
this.rawCovStats = [] | ||
// Processed coverage information, per file to cover: | ||
this.covStats = {} | ||
if (dir) { | ||
var filesToCover = "./lib" | ||
, coverageOutDir = "./coverage" | ||
if (doCoverage) { | ||
dir = dir.filter(function(arg) { | ||
if (arg.match(/^--cover=/)) { | ||
filesToCover = arg.split("--cover=")[1] | ||
return false | ||
} else if (arg.match(/^--cover-dir=/)) { | ||
coverageOutDir = arg.split("--cover-dir=")[1] | ||
return false | ||
} | ||
return true | ||
}) | ||
coverageOutDir = path.resolve(coverageOutDir) | ||
path.exists(coverageOutDir, function(exists) { | ||
if (!exists) { | ||
fs.mkdir(coverageOutDir, 0755, function(er) { | ||
if (er) { | ||
throw er | ||
} | ||
}) | ||
} | ||
}) | ||
this.coverageOutDir = coverageOutDir | ||
this.getFilesToCover(filesToCover) | ||
} | ||
this.run(dir, cb) | ||
} | ||
} | ||
Runner.prototype.run = function () { | ||
inherits(Runner, TapProducer) | ||
Runner.prototype.run = function() { | ||
var self = this | ||
, args = Array.prototype.slice.call(arguments) | ||
, cb = args.pop() || function (er) { | ||
if (er) self.emit("error", er) | ||
self.end() | ||
if (er) { | ||
self.emit("error", er) | ||
} | ||
if (doCoverage) { | ||
// Cleanup temporary test files with coverage: | ||
self.f2delete.forEach(function(f) { | ||
fs.unlinkSync(f) | ||
}) | ||
self.getFilesToCoverSource(function(err, data) { | ||
if (err) { | ||
self.emit("error", err) | ||
} | ||
self.getPerFileCovInfo(function(err, data) { | ||
if (err) { | ||
self.emit("error", err) | ||
} | ||
self.mergeCovStats(function(err, data) { | ||
if (err) { | ||
self.emit("error", err) | ||
} | ||
CovHtml(self.covStats, self.coverageOutDir, function() { | ||
self.end() | ||
}) | ||
}) | ||
}) | ||
}) | ||
} else { | ||
self.end() | ||
} | ||
} | ||
if (Array.isArray(args[0])) args = args[0] | ||
if (Array.isArray(args[0])) { | ||
args = args[0] | ||
} | ||
self.runFiles(args, "", cb) | ||
@@ -36,9 +114,12 @@ } | ||
if (er) { | ||
self.write(assert.fail("failed to readdir "+dir, | ||
{ error: er })) | ||
self.write(assert.fail("failed to readdir " + dir, { error: er })) | ||
self.end() | ||
return | ||
} | ||
files = files.sort(function (a,b) {return a>b ? 1 : -1}) | ||
files = files.filter(function (f) {return !f.match(/^\./)}) | ||
files = files.sort(function(a, b) { | ||
return a > b ? 1 : -1 | ||
}) | ||
files = files.filter(function(f) { | ||
return !f.match(/^\./) | ||
}) | ||
files = files.map(path.resolve.bind(path, dir)) | ||
@@ -50,72 +131,288 @@ | ||
Runner.prototype.runFiles = function (files, dir, cb) { | ||
var self = this | ||
chain(files.map(function (f) { return function (cb) { | ||
var relDir = dir || path.dirname(f) | ||
, fileName = relDir === "." ? f : f.substr(relDir.length + 1) | ||
chain(files.map(function(f) { | ||
return function (cb) { | ||
var relDir = dir || path.dirname(f) | ||
, fileName = relDir === "." ? f : f.substr(relDir.length + 1) | ||
self.write(fileName) | ||
fs.lstat(f, function (er, st) { | ||
if (er) { | ||
self.write(assert.fail("failed to stat "+f, | ||
{error: er})) | ||
return cb() | ||
} | ||
self.write(fileName) | ||
fs.lstat(f, function(er, st) { | ||
if (er) { | ||
self.write(assert.fail("failed to stat " + f, {error: er})) | ||
return cb() | ||
} | ||
var cmd = f | ||
, args = [] | ||
var cmd = f, args = [], env = {} | ||
if (path.extname(f) === ".js") { | ||
cmd = "node" | ||
args = [fileName] | ||
} else if (path.extname(f) === ".coffee") { | ||
cmd = "coffee" | ||
args = [fileName] | ||
} | ||
if (st.isDirectory()) { | ||
return self.runDir(f, cb) | ||
} | ||
if (path.extname(f) === ".js") { | ||
cmd = "node" | ||
args = [fileName] | ||
} else if (path.extname(f) === ".coffee") { | ||
cmd = "coffee" | ||
args = [fileName] | ||
} | ||
var env = {} | ||
for (var i in process.env) env[i] = process.env[i] | ||
env.TAP = 1 | ||
if (st.isDirectory()) { | ||
return self.runDir(f, cb) | ||
} | ||
var cp = child_process.spawn(cmd, args, { env: env, cwd: relDir }) | ||
, out = "" | ||
, err = "" | ||
, tc = new TapConsumer | ||
, childTests = [f] | ||
if (doCoverage && path.extname(f) === ".js") { | ||
var foriginal = fs.readFileSync(f, "utf8") | ||
, fcontents = self.coverHeader() + foriginal + self.coverFooter() | ||
, tmpBaseName = path.basename(f, path.extname(f)) | ||
+ ".with-coverage." + process.pid + path.extname(f) | ||
, tmpFname = path.resolve(path.dirname(f), tmpBaseName) | ||
, i | ||
tc.on("data", function (c) { | ||
self.emit("result", c) | ||
self.write(c) | ||
fs.writeFileSync(tmpFname, fcontents, "utf8") | ||
args = [tmpFname] | ||
} | ||
for (i in process.env) { | ||
env[i] = process.env[i] | ||
} | ||
env.TAP = 1 | ||
var cp = child_process.spawn(cmd, args, { env: env, cwd: relDir }) | ||
, out = "" | ||
, err = "" | ||
, tc = new TapConsumer() | ||
, childTests = [f] | ||
setTimeout(function () { | ||
if (!cp._ended) { | ||
cp.kill() | ||
console.error("killing: " + f + " (timed out)") | ||
} | ||
}, ((process.env.TAP_TIMEOUT || 30) * 1000)) | ||
tc.on("data", function(c) { | ||
self.emit("result", c) | ||
self.write(c) | ||
}) | ||
cp.stdout.pipe(tc) | ||
cp.stdout.on("data", function(c) { out += c }) | ||
cp.stderr.on("data", function(c) { err += c }) | ||
cp.on("exit", function(code) { | ||
cp._ended = true | ||
//childTests.forEach(function (c) { self.write(c) }) | ||
var res = { name: path.dirname(f).replace(process.cwd() + "/", "") | ||
+ "/" + fileName | ||
, ok: !code } | ||
if (err) { | ||
res.stderr = err | ||
if (tc.results.ok && tc.results.tests === 0) { | ||
// perhaps a compilation error or something else failed... | ||
console.error(err) | ||
} | ||
} | ||
res.command = [cmd].concat(args).map(JSON.stringify).join(" ") | ||
self.emit("result", res) | ||
self.emit("file", f, res, tc.results) | ||
self.write(res) | ||
self.write("\n") | ||
if (doCoverage) { | ||
self.f2delete.push(tmpFname) | ||
} | ||
cb() | ||
}) | ||
}) | ||
} | ||
}), cb) | ||
cp.stdout.pipe(tc) | ||
cp.stdout.on("data", function (c) { out += c }) | ||
cp.stderr.on("data", function (c) { err += c }) | ||
return self | ||
} | ||
cp.on("exit", function (code) { | ||
//childTests.forEach(function (c) { self.write(c) }) | ||
var res = { name: fileName | ||
, ok: !code } | ||
// Get an array of full paths to files we are interested into obtain | ||
// code coverage. | ||
Runner.prototype.getFilesToCover = function(filesToCover) { | ||
var self = this | ||
filesToCover = filesToCover.split(",").map(function(f) { | ||
return path.resolve(f) | ||
}).filter(function(f) { | ||
return path.existsSync(f) | ||
}) | ||
function recursive(f) { | ||
if (path.extname(f) === "") { | ||
// Is a directory: | ||
fs.readdirSync(f).forEach(function(p) { | ||
recursive(f + "/" + p) | ||
}) | ||
} else { | ||
self.coverageFiles.push(f) | ||
} | ||
} | ||
filesToCover.forEach(function(f) { | ||
recursive(f) | ||
}) | ||
} | ||
// Prepend to every test file to run. Note tap.test at the very top due it | ||
// "plays" with include paths. | ||
Runner.prototype.coverHeader = function() { | ||
// semi here since we're injecting it before the first line, | ||
// and don't want to mess up line numbers in the test files. | ||
return "var ___TAP_COVERAGE = require(" | ||
+ JSON.stringify(require.resolve("runforcover")) | ||
+ ").cover(/.*/g);" | ||
} | ||
// Append at the end of every test file to run. Actually, the stuff which gets | ||
// the coverage information. | ||
// Maybe it would be better to move into a separate file template so editing | ||
// could be easier. | ||
Runner.prototype.coverFooter = function() { | ||
var self = this | ||
// This needs to be a string with proper interpolations: | ||
return [ "" | ||
, "var ___TAP = require(" + JSON.stringify(require.resolve("./main.js")) + ")" | ||
, "if (typeof ___TAP._plan === 'number') ___TAP._plan ++" | ||
, "___TAP.test(" + JSON.stringify("___coverage") + ", function(t) {" | ||
, " var covFiles = " + JSON.stringify(self.coverageFiles) | ||
, " , covDir = " + JSON.stringify(self.coverageOutDir) | ||
, " , path = require('path')" | ||
, " , fs = require('fs')" | ||
, " , testFnBase = path.basename(__filename, '.js') + '.json'" | ||
, " , testFn = path.resolve(covDir, testFnBase)" | ||
, "" | ||
, " function asyncForEach(arr, fn, callback) {" | ||
, " if (!arr.length) {" | ||
, " return callback()" | ||
, " }" | ||
, " var completed = 0" | ||
, " arr.forEach(function(i) {" | ||
, " fn(i, function (err) {" | ||
, " if (err) {" | ||
, " callback(err)" | ||
, " callback = function () {}" | ||
, " } else {" | ||
, " completed += 1" | ||
, " if (completed === arr.length) {" | ||
, " callback()" | ||
, " }" | ||
, " }" | ||
, " })" | ||
, " })" | ||
, " }" | ||
, "" | ||
, " ___TAP_COVERAGE(function(coverageData) {" | ||
, " var outObj = {}" | ||
, " asyncForEach(covFiles, function(f, cb) {" | ||
, " if (coverageData[f]) {" | ||
, " var stats = coverageData[f].stats()" | ||
, " , stObj = stats" | ||
, " stObj.lines = stats.lines.map(function (l) {" | ||
, " return { number: l.lineno, source: l.source() }" | ||
, " })" | ||
, " outObj[f] = stObj" | ||
, " }" | ||
, " cb()" | ||
, " }, function(err) {" | ||
, " ___TAP_COVERAGE.release()" | ||
, " fs.writeFileSync(testFn, JSON.stringify(outObj))" | ||
, " t.end()" | ||
, " })" | ||
, " })" | ||
, "})" ].join("\n") | ||
} | ||
Runner.prototype.getFilesToCoverSource = function(cb) { | ||
var self = this | ||
asyncMap(self.coverageFiles, function(f, cb) { | ||
fs.readFile(f, "utf8", function(err, data) { | ||
var lc = 0 | ||
if (err) { | ||
cb(err) | ||
} | ||
self.coverageFilesSource[f] = data.split("\n").map(function(l) { | ||
lc += 1 | ||
return { number: lc, source: l } | ||
}) | ||
cb() | ||
}) | ||
}, cb) | ||
} | ||
Runner.prototype.getPerFileCovInfo = function(cb) { | ||
var self = this | ||
, covPath = path.resolve(self.coverageOutDir) | ||
fs.readdir(covPath, function(err, files) { | ||
if (err) { | ||
self.emit("error", err) | ||
} | ||
var covFiles = files.filter(function(f) { | ||
return path.extname(f) === ".json" | ||
}) | ||
asyncMap(covFiles, function(f, cb) { | ||
fs.readFile(path.resolve(covPath, f), "utf8", function(err, data) { | ||
if (err) { | ||
res.stderr = err | ||
if (tc.results.ok && tc.results.tests === 0) { | ||
// perhaps a compilation error or something else failed... | ||
console.error(err) | ||
} | ||
cb(err) | ||
} | ||
res.command = [cmd].concat(args).map(JSON.stringify).join(" ") | ||
self.emit("result", res) | ||
self.emit("file", f, res, tc.results) | ||
self.write(res) | ||
self.write("\n") | ||
self.rawCovStats.push(JSON.parse(data)) | ||
cb() | ||
}) | ||
}, function(f, cb) { | ||
fs.unlink(path.resolve(covPath, f), cb) | ||
}, cb) | ||
}) | ||
} | ||
Runner.prototype.mergeCovStats = function(cb) { | ||
var self = this | ||
self.rawCovStats.forEach(function(st) { | ||
Object.keys(st).forEach(function(i) { | ||
// If this is the first time we reach this file, just add the info: | ||
if (!self.covStats[i]) { | ||
self.covStats[i] = { | ||
missing: st[i].lines | ||
} | ||
} else { | ||
// If we already added info for this file before, we need to remove | ||
// from self.covStats any line not duplicated again (since it has | ||
// run on such case) | ||
self.covStats[i].missing = self.covStats[i].missing.filter( | ||
function(l) { | ||
return (st[i].lines.indexOf(l)) | ||
}) | ||
} | ||
}) | ||
}}), cb) | ||
}) | ||
return self | ||
// This is due to a bug into | ||
// chrisdickinson/node-bunker/blob/feature/add-coverage-interface | ||
// which is using array indexes for line numbers instead of the right number | ||
Object.keys(self.covStats).forEach(function(f) { | ||
self.covStats[f].missing = self.covStats[f].missing.map(function(line) { | ||
return { number: line.number, source: line.source } | ||
}) | ||
}) | ||
Object.keys(self.coverageFilesSource).forEach(function(f) { | ||
if (!self.covStats[f]) { | ||
self.covStats[f] = { missing: self.coverageFilesSource[f] | ||
, percentage: 0 | ||
} | ||
} | ||
self.covStats[f].lines = self.coverageFilesSource[f] | ||
self.covStats[f].loc = self.coverageFilesSource[f].length | ||
if (!self.covStats[f].percentage) { | ||
self.covStats[f].percentage = | ||
1 - (self.covStats[f].missing.length / self.covStats[f].loc) | ||
} | ||
}) | ||
cb() | ||
} | ||
@@ -88,2 +88,3 @@ // This is a very simple test framework that leverages the tap framework | ||
this.result(res) | ||
return res | ||
}} | ||
@@ -90,0 +91,0 @@ |
{ "name" : "tap" | ||
, "version" : "0.1.0" | ||
, "version" : "0.1.1" | ||
, "author" : "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)" | ||
@@ -11,2 +11,3 @@ , "description" : "A Test-Anything-Protocol library" | ||
, "slide": "*" | ||
, "runforcover": "0.0.1" | ||
} | ||
@@ -13,0 +14,0 @@ , "bundledDependencies" : |
@@ -47,3 +47,3 @@ This is a mix-and-match set of utilities that you can use to write test | ||
Node-tap is actually a collection of several packages, any of which may be | ||
Node-tap is actually a collection of several modules, any of which may be | ||
mixed and matched however you please. | ||
@@ -57,4 +57,2 @@ | ||
That matters. Or rather, it will, very soon. | ||
You can also use this to build programs that *consume* the TAP data, so | ||
@@ -79,3 +77,9 @@ this is very useful for CI systems and such. | ||
More docs coming soon, hopefully. | ||
## Experimental Code Coverage with runforcover & bunker: | ||
``` | ||
TAP_COV=1 tap ./tests [--cover=./lib,foo.js] [--cover-dir=./coverage] | ||
``` | ||
This feature is experimental, and will most likely change somewhat | ||
before being finalized. Feedback welcome. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
88095
47
2486
83
3
4
14
+ Addedrunforcover@0.0.1
+ Addedbunker@0.1.2(transitive)
+ Addedburrito@0.2.12(transitive)
+ Addedrunforcover@0.0.1(transitive)
+ Addedtraverse@0.5.2(transitive)
+ Addeduglify-js@1.1.1(transitive)