Name: js-handler/node_modules/nodeunit/node_modules/tap/lib/tap-runner.js
| 1: | var fs = require("fs") |
| 2: | , child_process = require("child_process") |
| 3: | , path = require("path") |
| 4: | , chain = require("slide").chain |
| 5: | , asyncMap = require("slide").asyncMap |
| 6: | , TapProducer = require("./tap-producer.js") |
| 7: | , TapConsumer = require("./tap-consumer.js") |
| 8: | , assert = require("./tap-assert.js") |
| 9: | , inherits = require("inherits") |
| 10: | , util = require("util") |
| 11: | , CovHtml = require("./tap-cov-html.js") |
| 12: | , glob = require("glob") |
| 13: | |
| 14: | // XXX Clean up the coverage options |
| 15: | , doCoverage = process.env.TAP_COV |
| 16: | || process.env.npm_package_config_coverage |
| 17: | || process.env.npm_config_coverage |
| 18: | |
| 19: | module.exports = Runner |
| 20: | |
| 21: | inherits(Runner, TapProducer) |
| 22: | |
| 23: | function Runner (options, cb) { |
| 24: | this.options = options |
| 25: | |
| 26: | var diag = this.options.diag |
| 27: | var dir = this.options.argv.remain |
| 28: | TapProducer.call(this, diag) |
| 29: | |
| 30: | this.doCoverage = doCoverage |
| 31: | // An array of full paths to files to obtain coverage |
| 32: | this.coverageFiles = [] |
| 33: | // The source of these files |
| 34: | this.coverageFilesSource = {} |
| 35: | // Where to write coverage information |
| 36: | this.coverageOutDir = this.options["coverage-dir"] |
| 37: | // Temporary test files bunkerified we'll remove later |
| 38: | this.f2delete = [] |
| 39: | // Raw coverage stats, as read from JSON files |
| 40: | this.rawCovStats = [] |
| 41: | // Processed coverage information, per file to cover: |
| 42: | this.covStats = {} |
| 43: | |
| 44: | if (dir) { |
| 45: | var filesToCover = this.options.cover |
| 46: | |
| 47: | if (doCoverage) { |
| 48: | var mkdirp = require("mkdirp") |
| 49: | this.coverageOutDir = path.resolve(this.coverageOutDir) |
| 50: | this.getFilesToCover(filesToCover) |
| 51: | var self = this |
| 52: | return mkdirp(this.coverageOutDir, 0755, function (er) { |
| 53: | if (er) return self.emit("error", er) |
| 54: | self.run(dir, cb) |
| 55: | }) |
| 56: | } |
| 57: | |
| 58: | this.run(dir, cb) |
| 59: | } |
| 60: | } |
| 61: | |
| 62: | |
| 63: | Runner.prototype.run = function() { |
| 64: | var self = this |
| 65: | , args = Array.prototype.slice.call(arguments) |
| 66: | , cb = args.pop() || finish |
| 67: | |
| 68: | function finish (er) { |
| 69: | if (er) { |
| 70: | self.emit("error", er) |
| 71: | } |
| 72: | |
| 73: | if (!doCoverage) return self.end() |
| 74: | |
| 75: | // Cleanup temporary test files with coverage: |
| 76: | self.f2delete.forEach(function(f) { |
| 77: | fs.unlinkSync(f) |
| 78: | }) |
| 79: | self.getFilesToCoverSource(function(err, data) { |
| 80: | if (err) { |
| 81: | self.emit("error", err) |
| 82: | } |
| 83: | self.getPerFileCovInfo(function(err, data) { |
| 84: | if (err) { |
| 85: | self.emit("error", err) |
| 86: | } |
| 87: | self.mergeCovStats(function(err, data) { |
| 88: | if (err) { |
| 89: | self.emit("error", err) |
| 90: | } |
| 91: | CovHtml(self.covStats, self.coverageOutDir, function() { |
| 92: | self.end() |
| 93: | }) |
| 94: | }) |
| 95: | }) |
| 96: | }) |
| 97: | } |
| 98: | |
| 99: | if (Array.isArray(args[0])) { |
| 100: | args = args[0] |
| 101: | } |
| 102: | self.runFiles(args, "", cb) |
| 103: | } |
| 104: | |
| 105: | Runner.prototype.runDir = function (dir, cb) { |
| 106: | var self = this |
| 107: | fs.readdir(dir, function (er, files) { |
| 108: | if (er) { |
| 109: | self.write(assert.fail("failed to readdir " + dir, { error: er })) |
| 110: | self.end() |
| 111: | return |
| 112: | } |
| 113: | files = files.sort(function(a, b) { |
| 114: | return a > b ? 1 : -1 |
| 115: | }) |
| 116: | files = files.filter(function(f) { |
| 117: | return !f.match(/^\./) |
| 118: | }) |
| 119: | files = files.map(function(file) { |
| 120: | return path.resolve(dir, file) |
| 121: | }) |
| 122: | |
| 123: | self.runFiles(files, path.resolve(dir), cb) |
| 124: | }) |
| 125: | } |
| 126: | |
| 127: | |
| 128: | // glob the filenames so that test/*.js works on windows |
| 129: | Runner.prototype.runFiles = function (files, dir, cb) { |
| 130: | var self = this |
| 131: | var globRes = [] |
| 132: | chain(files.map(function (f) { |
| 133: | return function (cb) { |
| 134: | glob(f, function (er, files) { |
| 135: | if (er) |
| 136: | return cb(er) |
| 137: | globRes.push.apply(globRes, files) |
| 138: | cb() |
| 139: | }) |
| 140: | } |
| 141: | }), function (er) { |
| 142: | if (er) |
| 143: | return cb(er) |
| 144: | runFiles(self, globRes, dir, cb) |
| 145: | }) |
| 146: | } |
| 147: | |
| 148: | function runFiles(self, files, dir, cb) { |
| 149: | chain(files.map(function(f) { |
| 150: | return function (cb) { |
| 151: | if (self._bailedOut) return |
| 152: | var relDir = dir || path.dirname(f) |
| 153: | , fileName = relDir === "." ? f : f.substr(relDir.length + 1) |
| 154: | |
| 155: | self.write(fileName) |
| 156: | fs.lstat(f, function(er, st) { |
| 157: | if (er) { |
| 158: | self.write(assert.fail("failed to stat " + f, {error: er})) |
| 159: | return cb() |
| 160: | } |
| 161: | |
| 162: | var cmd = f, args = [], env = {} |
| 163: | |
| 164: | if (path.extname(f) === ".js") { |
| 165: | cmd = "node" |
| 166: | if (self.options.gc) { |
| 167: | args.push("--expose-gc") |
| 168: | } |
| 169: | args.push(fileName) |
| 170: | } else if (path.extname(f) === ".coffee") { |
| 171: | cmd = "coffee" |
| 172: | args.push(fileName) |
| 173: | } else { |
| 174: | // Check if file is executable |
| 175: | if ((st.mode & 0100) && process.getuid) { |
| 176: | if (process.getuid() != st.uid) { |
| 177: | return cb() |
| 178: | } |
| 179: | } else if ((st.mode & 0010) && process.getgid) { |
| 180: | if (process.getgid() != st.gid) { |
| 181: | return cb() |
| 182: | } |
| 183: | } else if ((st.mode & 0001) == 0) { |
| 184: | return cb() |
| 185: | } |
| 186: | } |
| 187: | |
| 188: | if (st.isDirectory()) { |
| 189: | return self.runDir(f, cb) |
| 190: | } |
| 191: | |
| 192: | if (doCoverage && path.extname(f) === ".js") { |
| 193: | var foriginal = fs.readFileSync(f, "utf8") |
| 194: | , fcontents = self.coverHeader() + foriginal + self.coverFooter() |
| 195: | , tmpBaseName = path.basename(f, path.extname(f)) |
| 196: | + ".with-coverage." + process.pid + path.extname(f) |
| 197: | , tmpFname = path.resolve(path.dirname(f), tmpBaseName) |
| 198: | |
| 199: | fs.writeFileSync(tmpFname, fcontents, "utf8") |
| 200: | args.splice(-1, 1, tmpFname) |
| 201: | } |
| 202: | |
| 203: | for (var i in process.env) { |
| 204: | env[i] = process.env[i] |
| 205: | } |
| 206: | env.TAP = 1 |
| 207: | |
| 208: | var cp = child_process.spawn(cmd, args, { env: env, cwd: relDir }) |
| 209: | , out = "" |
| 210: | , err = "" |
| 211: | , tc = new TapConsumer() |
| 212: | , childTests = [f] |
| 213: | |
| 214: | var timeout = setTimeout(function () { |
| 215: | if (!cp._ended) { |
| 216: | cp._timedOut = true |
| 217: | cp.kill() |
| 218: | } |
| 219: | }, self.options.timeout * 1000) |
| 220: | |
| 221: | tc.on("data", function(c) { |
| 222: | self.emit("result", c) |
| 223: | self.write(c) |
| 224: | }) |
| 225: | |
| 226: | tc.on("bailout", function (message) { |
| 227: | clearTimeout(timeout) |
| 228: | console.log("# " + f.substr(process.cwd().length + 1)) |
| 229: | process.stderr.write(err) |
| 230: | process.stdout.write(out + "\n") |
| 231: | self._bailedOut = true |
| 232: | cp._ended = true |
| 233: | cp.kill() |
| 234: | }) |
| 235: | |
| 236: | cp.stdout.pipe(tc) |
| 237: | cp.stdout.on("data", function (c) { out += c }) |
| 238: | cp.stderr.on("data", function (c) { |
| 239: | if (self.options.stderr) process.stderr.write(c) |
| 240: | err += c |
| 241: | }) |
| 242: | |
| 243: | cp.on("close", function (code, signal) { |
| 244: | if (cp._ended) return |
| 245: | cp._ended = true |
| 246: | var ok = !cp._timedOut && code === 0 |
| 247: | clearTimeout(timeout) |
| 248: | //childTests.forEach(function (c) { self.write(c) }) |
| 249: | var res = { name: path.dirname(f).replace(process.cwd() + "/", "") |
| 250: | + "/" + fileName |
| 251: | , ok: ok |
| 252: | , exit: code } |
| 253: | |
| 254: | if (cp._timedOut) |
| 255: | res.timedOut = cp._timedOut |
| 256: | if (signal) |
| 257: | res.signal = signal |
| 258: | |
| 259: | if (err) { |
| 260: | res.stderr = err |
| 261: | if (tc.results.ok && |
| 262: | tc.results.tests === 0 && |
| 263: | !self.options.stderr) { |
| 264: | // perhaps a compilation error or something else failed. |
| 265: | // no need if stderr is set, since it will have been |
| 266: | // output already anyway. |
| 267: | console.error(err) |
| 268: | } |
| 269: | } |
| 270: | |
| 271: | // tc.results.ok = tc.results.ok && ok |
| 272: | tc.results.add(res) |
| 273: | res.command = [cmd].concat(args).map(JSON.stringify).join(" ") |
| 274: | self.emit("result", res) |
| 275: | self.emit("file", f, res, tc.results) |
| 276: | self.write(res) |
| 277: | self.write("\n") |
| 278: | if (doCoverage) { |
| 279: | self.f2delete.push(tmpFname) |
| 280: | } |
| 281: | cb() |
| 282: | }) |
| 283: | }) |
| 284: | } |
| 285: | }), cb) |
| 286: | |
| 287: | return self |
| 288: | } |
| 289: | |
| 290: | |
| 291: | // Get an array of full paths to files we are interested into obtain |
| 292: | // code coverage. |
| 293: | Runner.prototype.getFilesToCover = function(filesToCover) { |
| 294: | var self = this |
| 295: | filesToCover = filesToCover.split(",").map(function(f) { |
| 296: | return path.resolve(f) |
| 297: | }).filter(function(f) { |
| 298: | return path.existsSync(f) |
| 299: | }) |
| 300: | |
| 301: | function recursive(f) { |
| 302: | if (path.extname(f) === "") { |
| 303: | // Is a directory: |
| 304: | fs.readdirSync(f).forEach(function(p) { |
| 305: | recursive(f + "/" + p) |
| 306: | }) |
| 307: | } else { |
| 308: | self.coverageFiles.push(f) |
| 309: | } |
| 310: | } |
| 311: | filesToCover.forEach(function(f) { |
| 312: | recursive(f) |
| 313: | }) |
| 314: | } |
| 315: | |
| 316: | // Prepend to every test file to run. Note tap.test at the very top due it |
| 317: | // "plays" with include paths. |
| 318: | Runner.prototype.coverHeader = function() { |
| 319: | // semi here since we're injecting it before the first line, |
| 320: | // and don't want to mess up line numbers in the test files. |
| 321: | return "var ___TAP_COVERAGE = require(" |
| 322: | + JSON.stringify(require.resolve("runforcover")) |
| 323: | + ").cover(/.*/g);" |
| 324: | } |
| 325: | |
| 326: | // Append at the end of every test file to run. Actually, the stuff which gets |
| 327: | // the coverage information. |
| 328: | // Maybe it would be better to move into a separate file template so editing |
| 329: | // could be easier. |
| 330: | Runner.prototype.coverFooter = function() { |
| 331: | var self = this |
| 332: | // This needs to be a string with proper interpolations: |
| 333: | return [ "" |
| 334: | , "var ___TAP = require(" + JSON.stringify(require.resolve("./main.js")) + ")" |
| 335: | , "if (typeof ___TAP._plan === 'number') ___TAP._plan ++" |
| 336: | , "___TAP.test(" + JSON.stringify("___coverage") + ", function(t) {" |
| 337: | , " var covFiles = " + JSON.stringify(self.coverageFiles) |
| 338: | , " , covDir = " + JSON.stringify(self.coverageOutDir) |
| 339: | , " , path = require('path')" |
| 340: | , " , fs = require('fs')" |
| 341: | , " , testFnBase = path.basename(__filename, '.js') + '.json'" |
| 342: | , " , testFn = path.resolve(covDir, testFnBase)" |
| 343: | , "" |
| 344: | , " function asyncForEach(arr, fn, callback) {" |
| 345: | , " if (!arr.length) {" |
| 346: | , " return callback()" |
| 347: | , " }" |
| 348: | , " var completed = 0" |
| 349: | , " arr.forEach(function(i) {" |
| 350: | , " fn(i, function (err) {" |
| 351: | , " if (err) {" |
| 352: | , " callback(err)" |
| 353: | , " callback = function () {}" |
| 354: | , " } else {" |
| 355: | , " completed += 1" |
| 356: | , " if (completed === arr.length) {" |
| 357: | , " callback()" |
| 358: | , " }" |
| 359: | , " }" |
| 360: | , " })" |
| 361: | , " })" |
| 362: | , " }" |
| 363: | , "" |
| 364: | , " ___TAP_COVERAGE(function(coverageData) {" |
| 365: | , " var outObj = {}" |
| 366: | , " asyncForEach(covFiles, function(f, cb) {" |
| 367: | , " if (coverageData[f]) {" |
| 368: | , " var stats = coverageData[f].stats()" |
| 369: | , " , stObj = stats" |
| 370: | , " stObj.lines = stats.lines.map(function (l) {" |
| 371: | , " return { number: l.lineno, source: l.source() }" |
| 372: | , " })" |
| 373: | , " outObj[f] = stObj" |
| 374: | , " }" |
| 375: | , " cb()" |
| 376: | , " }, function(err) {" |
| 377: | , " ___TAP_COVERAGE.release()" |
| 378: | , " fs.writeFileSync(testFn, JSON.stringify(outObj))" |
| 379: | , " t.end()" |
| 380: | , " })" |
| 381: | , " })" |
| 382: | , "})" ].join("\n") |
| 383: | } |
| 384: | |
| 385: | |
| 386: | Runner.prototype.getFilesToCoverSource = function(cb) { |
| 387: | var self = this |
| 388: | asyncMap(self.coverageFiles, function(f, cb) { |
| 389: | fs.readFile(f, "utf8", function(err, data) { |
| 390: | var lc = 0 |
| 391: | if (err) { |
| 392: | cb(err) |
| 393: | } |
| 394: | self.coverageFilesSource[f] = data.split("\n").map(function(l) { |
| 395: | lc += 1 |
| 396: | return { number: lc, source: l } |
| 397: | }) |
| 398: | cb() |
| 399: | }) |
| 400: | }, cb) |
| 401: | } |
| 402: | |
| 403: | Runner.prototype.getPerFileCovInfo = function(cb) { |
| 404: | var self = this |
| 405: | , covPath = path.resolve(self.coverageOutDir) |
| 406: | |
| 407: | fs.readdir(covPath, function(err, files) { |
| 408: | if (err) { |
| 409: | self.emit("error", err) |
| 410: | } |
| 411: | var covFiles = files.filter(function(f) { |
| 412: | return path.extname(f) === ".json" |
| 413: | }) |
| 414: | asyncMap(covFiles, function(f, cb) { |
| 415: | fs.readFile(path.resolve(covPath, f), "utf8", function(err, data) { |
| 416: | if (err) { |
| 417: | cb(err) |
| 418: | } |
| 419: | self.rawCovStats.push(JSON.parse(data)) |
| 420: | cb() |
| 421: | }) |
| 422: | }, function(f, cb) { |
| 423: | fs.unlink(path.resolve(covPath, f), cb) |
| 424: | }, cb) |
| 425: | }) |
| 426: | } |
| 427: | |
| 428: | Runner.prototype.mergeCovStats = function(cb) { |
| 429: | var self = this |
| 430: | self.rawCovStats.forEach(function(st) { |
| 431: | Object.keys(st).forEach(function(i) { |
| 432: | // If this is the first time we reach this file, just add the info: |
| 433: | if (!self.covStats[i]) { |
| 434: | self.covStats[i] = { |
| 435: | missing: st[i].lines |
| 436: | } |
| 437: | } else { |
| 438: | // If we already added info for this file before, we need to remove |
| 439: | // from self.covStats any line not duplicated again (since it has |
| 440: | // run on such case) |
| 441: | self.covStats[i].missing = self.covStats[i].missing.filter( |
| 442: | function(l) { |
| 443: | return (st[i].lines.indexOf(l)) |
| 444: | }) |
| 445: | } |
| 446: | }) |
| 447: | }) |
| 448: | |
| 449: | // This is due to a bug into |
| 450: | // chrisdickinson/node-bunker/blob/feature/add-coverage-interface |
| 451: | // which is using array indexes for line numbers instead of the right number |
| 452: | Object.keys(self.covStats).forEach(function(f) { |
| 453: | self.covStats[f].missing = self.covStats[f].missing.map(function(line) { |
| 454: | return { number: line.number, source: line.source } |
| 455: | }) |
| 456: | }) |
| 457: | |
| 458: | Object.keys(self.coverageFilesSource).forEach(function(f) { |
| 459: | if (!self.covStats[f]) { |
| 460: | self.covStats[f] = { missing: self.coverageFilesSource[f] |
| 461: | , percentage: 0 |
| 462: | } |
| 463: | } |
| 464: | self.covStats[f].lines = self.coverageFilesSource[f] |
| 465: | self.covStats[f].loc = self.coverageFilesSource[f].length |
| 466: | |
| 467: | if (!self.covStats[f].percentage) { |
| 468: | self.covStats[f].percentage = |
| 469: | 1 - (self.covStats[f].missing.length / self.covStats[f].loc) |
| 470: | } |
| 471: | |
| 472: | }) |
| 473: | cb() |
| 474: | } |
