blob: 9968af45b168562ccb87800d96a8478aa34e3654 [file] [log] [blame]
'use strict'
// We need TWO queues (work and subtest) and one jobs pool
//
// The pool stores buffered subtests being run in parallel.
//
// When new subtests are created, they get put in the work queue and also
// in the subtests queue if they are buffered and jobs>0. When we put a
// test in the subtest queue, we also process it.
//
// Processing the subtest queue means moving tests into the jobs pool until
// the jobs pool length is at this.jobs
//
// Any output functions get put in the work queue if its length > 0 (ie,
// no cutting the line)
//
// Processing the work queue means walking until we run out of things, or
// encounter an unfinished test. When we encounter ANY kind of test, we
// block until its output is completed, dumping it all into the parser.
const Base = require('./base.js')
const Spawn = require('./spawn.js')
const Stdin = require('./stdin.js')
const Deferred = require('trivial-deferred')
const Pool = require('yapool')
const TestPoint = require('./point.js')
const parseTestArgs = require('./parse-test-args.js')
const loop = require('function-loop')
const path = require('path')
const extraFromError = require('./extra-from-error.js')
const tsame = require('tsame') // same thing, strict or not
const tmatch = require('tmatch') // ok with partial estimates
const stack = require('./stack.js')
const synonyms = require('./synonyms.js')
const assert = require('assert')
const util = require('util')
const ownOr = require('own-or')
const ownOrEnv = require('own-or-env')
const Promise = require('bluebird')
const bindObj = require('bind-obj-methods')
const cwd = process.cwd()
// A sigil object for implicit end() calls that should not
// trigger an error if the user then calls t.end()
const IMPLICIT = Symbol('implicit t.end()')
// Sigil to put in the queue to signal the end of all things
const EOF = Symbol('EOF')
const _currentAssert = Symbol('_currentAssert')
const _end = Symbol('_end')
const _snapshot = Symbol('_snapshot')
const Snapshot = require('./snapshot.js')
const hasOwn = (obj, key) =>
Object.prototype.hasOwnProperty.call(obj, key)
const isRegExp = re =>
Object.prototype.toString.call(re) === '[object RegExp]'
class Test extends Base {
constructor (options) {
options = options || {}
super(options)
this.pushedEnd = false
this.jobs = ownOr(options, 'jobs', 1)
this.subtests = []
this.pool = new Pool()
this.queue = ['TAP version 13\n']
// snapshots are keyed off of the main file that loads the
// root test object. Typically, this is the TAP object.
// To do this, we climb the ladder and only save in the teardown
// of that root (parentless) test object. This allows handling
// cases where the same test name can be used multiple times
// in a single test file, which would otherwise clobber snapshots.
this.writeSnapshot = ownOrEnv(
options, 'snapshot', 'TAP_SNAPSHOT', true)
if (this.parent && this.parent[_snapshot])
this[_snapshot] = this.parent[_snapshot]
else
this[_snapshot] = new Snapshot(this)
this.noparallel = false
if (options.cb)
this.cb = this.domain.bind(options.cb)
this.occupied = false
this[_currentAssert] = null
this.count = 0
this.n = 0
this.ended = false
this.explicitEnded = false
this.multiEndThrew = false
this.assertAt = null
this.assertStack = null
this.planEnd = -1
this.onBeforeEach = []
this.onAfterEach = []
this.ranAfterEach = false
// bind all methods to this object, so we can pass t.end as a callback
// and do `const test = require('tap').test` like people do.
const bound = Object.create(null)
bindObj(this, this, bound)
bindObj(this, Object.getPrototypeOf(this), bound)
bindObj(this, Test.prototype, bound)
}
spawn (cmd, args, options, name) {
if (typeof args === 'string')
args = [ args ]
args = args || []
if (typeof options === 'string') {
name = options
options = {}
}
options = options || {}
options.name = ownOr(options, 'name', name)
options.command = cmd
options.args = args
return this.sub(Spawn, options, Test.prototype.spawn)
}
sub (Class, extra, caller) {
if (this.results || this.ended) {
const er = new Error('cannot create subtest after parent test end')
this.threw(er)
return Promise.resolve(this)
}
if (!extra.skip && this.grep.length) {
const m = this.grep[0].test(extra.name)
const match = this.grepInvert ? !m : m
if (!match) {
const p = 'filter' + (this.grepInvert ? ' out' : '') + ': '
extra.skip = p + this.grep[0]
}
}
if (extra.only && !this.runOnly)
this.comment('%j has `only` set but all tests run', extra.name)
if (this.runOnly && !extra.only)
extra.skip = 'filter: only'
if (extra.todo || extra.skip) {
this.pass(extra.name, extra)
return Promise.resolve(this)
}
if (!extra.grep) {
extra.grep = this.grep.slice(1)
extra.grepInvert = this.grepInvert
}
extra.indent = ' '
if (this.jobs > 1 && process.env.TAP_BUFFER === undefined)
extra.buffered = ownOr(extra, 'buffered', true)
else
extra.buffered = ownOrEnv(extra, 'buffered', 'TAP_BUFFER', true)
extra.bail = ownOr(extra, 'bail', this.bail)
extra.parent = this
extra.stack = stack.captureString(80, caller)
const t = new Class(extra)
this.queue.push(t)
this.subtests.push(t)
const d = new Deferred()
t.deferred = d
this.process()
return d.promise
}
todo (name, extra, cb) {
extra = parseTestArgs(name, extra, cb)
extra.todo = true
return this.sub(Test, extra, Test.prototype.todo)
}
skip (name, extra, cb) {
extra = parseTestArgs(name, extra, cb)
extra.skip = true
return this.sub(Test, extra, Test.prototype.skip)
}
only (name, extra, cb) {
extra = parseTestArgs(name, extra, cb)
extra.only = true
return this.sub(Test, extra, Test.prototype.only)
}
test (name, extra, cb) {
extra = parseTestArgs(name, extra, cb)
return this.sub(Test, extra, Test.prototype.test)
}
stdin (name, extra) {
/* istanbul ignore next */
extra = parseTestArgs(name, extra, false, '/dev/stdin')
return this.sub(Stdin, extra, Test.prototype.stdin)
}
bailout (message) {
if (this.parent && (this.results || this.ended))
this.parent.bailout(message)
else {
this.process()
message = message ? ' ' + ('' + message).trim() : ''
message = message.replace(/[\r\n]/g, ' ')
this.parser.write('Bail out!' + message + '\n')
}
this.end(IMPLICIT)
this.process()
}
comment () {
const body = util.format.apply(util, arguments)
const message = '# ' + body.split(/\r?\n/).join('\n# ') + '\n'
if (this.results)
this.write(message)
else
this.queue.push(message)
this.process()
}
timeout (options) {
options = options || {}
options.expired = options.expired || this.name
if (this.occupied)
this.occupied.timeout(options)
else
Base.prototype.timeout.call(this, options)
this.end(IMPLICIT)
}
main (cb) {
this.setTimeout(this.options.timeout)
this.debug('MAIN pre', this)
const end = () => {
this.debug(' > implicit end for promise')
this.end(IMPLICIT)
done()
}
const done = (er) => {
if (er)
this.threw(er)
if (this.results || this.bailedOut)
cb()
else
this.ondone = cb
}
// This bit of overly clever line-noise wraps the call to user-code
// in a try-catch. We can't rely on the domain for this yet, because
// the 'end' event can trigger a throw after the domain is unhooked,
// but before this is no longer the official "active test"
const ret = (() => {
try {
return this.cb(this)
} catch (er) {
this.threw(er)
}
})()
if (ret && ret.then) {
this.promise = ret
ret.tapAbortPromise = done
ret.then(end, done)
} else
done()
this.debug('MAIN post', this)
}
process () {
if (this.processing)
return this.debug(' < already processing')
this.debug('\nPROCESSING(%s)', this.name, this.queue.length)
this.processing = true
while (!this.occupied) {
const p = this.queue.shift()
if (!p)
break
if (p instanceof Base) {
this.processSubtest(p)
} else if (p === EOF) {
this.debug(' > EOF', this.name)
// I AM BECOME EOF, DESTROYER OF STREAMS
if (this.writeSnapshot)
this[_snapshot].save()
this.parser.end()
} else if (p instanceof TestPoint) {
this.debug(' > TESTPOINT')
this.parser.write(p.ok + (++this.n) + p.message)
} else if (typeof p === 'string') {
this.debug(' > STRING')
this.parser.write(p)
} else {
/* istanbul ignore else */
if (Array.isArray(p)) {
this.debug(' > METHOD')
const m = p.shift()
this[m].apply(this, p)
} else {
throw new Error('weird thing got in the queue')
}
}
}
while (!this.noparallel && this.pool.length < this.jobs) {
const p = this.subtests.shift()
if (!p)
break
if (!p.buffered) {
this.noparallel = true
break
}
this.debug('start subtest', p)
this.pool.add(p)
if (this.bailedOut)
this.onbufferedend(p)
else
this.runBeforeEach(p, () =>
p.main(() => this.onbufferedend(p)))
}
this.debug('done processing', this.queue, this.occupied)
this.processing = false
// just in case any tests ended, and we have sync stuff still
// waiting around in the queue to be processed
if (!this.occupied && this.queue.length)
this.process()
this.maybeAutoend()
}
processSubtest (p) {
this.debug(' > subtest')
this.occupied = p
if (!p.buffered) {
if (this.bailedOut)
return this.onindentedend(p)
this.debug(' > subtest indented')
p.pipe(this.parser, { end: false })
this.runBeforeEach(p, () =>
this.writeSubComment(p, () =>
p.main(() => this.onindentedend(p))))
} else if (p.readyToProcess) {
this.debug(' > subtest buffered, finished')
// finished! do the thing!
this.occupied = null
if (!p.passing() || !p.silent) {
this.queue.unshift(['emitSubTeardown', p])
this.printResult(p.passing(), p.name, p.options, true)
}
} else {
this.occupied = p
this.debug(' > subtest buffered, unfinished', p)
// unfinished buffered test.
// nothing to do yet, just leave it there.
this.queue.unshift(p)
}
}
emitSubTeardown (p) {
try {
p.emit('teardown')
} catch (er) {
delete p.options.time
p.threw(er)
}
}
writeSubComment (p, cb) {
const comment = '# Subtest' +
(p.name ? ': ' + p.name : '') +
'\n'
this.parser.write(comment)
cb()
}
onbufferedend (p) {
delete p.ondone
p.results = p.results || {}
p.readyToProcess = true
const to = p.options.timeout
const dur = (to && p.passing()) ? Date.now() - p.start : null
if (dur && dur > to)
p.timeout()
else
p.setTimeout(false)
this.debug('%s.onbufferedend', this.name, p.name, p.results.bailout)
this.pool.remove(p)
p.options.tapChildBuffer = p.output || ''
p.options.stack = ''
if (p.time)
p.options.time = p.time
if (this.occupied === p)
this.occupied = null
p.deferred.resolve(this)
this.process()
}
onindentedend (p) {
delete p.ondone
this.debug('onindentedend', p)
this.noparallel = false
const sti = this.subtests.indexOf(p)
if (sti !== -1)
this.subtests.splice(sti, 1)
p.readyToProcess = true
p.results = p.results || {}
if (p.time)
p.options.time = p.time
const to = p.options.timeout
const dur = (to && p.passing()) ? Date.now() - p.start : null
if (dur && dur > to)
p.timeout()
else
p.setTimeout(false)
this.debug('onindentedend %s(%s)', this.name, p.name)
if (!p.bailedOut)
assert.equal(this.occupied, p)
this.occupied = null
this.debug('OIE(%s) b>shift into queue', this.name, this.queue)
p.options.stack = ''
this.queue.unshift(['emitSubTeardown', p])
this.printResult(p.passing(), p.name, p.options, true)
this.debug('OIE(%s) shifted into queue', this.name, this.queue)
p.deferred.resolve(this)
this.process()
}
addAssert (name, length, fn) {
if (!name)
throw new TypeError('name is required for addAssert')
if (!(typeof length === 'number' && length >= 0))
throw new TypeError('number of args required')
if (typeof fn !== 'function')
throw new TypeError('function required for addAssert')
if (Test.prototype[name] || this[name])
throw new TypeError('attempt to re-define `' + name + '` assert')
function ASSERT () {
this.currentAssert = ASSERT
const args = new Array(length + 2)
for (let i = 0; i < length; i++) {
args[i] = arguments[i]
}
if (typeof arguments[length] === 'object') {
args[length] = ''
args[length + 1] = arguments[length]
} else {
args[length] = arguments[length] || ''
args[length + 1] = arguments[length + 1] || {}
}
return fn.apply(this, args)
}
this[name] = ASSERT
}
printResult (ok, message, extra, front) {
const n = this.count + 1
this.currentAssert = Test.prototype.printResult
const fn = this[_currentAssert]
this[_currentAssert] = null
if (this.planEnd !== -1 && n > this.planEnd) {
if (!this.passing())
return
const failMessage = this.explicitEnded
? 'test after end() was called'
: 'test count exceeds plan'
const er = new Error(failMessage)
Error.captureStackTrace(er, fn)
er.test = this.name
er.plan = this.planEnd
this.threw(er)
return
}
extra = extra || {}
if (extra.expectFail)
ok = !ok
if (this.assertAt) {
extra.at = this.assertAt
this.assertAt = null
}
if (this.assertStack) {
extra.stack = this.assertStack
this.assertStack = null
}
if (hasOwn(extra, 'stack') && !hasOwn(extra, 'at'))
extra.at = stack.parseLine(extra.stack.split('\n')[0])
if (!ok && !extra.skip && !hasOwn(extra, 'at')) {
assert.equal(typeof fn, 'function')
extra.at = stack.at(fn)
if (!extra.todo)
extra.stack = stack.captureString(80, fn)
}
const diagnostic =
typeof extra.diagnostic === 'boolean' ? extra.diagnostic
: process.env.TAP_DIAG === '0' ? false
: process.env.TAP_DIAG === '1' ? true
: extra.skip ? false
: !ok
if (diagnostic)
extra.diagnostic = true
this.count = n
message = message + ''
const res = { ok: ok, message: message, extra: extra }
const output = new TestPoint(ok, message, extra)
// when we jump the queue, skip an extra line
if (front)
output.message = output.message.trimRight() + '\n\n'
if (front) {
this.emit('result', res)
this.parser.write(output.ok + (++this.n) + output.message)
} else
this.queue.push(['emit', 'result', res], output)
if (this.planEnd === this.count)
this.end(IMPLICIT)
this.process()
}
pragma (set) {
const p = Object.keys(set).reduce((acc, i) =>
acc + 'pragma ' + (set[i] ? '+' : '-') + i + '\n', '')
this.queue.push(p)
this.process()
}
plan (n, comment) {
if (this.bailedOut)
return
if (this.planEnd !== -1) {
throw new Error('Cannot set plan more than once')
}
if (typeof n !== 'number' || n < 0) {
throw new TypeError('plan must be a number')
}
// Cannot get any tests after a trailing plan, or a plan of 0
const ending = this.count !== 0 || n === 0
if (n === 0 && comment && !this.options.skip)
this.options.skip = comment
this.planEnd = n
comment = comment ? ' # ' + comment.trim() : ''
this.queue.push('1..' + n + comment + '\n')
if (ending)
this.end(IMPLICIT)
else
this.process()
}
end (implicit) {
this.debug('END implicit=%j', implicit === IMPLICIT)
if (this.ended && implicit === IMPLICIT)
return
// beyond here we have to be actually done with things, or else
// the semantic checks on counts and such will be off.
if (!queueEmpty(this) || this.occupied) {
if (!this.pushedEnd)
this.queue.push(['end', implicit])
this.pushedEnd = true
return this.process()
}
if (!this.ranAfterEach && this.parent) {
this.ranAfterEach = true
this.parent.runAfterEach(this, () => this[_end](implicit))
return
} else
this[_end](implicit)
}
[_end] (implicit) {
this.ended = true
if (implicit !== IMPLICIT && !this.multiEndThrew) {
if (this.explicitEnded) {
this.multiEndThrew = true
const er = new Error('test end() method called more than once')
Error.captureStackTrace(er, this[_currentAssert] ||
Test.prototype[_end])
er.test = this.name
this.threw(er)
return
}
this.explicitEnded = true
}
if (this.planEnd === -1) {
this.debug('END(%s) implicit plan', this.name, this.count)
this.plan(this.count)
}
this.queue.push(EOF)
this.process()
}
threw (er, extra, proxy) {
this.debug('THREW', er.message, extra, proxy)
// event emitters 'error' events need to re-throw so that they
// can jump out of the flow like a normal throw. They'll just
// end up back here once that happens, though, unless there's a
// try/catch somewhere in the call stack.
if (er.domainEmitter) {
delete er.domainEmitter
throw er
}
if (this.name && !proxy)
er.test = this.name
if (!proxy)
extra = extraFromError(er, extra, this.options)
Base.prototype.threw.call(this, er, extra, proxy)
if (!this.results) {
this.fail(extra.message || er.message, extra)
if (!proxy)
this.end(IMPLICIT)
}
this.process()
}
runBeforeEach (who, cb) {
if (this.parent)
this.parent.runBeforeEach(who, () => {
loop(who, this.onBeforeEach, cb, who.threw)
})
else
loop(who, this.onBeforeEach, cb, who.threw)
}
runAfterEach (who, cb) {
loop(who, this.onAfterEach, () => {
if (this.parent)
this.parent.runAfterEach(who, cb)
else
cb()
}, who.threw)
}
beforeEach (fn) {
this.onBeforeEach.push(fn)
}
afterEach (fn) {
this.onAfterEach.push(fn)
}
teardown (fn) {
this.on('teardown', fn)
}
shouldAutoend () {
const should = (
this.options.autoend &&
!this.ended &&
!this.occupied &&
queueEmpty(this) &&
!this.pool.length &&
!this.subtests.length &&
this.planEnd === -1
)
return should
}
autoend () {
this.options.autoend = true
this.maybeAutoend()
}
maybeAutoend () {
if (this.shouldAutoend()) {
clearTimeout(this.autoendTimer)
this.autoendTimer = setTimeout(() => {
if (this.shouldAutoend()) {
clearTimeout(this.autoendTimer)
this.autoendTimer = setTimeout(() => {
if (this.shouldAutoend())
this.end(IMPLICIT)
})
}
})
}
}
endAll (sub) {
// in the case of the root TAP test object, we might sometimes
// call endAll on a bailing-out test, as the process is ending
// In that case, we WILL have a this.occupied and a full queue
// These cases are very rare to encounter in other Test objs tho
this.processing = true
if (this.occupied) {
const p = this.occupied
if (p.endAll)
p.endAll(true)
else {
p.parser.abort('test unfinished')
}
} else if (sub) {
this.process()
if (queueEmpty(this)) {
const options = Object.assign({}, this.options)
this.options.at = null
this.options.stack = ''
options.test = this.name
this.fail('test unfinished', options)
}
}
if (this.promise && this.promise.tapAbortPromise)
this.promise.tapAbortPromise()
if (this.occupied) {
this.queue.unshift(this.occupied)
this.occupied = null
}
endAllQueue(this.queue)
this.processing = false
this.process()
this.parser.end()
}
get currentAssert () {
return this[_currentAssert]
}
set currentAssert (fn) {
if (!this[_currentAssert])
this[_currentAssert] = fn
}
pass (message, extra) {
this.currentAssert = Test.prototype.pass
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
this.printResult(true, message || '(unnamed test)', extra)
return true
}
fail (message, extra) {
this.currentAssert = Test.prototype.fail
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
this.printResult(false, message || '(unnamed test)', extra)
return !!(extra.todo || extra.skip)
}
ok (obj, message, extra) {
this.currentAssert = Test.prototype.ok
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
message = message || 'expect truthy value'
return obj ? this.pass(message, extra) : this.fail(message, extra)
}
notOk (obj, message, extra) {
this.currentAssert = Test.prototype.notOk
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
message = message || 'expect falsey value'
return this.ok(!obj, message, extra)
}
error (er, message, extra) {
this.currentAssert = Test.prototype.error
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
if (!er) {
return this.pass(message || 'should not error', extra)
}
if (!(er instanceof Error)) {
extra.found = er
return this.fail(message || 'non-Error error encountered', extra)
}
message = message || er.message
extra.found = er
return this.fail(message, extra)
}
equal (found, wanted, message, extra) {
this.currentAssert = Test.prototype.equal
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
message = message || 'should be equal'
if (found === wanted) {
return this.pass(message, extra)
}
extra.found = found
extra.wanted = wanted
extra.compare = '==='
if (typeof found === 'object' &&
typeof wanted === 'object' &&
found &&
wanted &&
tsame(found, wanted)) {
extra.note = 'Objects never === one another'
}
return this.fail(message, extra)
}
not (found, wanted, message, extra) {
this.currentAssert = Test.prototype.not
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
message = message || 'should not be equal'
if (found !== wanted) {
return this.pass(message, extra)
}
extra.found = found
extra.doNotWant = wanted
extra.compare = '!=='
return this.fail(message, extra)
}
same (found, wanted, message, extra) {
this.currentAssert = Test.prototype.same
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
message = message || 'should be equivalent'
extra.found = found
extra.wanted = wanted
return this.ok(tsame(found, wanted), message, extra)
}
notSame (found, wanted, message, extra) {
this.currentAssert = Test.prototype.notSame
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
message = message || 'should not be equivalent'
extra.found = found
extra.doNotWant = wanted
return this.notOk(tsame(found, wanted), message, extra)
}
strictSame (found, wanted, message, extra) {
this.currentAssert = Test.prototype.strictSame
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
message = message || 'should be equivalent strictly'
extra.found = found
extra.wanted = wanted
return this.ok(tsame.strict(found, wanted), message, extra)
}
strictNotSame (found, wanted, message, extra) {
this.currentAssert = Test.prototype.strictNotSame
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
message = message || 'should not be equivalent strictly'
extra.found = found
extra.doNotWant = wanted
return this.notOk(tsame.strict(found, wanted), message, extra)
}
get fullname () {
const main = process.argv[1] || ''
return (this.parent ? this.parent.fullname
: main.indexOf(cwd) === 0 ? main.substr(cwd.length + 1)
: path.basename(main)) +
' ' + (this.name || '').trim()
}
matchSnapshot (found, message, extra) {
this.currentAssert = Test.prototype.matchSnapshot
if (message && typeof message === 'object')
extra = message, message = 'must match snapshot'
if (!extra)
extra = {}
// use notOk because snap doesn't return a truthy value
const m = this.fullname + ' > ' + message
if (typeof found !== 'string')
found = util.inspect(found, {
showHidden: false,
depth: Infinity
})
return this.writeSnapshot
? this.notOk(this[_snapshot].snap(found, m),
message, extra)
: this.equal(found, this[_snapshot].read(m),
message, extra)
}
match (found, wanted, message, extra) {
this.currentAssert = Test.prototype.match
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
message = message || 'should match pattern provided'
extra.found = found
extra.pattern = wanted
return this.ok(tmatch(found, wanted), message, extra)
}
notMatch (found, wanted, message, extra) {
this.currentAssert = Test.prototype.notMatch
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
message = message || 'should not match pattern provided'
extra.found = found
extra.pattern = wanted
return this.ok(!tmatch(found, wanted), message, extra)
}
type (obj, klass, message, extra) {
this.currentAssert = Test.prototype.type
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
const name = typeof klass === 'function' ?
klass.name || '(anonymous constructor)'
: klass
message = message || 'type is ' + name
// simplest case, it literally is the same thing
if (obj === klass) {
return this.pass(message, extra)
}
const tof = typeof obj
const type = (!obj && tof === 'object') ? 'null'
// treat as object, but not Object
// t.type(() => {}, Function)
: (tof === 'function' &&
typeof klass === 'function' &&
klass !== Object) ? 'object'
: tof
if (type === 'object' && klass !== 'object') {
if (typeof klass === 'function') {
extra.found = Object.getPrototypeOf(obj).constructor.name
extra.wanted = name
return this.ok(obj instanceof klass, message, extra)
}
// check prototype chain for name
// at this point, we already know klass is not a function
// if the klass specified is an obj in the proto chain, pass
// if the name specified is the name of a ctor in the chain, pass
for (let p = obj; p; p = Object.getPrototypeOf(p)) {
const ctor = p.constructor && p.constructor.name
if (p === klass || ctor === name) {
return this.pass(message, extra)
}
}
}
return this.equal(type, name, message, extra)
}
throws (_fn, _wanted, _message, _extra) {
this.currentAssert = Test.prototype.throws
let fn, wanted, message, extra
for (let i = 0; i < arguments.length; i++) {
const arg = arguments[i]
if (typeof arg === 'function') {
if (arg === Error || arg.prototype instanceof Error) {
wanted = arg
} else if (!fn) {
fn = arg
}
} else if (typeof arg === 'string' && arg) {
message = arg
} else if (typeof arg === 'object') {
if (!wanted) {
wanted = arg
} else {
extra = arg
}
}
}
if (!extra)
extra = {}
if (!message)
message = fn && fn.name || 'expected to throw'
if (wanted) {
if (wanted instanceof Error) {
const w = {
message: wanted.message
}
if (wanted.name) {
w.name = wanted.name
}
// intentionally copying non-local properties, since this
// is an Error object, and those are funky.
for (let i in wanted) {
w[i] = wanted[i]
}
wanted = w
message += ': ' + (wanted.name || 'Error') + ' ' + wanted.message
extra.wanted = wanted
}
}
if (typeof fn !== 'function') {
extra.todo = true
return this.pass(message, extra)
}
try {
fn()
return this.fail(message, extra)
} catch (er) {
// 'name' is a getter.
if (er.name) {
Object.defineProperty(er, 'name', {
value: er.name + '',
enumerable: true,
configurable: true,
writable: true
})
}
const actual = isRegExp(wanted) ? er.message : er
return wanted ? this.match(actual, wanted, message, extra)
: this.pass(message, extra)
}
}
doesNotThrow (fn, message, extra) {
this.currentAssert = Test.prototype.doesNotThrow
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
if (typeof fn === 'string') {
const x = fn
fn = message
message = x
}
if (!message) {
message = fn && fn.name || 'expected to not throw'
}
if (typeof fn !== 'function') {
extra.todo = true
return this.pass(message, extra)
}
try {
fn()
return this.pass(message, extra)
} catch (er) {
const e = extraFromError(er, extra)
e.message = er.message
return this.fail(message, e)
}
}
// like throws, but rejects a returned promise instead
// also, can pass in a promise instead of a function
rejects (_fn, _wanted, _message, _extra) {
this.currentAssert = Test.prototype.rejects
let fn, wanted, extra, promise, message
for (let i = 0; i < arguments.length; i++) {
const arg = arguments[i]
if (typeof arg === 'function') {
if (arg === Error || arg.prototype instanceof Error) {
wanted = arg
} else if (!fn) {
fn = arg
}
} else if (typeof arg === 'string' && arg) {
message = arg
} else if (arg && typeof arg.then === 'function' && !promise) {
promise = arg
} else if (typeof arg === 'object') {
if (!wanted) {
wanted = arg
} else {
extra = arg
}
}
}
if (!extra)
extra = {}
if (!message)
message = fn && fn.name || 'expect rejected Promise'
if (wanted) {
if (wanted instanceof Error) {
const w = {
message: wanted.message
}
if (wanted.name)
w.name = wanted.name
// intentionally copying non-local properties, since this
// is an Error object, and those are funky.
for (let i in wanted) {
w[i] = wanted[i]
}
wanted = w
message += ': ' + (wanted.name || 'Error') + ' ' + wanted.message
extra.wanted = wanted
}
}
if (!promise && typeof fn !== 'function') {
extra.todo = true
this.pass(message, extra)
return Promise.resolve(this)
}
if (!promise)
promise = fn()
const textra = {
buffered: true,
todo: extra.todo,
skip: extra.skip
}
if (!promise || typeof promise.then !== 'function')
return this.test(message, textra, t => {
t.fail(message, extra)
t.end()
})
// have to do as a subtest, because promises are async
extra.at = stack.at(this.currentAssert)
return this.test(message, textra, t => promise.then(value => {
extra.found = value
t.fail(message, extra)
}, er => {
// 'name' is a getter.
if (er.name) {
Object.defineProperty(er, 'name', {
value: er.name + '',
enumerable: true,
configurable: true,
writable: true
})
}
const actual = isRegExp(wanted) ? er.message : er
return wanted ? t.match(actual, wanted, message, extra)
: t.pass(message, extra)
}))
}
resolves (promise, message, extra) {
this.currentAssert = Test.prototype.resolves
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
if (!message)
message = 'expect resolving Promise'
if (typeof promise === 'function')
promise = promise()
// have to do as a subtest, because promises are async
extra.at = stack.at(this.currentAssert)
const textra = {
buffered: true,
todo: extra.todo,
skip: extra.skip
}
if (!promise || typeof promise.then !== 'function')
return this.test(message, textra, t => {
t.fail(message, extra)
t.end()
})
return this.test(message, textra, t =>
promise.then(value => {
extra.found = value
t.pass(message, extra)
}))
}
resolveMatch (promise, wanted, message, extra) {
this.currentAssert = Test.prototype.resolveMatch
if (message && typeof message === 'object')
extra = message, message = ''
if (!extra)
extra = {}
if (!message)
message = 'expect resolving Promise'
// have to do as a subtest, because promises are async
extra.at = stack.at(this.currentAssert)
const textra = {
buffered: true,
todo: extra.todo,
skip: extra.skip
}
if (!promise || typeof promise.then !== 'function')
return this.test(message, textra, t => {
t.fail(message, extra)
t.end()
})
return this.test(message, textra, t =>
promise.then(value => {
extra.found = value
t.match(value, wanted, extra)
}))
}
}
const endAllQueue = queue => {
queue.forEach((p, i) => {
if ((p instanceof Base) && !p.readyToProcess)
queue[i] = new TestPoint(false,
'child test left in queue:' +
' t.' + p.constructor.name.toLowerCase() + ' ' + p.name,
p.options)
})
queue.push(['end', IMPLICIT])
}
const queueEmpty = t =>
t.queue.length === 0 ||
t.queue.length === 1 && t.queue[0] === 'TAP version 13\n'
Test.prototype.done = Test.prototype.end
Test.prototype.tearDown = Test.prototype.teardown
Object.keys(synonyms)
.filter(c => Test.prototype[c])
.forEach(c => synonyms[c].forEach(s =>
Object.defineProperty(Test.prototype, s, {
value: Test.prototype[c],
enumerable: false,
configurable: true,
writable: true
})))
module.exports = Test