blob: 4b5ea0f9ccfa4d880be6a750e6e2ffc9ff66fab9 [file] [log] [blame]
// 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
}