| // A facade from the tap-parser to the Mocha "Runner" object. |
| // Note that pass/fail/suite events need to also mock the "Runnable" |
| // objects (either "Suite" or "Test") since these have functions |
| // which are called by the formatters. |
| |
| module.exports = Runner |
| |
| // relevant events: |
| // |
| // start() |
| // Start of the top-level test set |
| // |
| // end() |
| // End of the top-level test set. |
| // |
| // fail(test, err) |
| // any "not ok" test that is not the trailing test for a suite |
| // of >0 test points. |
| // |
| // pass(test) |
| // any "ok" test point that is not the trailing test for a suite |
| // of >0 tests |
| // |
| // pending(test) |
| // Any "todo" test |
| // |
| // suite(suite) |
| // A suite is a child test with >0 test points. This is a little bit |
| // tricky, because TAP will provide a "child" event before we know |
| // that it's a "suite". We see the "# Subtest: name" comment as the |
| // first thing in the subtest. Then, when we get our first test point, |
| // we know that it's a suite, and can emit the event with the mock suite. |
| // |
| // suite end(suite) |
| // Emitted when we end the subtest |
| // |
| // test(test) |
| // Any test point which is not the trailing test for a suite. |
| // |
| // test end(test) |
| // Emitted immediately after the "test" event because test points are |
| // not async in TAP. |
| |
| var util = require('util') |
| var Test = require('./test.js') |
| var Suite = require('./suite.js') |
| var Writable = require('stream').Writable |
| if (!Writable) { |
| try { |
| Writable = require('readable-stream').Writable |
| } catch (er) { |
| throw new Error('Please install "readable-stream" to use this module ' + |
| 'with Node.js v0.8 and before') |
| } |
| } |
| |
| var Parser = require('tap-parser') |
| |
| // $1 = number, $2 = units |
| var timere = /^#\s*time=((?:0|[1-9][0-9]*?)(?:\.[0-9]+)?)(ms|s)?$/ |
| |
| util.inherits(Runner, Writable) |
| |
| function Runner (options) { |
| if (!(this instanceof Runner)) |
| return new Runner(options) |
| |
| var parser = this.parser = new Parser(options) |
| this.startTime = new Date() |
| |
| attachEvents(this, parser, 0) |
| Writable.call(this, options) |
| } |
| |
| Runner.prototype.write = function () { |
| if (!this.emittedStart) { |
| this.emittedStart = true |
| this.emit('start') |
| } |
| |
| return this.parser.write.apply(this.parser, arguments) |
| } |
| |
| Runner.prototype.end = function () { |
| return this.parser.end.apply(this.parser, arguments) |
| } |
| |
| Parser.prototype.fullTitle = function () { |
| if (!this.parent) |
| return this.name || '' |
| else |
| return (this.parent.fullTitle() + ' ' + (this.name || '')).trim() |
| } |
| |
| function attachEvents (runner, parser, level) { |
| parser.runner = runner |
| |
| if (level === 0) { |
| parser.on('line', function (c) { |
| runner.emit('line', c) |
| }) |
| parser.on('version', function (v) { |
| runner.emit('version', v) |
| }) |
| parser.on('complete', function (res) { |
| runner.emit('end') |
| }) |
| parser.on('comment', function (c) { |
| var tmatch = c.trim().match(timere) |
| if (tmatch) { |
| var t = +tmatch[1] |
| if (tmatch[2] === 's') |
| t *= 1000 |
| parser.time = t |
| if (runner.stats) |
| runner.stats.duration = t |
| } |
| }) |
| } |
| |
| parser.emittedSuite = false |
| parser.didAssert = false |
| parser.name = parser.name || '' |
| parser.doingChild = null |
| |
| parser.on('complete', function (res) { |
| if (!res.ok) { |
| var fail = { ok: false, diag: {} } |
| var count = res.count |
| if (res.plan) { |
| var plan = res.plan.end - res.plan.start + 1 |
| if (count !== plan) { |
| fail.name = 'test count !== plan' |
| fail.diag = { |
| found: count, |
| wanted: plan |
| } |
| } else { |
| // probably handled on child parser |
| return |
| } |
| } else { |
| fail.name = 'missing plan' |
| } |
| fail.diag.results = res |
| emitTest(parser, fail) |
| } |
| }) |
| |
| parser.on('child', function (child) { |
| child.parent = parser |
| attachEvents(runner, child, level + 1) |
| |
| // if we're in a suite, but we haven't emitted it yet, then we |
| // know that an assert will follow this child, even if there are |
| // no others. That means that we will definitely have a 'suite' |
| // event to emit. |
| emitSuite(this) |
| |
| this.didAssert = true |
| this.doingChild = child |
| }) |
| |
| if (!parser.name) { |
| parser.on('comment', function (c) { |
| if (!this.name && c.match(/^# Subtest: /)) { |
| c = c.trim().replace(/^# Subtest: /, '') |
| this.name = c |
| } |
| }) |
| } |
| |
| // Just dump all non-parsing stuff to stderr |
| parser.on('extra', function (c) { |
| process.stderr.write(c) |
| }) |
| |
| parser.on('assert', function (result) { |
| emitSuite(this) |
| |
| // no need to print the trailing assert for subtests |
| // we've already emitted a 'suite end' event for this. |
| // UNLESS, there were no other asserts, AND it's root level |
| if (this.doingChild) { |
| var suite = this.doingChild.suite |
| if (this.doingChild.name === result.name) { |
| if (suite) { |
| if (result.time) |
| suite.duration = result.time |
| |
| // If it's ok so far, but the ending result is not-ok, then |
| // that means that it exited non-zero. Emit the test so |
| // that we can print it as a failure. |
| if (suite.ok && !result.ok) |
| emitTest(this, result) |
| } |
| } |
| |
| var emitOn = this |
| var dc = this.doingChild |
| this.doingChild = null |
| |
| if (!dc.didAssert && dc.level === 1) { |
| emitOn = dc |
| } else if (dc.didAssert) { |
| if (dc.suite) |
| runner.emit('suite end', dc.suite) |
| return |
| } else { |
| emitOn = this |
| } |
| |
| emitSuite(emitOn) |
| emitTest(emitOn, result) |
| if (emitOn !== this && emitOn.suite) { |
| runner.emit('suite end', emitOn.suite) |
| delete emitOn.suite |
| } |
| if (dc.suite) { |
| runner.emit('suite end', dc.suite) |
| } |
| return |
| } |
| |
| this.didAssert = true |
| this.doingChild = null |
| |
| emitTest(this, result) |
| }) |
| |
| parser.on('complete', function (results) { |
| this.results = results |
| }) |
| |
| parser.on('bailout', function (reason) { |
| var suite = this.suite |
| runner.emit('bailout', reason, suite) |
| if (suite) |
| this.suite = suite.parent |
| }) |
| |
| // proxy all stream events directly |
| var streamEvents = [ |
| 'pipe', 'prefinish', 'finish', 'unpipe', 'close' |
| ] |
| |
| streamEvents.forEach(function (ev) { |
| parser.on(ev, function () { |
| var args = [ev] |
| args.push.apply(args, arguments) |
| runner.emit.apply(runner, args) |
| }) |
| }) |
| } |
| |
| function emitSuite (parser) { |
| if (!parser.emittedSuite && parser.name) { |
| parser.emittedSuite = true |
| var suite = parser.suite = new Suite(parser) |
| if (parser.parent && parser.parent.suite) |
| parser.parent.suite.suites.push(suite) |
| if (parser.runner.stats) |
| parser.runner.stats.suites ++ |
| |
| parser.runner.emit('suite', suite) |
| } |
| } |
| |
| function emitTest (parser, result) { |
| var runner = parser.runner |
| var test = new Test(result, parser) |
| |
| if (parser.suite) { |
| parser.suite.tests.push(test) |
| if (!result.ok) { |
| for (var p = parser; p && p.suite; p = p.parent) { |
| p.suite.ok = false |
| } |
| } |
| parser.suite.ok = parser.suite.ok && result.ok |
| } |
| |
| runner.emit('test', test) |
| if (result.skip || result.todo) { |
| runner.emit('pending', test) |
| } else if (result.ok) { |
| runner.emit('pass', test) |
| } else { |
| var error = getError(result) |
| runner.emit('fail', test, error) |
| } |
| runner.emit('test end', test) |
| } |
| |
| function getError (result) { |
| var err |
| |
| function reviveStack (stack) { |
| if (!stack) |
| return null |
| |
| return stack.trim().split('\n').map(function (line) { |
| return ' at ' + line |
| }).join('\n') |
| } |
| |
| if (result.diag && result.diag.error) { |
| err = { |
| name: result.diag.error.name || 'Error', |
| message: result.diag.error.message, |
| toString: function () { |
| return this.name + ': ' + this.message |
| }, |
| stack: result.diag.error.stack |
| } |
| } else { |
| err = { |
| message: (result.name || '(unnamed error)').replace(/^Error: /, ''), |
| toString: function () { |
| return 'Error: ' + this.message |
| }, |
| stack: result.diag && result.diag.stack |
| } |
| } |
| |
| var diag = result.diag |
| |
| if (err.stack) |
| err.stack = err.toString() + '\n' + reviveStack(err.stack) |
| |
| if (diag) { |
| var hasFound = diag.hasOwnProperty('found') |
| var hasWanted = diag.hasOwnProperty('wanted') |
| |
| if (hasFound) |
| err.actual = diag.found |
| |
| if (hasWanted) |
| err.expected = diag.wanted |
| |
| if (hasFound && hasWanted) |
| err.showDiff = true |
| } |
| |
| return err |
| } |
| |