UNPKG

compound-ex4

Version:

Compound-ex4 - MVC framework for NodeJS (ExpressJs 4 version), fork compoundjs(https://github.com/1602/compound)

503 lines (449 loc) 14.6 kB
var fs = require("fs") , child_process = require("child_process") , path = require("path") , chain = require("slide").chain , 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") , glob = require("glob") // XXX Clean up the coverage options , doCoverage = process.env.TAP_COV || process.env.npm_package_config_coverage || process.env.npm_config_coverage module.exports = Runner inherits(Runner, TapProducer) function Runner (options, cb) { this.options = options var diag = this.options.diag var dir = this.options.argv.remain TapProducer.call(this, diag) 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 = this.options["coverage-dir"] // 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 = this.options.cover if (doCoverage) { var mkdirp = require("mkdirp") this.coverageOutDir = path.resolve(this.coverageOutDir) this.getFilesToCover(filesToCover) var self = this return mkdirp(this.coverageOutDir, '0755', function (er) { if (er) return self.emit("error", er) self.run(dir, cb) }) } this.run(dir, cb) } } Runner.prototype.run = function() { var self = this , args = Array.prototype.slice.call(arguments) , cb = args.pop() || finish function finish (er) { if (er) { self.emit("error", er) } if (!doCoverage) return self.end() // 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() }) }) }) }) } if (Array.isArray(args[0])) { args = args[0] } self.runFiles(args, "", cb) } Runner.prototype.runDir = function (dir, cb) { var self = this fs.readdir(dir, function (er, files) { if (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.map(function(file) { return path.resolve(dir, file) }) self.runFiles(files, path.resolve(dir), cb) }) } // glob the filenames so that test/*.js works on windows Runner.prototype.runFiles = function (files, dir, cb) { var self = this var globRes = [] chain(files.map(function (f) { return function (cb) { glob(f, function (er, files) { if (er) return cb(er) globRes.push.apply(globRes, files) cb() }) } }), function (er) { if (er) return cb(er) runFiles(self, globRes, dir, cb) }) } // set some default options for node debugging tests function setOptionsForDebug(self) { // Note: we automatically increase the default timeout here. Yes // the user can specify --timeout to increase, but by default, // 30 seconds is not a long time to debug your test. self.options.timeout = 1000000; // Note: we automatically turn on stderr so user can see the 'debugger listening on port' message. // Without this it looks like tap has hung.. self.options.stderr = true; } function runFiles(self, files, dir, cb) { chain(files.map(function(f) { return function (cb) { if (self._bailedOut) return 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() } var cmd = f, args = [], env = {} if (path.extname(f) === ".js") { cmd = process.execPath if (self.options.gc) { args.push("--expose-gc") } if (self.options.debug) { args.push("--debug") setOptionsForDebug(self) } if (self.options["debug-brk"]) { args.push("--debug-brk") setOptionsForDebug(self) } if (self.options.strict) { args.push("--use-strict") } if (self.options.harmony) { args.push("--harmony") } args.push(fileName) } else if (path.extname(f) === ".coffee") { cmd = "coffee" args.push(fileName) } else { // Check if file is executable if ((st.mode & parseInt('0100', 8)) && process.getuid) { if (process.getuid() != st.uid) { return cb() } } else if ((st.mode & parseInt('0010', 8)) && process.getgid) { if (process.getgid() != st.gid) { return cb() } } else if ((st.mode & parseInt('0001', 8)) == 0) { return cb() } cmd = path.resolve(cmd) } if (st.isDirectory()) { return self.runDir(f, cb) } 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) fs.writeFileSync(tmpFname, fcontents, "utf8") args.splice(-1, 1, tmpFname) } for (var 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] var timeout = setTimeout(function () { if (!cp._ended) { cp._timedOut = true cp.kill() } }, self.options.timeout * 1000) tc.on("data", function(c) { self.emit("result", c) self.write(c) }) tc.on("bailout", function (message) { clearTimeout(timeout) console.log("# " + f.substr(process.cwd().length + 1)) process.stderr.write(err) process.stdout.write(out + "\n") self._bailedOut = true cp._ended = true cp.kill() }) cp.stdout.pipe(tc) cp.stdout.on("data", function (c) { out += c }) cp.stderr.on("data", function (c) { if (self.options.stderr) process.stderr.write(c) err += c }) cp.on("close", function (code, signal) { if (cp._ended) return cp._ended = true var ok = !cp._timedOut && code === 0 clearTimeout(timeout) //childTests.forEach(function (c) { self.write(c) }) var res = { name: path.dirname(f).replace(process.cwd() + "/", "") + "/" + fileName , ok: ok , exit: code } if (cp._timedOut) res.timedOut = cp._timedOut if (signal) res.signal = signal if (err) { res.stderr = err if (tc.results.ok && tc.results.tests === 0 && !self.options.stderr) { // perhaps a compilation error or something else failed. // no need if stderr is set, since it will have been // output already anyway. console.error(err) } } // tc.results.ok = tc.results.ok && ok tc.results.add(res) res.command = '"'+[cmd].concat(args).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) return self } // 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) { var existsSync = fs.existsSync || path.existsSync; return 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) { cb(err) } 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)) }) } }) }) // 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() }