'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.
// grab a reference right away, in case the user clubs global.process
const process = require('./process.js')
const path = require('path')
const assert = require('assert')
const util = require('util')
const {format, same, strict, match, has, hasStrict} = require('tcompare')
const Deferred = require('trivial-deferred')
const loop = require('function-loop')
const ownOr = require('own-or')
const ownOrEnv = require('own-or-env')
const bindObj = require('bind-obj-methods')
const esc = require('./esc.js')
const Base = require('./base.js')
const Spawn = require('./spawn.js')
const Stdin = require('./stdin.js')
const TestPoint = require('./point.js')
const parseTestArgs = require('./parse-test-args.js')
const Fixture = require('./fixture.js')
const Mock = require('./mock.js')
const cleanYamlObject = require('./clean-yaml-object.js')
const extraFromError = require('./extra-from-error.js')
const stack = require('./stack.js')
const settings = require('../settings.js')
const Snapshot = require('./snapshot.js')
const Waiter = require('./waiter.js')
const findMainScript = require('./find-main-script.js')
const formatSnapshotDefault = obj => format(obj, { sort: true })
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 _getSnapshot = Symbol('_getSnapshot')
const _beforeEnd = Symbol('_beforeEnd')
const _emits = Symbol('_emits')
const _nextChildId = Symbol('_nextChildId')
const _expectUncaught = Symbol('_expectUncaught')
const _createdFixture = Symbol('_createdFixture')
const _beforeCalled = Symbol('_beforeCalled')
const _printedResult = Symbol('_printedResult')
const hasOwn = (obj, key) =>
Object.prototype.hasOwnProperty.call(obj, key)
const isRegExp = re =>
Object.prototype.toString.call(re) === '[object RegExp]'
const normalizeMessageExtra = (defaultMessage, message, extra) => {
if (message && typeof message === 'object') {
return [defaultMessage, message]
}
return [
message || defaultMessage,
extra || {}
]
}
class Test extends Base {
constructor (options) {
options = options || {}
super(options)
const cmp = ownOr(options, 'compareOptions', undefined)
this.compareOptions = cmp && typeof cmp === 'object'
? Object.create(cmp) : {}
this[_nextChildId] = 1
this.pushedEnd = false
this.jobs = ownOr(options, 'jobs', 1)
this.doingStdinOnly = false
this.onTeardown = []
this[_createdFixture] = false
this.subtests = []
this.pool = new Set()
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 = hasOwn(options, 'snapshot') ? options.snapshot
: this.parent ? this.parent.writeSnapshot
: process.env.TAP_SNAPSHOT === '1'
if (this.parent && this.parent.cleanSnapshot)
this.cleanSnapshot = this.parent.cleanSnapshot
this.formatSnapshot = this.parent && this.parent.formatSnapshot
this.noparallel = false
if (options.cb)
this.cb = (...args) => this.hook.runInAsyncScope(options.cb, this, ...args)
this.occupied = false
this[_currentAssert] = null
this[_beforeEnd] = []
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
this[_expectUncaught] = []
// 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.bailedOut)
return
if (this.doingStdinOnly)
throw new Error('cannot run subtests in stdinOnly mode')
if (this.results || this.ended) {
const er = new Error('cannot create subtest after parent test end')
Error.captureStackTrace(er, caller)
this.threw(er)
return Promise.resolve(this)
}
extra.childId = this[_nextChildId]++
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.saveFixture = ownOr(extra, 'saveFixture', this.saveFixture)
extra.parent = this
extra.stack = stack.captureString(80, caller)
extra.context = this.context
extra.compareOptions = this.compareOptions
const t = new Class(extra)
this.queue.push(t)
this.subtests.push(t)
this.emit('subtestAdd', t)
const d = new Deferred()
t.deferred = d
this.process()
return d.promise
}
todo (name, extra, cb) {
extra = parseTestArgs(name, extra, cb)
extra.todo = extra.todo || true
return this.sub(Test, extra, Test.prototype.todo)
}
skip (name, extra, cb) {
extra = parseTestArgs(name, extra, cb)
extra.skip = 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)
}
stdinOnly (extra) {
const stream = extra && extra.tapStream || process.stdin
if (!stream)
throw new Error('cannot read stdin without stdin stream')
if (this.queue.length !== 1 ||
this.queue[0] !== 'TAP version 13\n' ||
this.processing ||
this.results ||
this.occupied ||
this.pool.size ||
this.subtests.length)
throw new Error('Cannot use stdinOnly on a test in progress')
this.doingStdinOnly = true
this.queue.length = 0
this.parser.on('child', p => {
// pretend to be a rooted parser, so it gets counts.
p.root = p
const t = new Base({
name: p.name,
parent: this,
parser: p,
root: p,
bail: p.bail,
strict: p.strict,
omitVersion: p.omitVersion,
preserveWhitespace: p.preserveWhitespace,
childId: this[_nextChildId]++,
})
this.emit('subtestAdd', t)
this.emit('subtestStart', t)
this.emit('subtestProcess', t)
p.on('complete', () => {
t.time = p.time
this.emit('subtestEnd', t)
})
})
stream.pause()
stream.pipe(this.parser)
stream.resume()
}
stdin (name, extra) {
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 ? ' ' + ('' + esc(message)).trim() : ''
message = message.replace(/[\r\n]/g, ' ')
this.parser.write('Bail out!' + message + '\n')
}
this.end(IMPLICIT)
this.process()
}
comment (...args) {
const body = util.format(...args)
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)
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) {
if (!er || typeof er !== 'object')
er = { error: er }
er.tapCaught = 'testFunctionThrow'
this.threw(er)
}
})()
if (ret && ret.then) {
this.promise = ret
ret.tapAbortPromise = done
ret.then(end, er => {
if (!er || typeof er !== 'object')
er = { error: er }
er.tapCaught = 'returnedPromiseRejection'
done(er)
})
} 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[_getSnapshot]().save()
this.parser.end()
} else if (p instanceof TestPoint) {
this.debug(' > TESTPOINT')
if (p.extra.tapChildBuffer || p.extra.tapChildBuffer === '') {
this.writeSubComment(p, () => {})
this.parser.write(p.extra.tapChildBuffer)
}
this.emit('res', p.res)
this.parser.write(p.ok + (++this.n) + p.message)
} else if (typeof p === 'string') {
this.debug(' > STRING')
this.parser.write(p)
} else if (p instanceof Waiter) {
p.ready = true
this.occupied = p
p.finish()
} else {
/* istanbul ignore else */
if (Array.isArray(p)) {
this.debug(' > METHOD')
const m = p.shift()
const ret = this[m].apply(this, p)
if (ret && typeof ret.then === 'function') {
// returned promise
ret.then(() => {
this.processing = false
this.process()
}, er => {
this.processing = false
this.threw(er)
})
return
}
} else {
throw new Error('weird thing got in the queue')
}
}
}
while (!this.noparallel && this.pool.size < this.jobs) {
const p = this.subtests.shift()
if (!p)
break
if (!p.buffered) {
this.noparallel = true
break
}
this.debug('start subtest', p)
this.emit('subtestStart', p)
this.pool.add(p)
if (this.bailedOut)
this.onbufferedend(p)
else
this.runBeforeEach(p, () =>
p.runMain(() => 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) {
this.emit('subtestStart', p)
this.debug(' > subtest indented')
p.pipe(this.parser, { end: false })
this.runBeforeEach(p, () =>
this.writeSubComment(p, () =>
p.runMain(() => this.onindentedend(p))))
} else if (p.readyToProcess) {
this.emit('subtestProcess', p)
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) {
// if it's not a thing that CAN have teardowns, nothing to do here
if (!p.onTeardown)
return
const otd = p.onTeardown
p.onTeardown = []
const threw = er => {
if (!er || typeof er !== 'object')
er = { error: er }
er.tapCaught = 'teardown'
delete p.options.time
p.threw(er)
}
for (let i = 0; i < otd.length; i++) {
const fn = otd[i]
try {
const ret = fn.call(p)
if (ret && typeof ret.then === 'function') {
p.onTeardown = otd.slice(i + 1)
this.queue.unshift(['emitSubTeardown', p])
return ret.then(() => this.emitSubTeardown(p), er => {
if (!er || typeof er !== 'object')
er = { error: er }
er.tapCaught = 'teardown'
throw er
})
}
} catch (er) {
threw(er)
}
}
// ok we're done, just delete the fixture if it created one.
// do this AFTER all user-generated teardowns, and asynchronously so
// that we can do the fancy backoff dance for Win32's weirdo fs.
if (p[_createdFixture]) {
const {rmdirRecursive} = settings
return new Promise((res, rej) => {
rmdirRecursive(p[_createdFixture], er =>
er ? /* istanbul ignore next - rimraf never fails lol */ rej(er)
: res())
}).then(() => p.emit('teardown'))
} else
p.emit('teardown')
}
writeSubComment (p, cb) {
const comment = '# Subtest' +
(p.name ? ': ' + esc(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.delete(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(p.results)
this.emit('subtestEnd', p)
this.process()
}
onindentedend (p) {
this.emit('subtestProcess', 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.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)
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(p.results)
this.emit('subtestEnd', p)
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')
const ASSERT = function (...args) {
this.currentAssert = ASSERT
args.splice(length, 0, ...normalizeMessageExtra('', ...args.splice(length, 2)))
return fn.apply(this, args)
}
this[name] = ASSERT
}
static addAssert (name, length, fn) {
this.prototype.addAssert(name, length, fn)
}
printResult (ok, message, extra, front) {
this[_printedResult] = true
if (this.doingStdinOnly)
throw new Error('cannot print results in stdinOnly mode')
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, message, extra }
const tp = new TestPoint(ok, message, extra)
// when we jump the queue, skip an extra line
if (front)
tp.message = tp.message.trimRight() + '\n\n'
if (this.occupied && this.occupied instanceof Waiter &&
this.occupied.finishing)
front = true
if (front) {
if (extra.tapChildBuffer || extra.tapChildBuffer === '') {
this.writeSubComment(tp, () => {})
this.parser.write(extra.tapChildBuffer)
}
this.emit('result', res)
this.parser.write(tp.ok + (++this.n) + tp.message)
if (this.bail && !ok && !extra.skip && !extra.todo)
this.parser.write('Bail out! ' + message + '\n')
} else {
this.queue.push(tp)
if (this.bail && !ok && !extra.skip && !extra.todo)
this.queue.push('Bail out! ' + message + '\n')
}
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 ? ' # ' + esc(comment.trim()) : ''
this.queue.push('1..' + n + comment + '\n')
if (ending)
this.end(IMPLICIT)
else
this.process()
}
end (implicit) {
if (this.doingStdinOnly && implicit !== IMPLICIT)
throw new Error('cannot explicitly end while in stdinOnly mode')
this.debug('END implicit=%j', implicit === IMPLICIT)
if (this.ended && implicit === IMPLICIT)
return
if (this[_beforeEnd].length) {
for (let b = 0; b < this[_beforeEnd].length; b++) {
const m = this[_beforeEnd][b].shift()
this[m].apply(this, this[_beforeEnd][b])
}
this[_beforeEnd].length = 0
}
// 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)
if (this[_expectUncaught].length) {
const wanted = this[_expectUncaught]
this[_expectUncaught] = []
const diag = {
wanted: wanted.map(a => a.filter(e => e != null)),
test: this.name,
at: null,
stack: null,
}
const msg = 'test end without expected uncaught exceptions'
this.queue.push(['threw', Object.assign(new Error(msg), diag)])
}
this.process()
}
threw (er, extra, proxy) {
// this can only happen if a beforeEach function raises an error
if (this.parent && !this.started) {
this.cb = () => {
this.threw(er)
this.end()
}
return
}
if (!er || typeof er !== 'object')
er = { error: er }
if (this[_expectUncaught].length && (
er.tapCaught === 'uncaughtException' ||
er.tapCaught === 'unhandledRejection')) {
const [wanted, message, extra] = this[_expectUncaught].shift()
const actual = isRegExp(wanted) ? er.message : er
return wanted
? this.match(actual, wanted, message, extra)
: this.pass(message, extra)
}
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)
}
// threw while waiting for a promise to resolve.
// probably it's not ever gonna.
if (this.occupied && this.occupied instanceof Waiter)
this.occupied.abort(Object.assign(
new Error('error thrown while awaiting Promise'),
{ thrown: er }
))
this.process()
}
runBeforeEach (who, cb) {
if (this.parent)
this.parent.runBeforeEach(who, () => {
loop(who, this.onBeforeEach, cb, er => {
who.threw(er)
cb()
})
})
else
loop(who, this.onBeforeEach, cb, er => {
who.threw(er)
cb()
})
}
runAfterEach (who, cb) {
loop(who, this.onAfterEach, () => {
if (this.parent)
this.parent.runAfterEach(who, cb)
else
cb()
}, who.threw)
}
beforeEach (fn) {
// use function so that 'this' can be overridden
this.onBeforeEach.push(function () {
return fn.call(this, this)
})
}
afterEach (fn) {
// use function so that 'this' can be overridden
this.onAfterEach.push(function () {
return fn.call(this, this)
})
}
teardown (fn) {
this.onTeardown.push(fn)
}
shouldAutoend () {
const should = (
this.options.autoend &&
!this.ended &&
!this.occupied &&
queueEmpty(this) &&
!this.pool.size &&
!this.subtests.length &&
this.planEnd === -1
)
return should
}
autoend (value) {
// set to false to NOT trigger autoend
if (value === false) {
this.options.autoend = false
clearTimeout(this.autoendTimer)
} else {
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)
})
}
})
}
}
onbail (message) {
super.onbail(message)
this.end(IMPLICIT)
if (!this.parent)
this.endAll()
}
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 instanceof Waiter)
p.abort(new Error('test unfinished'))
else 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
this.printResult(true, ...normalizeMessageExtra('(unnamed test)', message, extra))
return true
}
fail (message, extra) {
[message, extra] = normalizeMessageExtra('(unnamed test)', message, extra)
this.currentAssert = Test.prototype.fail
this.printResult(false, message, extra)
return !!(extra.todo || extra.skip)
}
ok (obj, message, extra) {
[message, extra] = normalizeMessageExtra('expect truthy value', message, extra)
this.currentAssert = Test.prototype.ok
return obj ? this.pass(message, extra) : this.fail(message, extra)
}
notOk (obj, message, extra) {
[message, extra] = normalizeMessageExtra('expect falsey value', message, extra)
this.currentAssert = Test.prototype.notOk
return this.ok(!obj, message, extra)
}
emits (emitter, event, message, extra) {
[message, extra] = normalizeMessageExtra(`expect ${event} event to be emitted`, message, extra)
this.currentAssert = Test.prototype.emits
let resolve
let reject
const p = new Promise((res, rej) => {
resolve = res
reject = rej
})
const handler = () => {
handler.emitted = true
resolve()
}
handler.emitted = false
emitter.once(event, handler)
extra.at = stack.at(Test.prototype.emits)
extra.stack = stack.captureString(80, Test.prototype.emits)
this[_beforeEnd].push([_emits, emitter, event, handler, message, extra])
return p
}
[_emits] (emitter, event, handler, message, extra, reject) {
if (handler.emitted)
return this.pass(message, extra)
else {
emitter.removeListener(event, handler)
return this.fail(message, extra)
}
}
error (er, message, extra) {
[message, extra] = normalizeMessageExtra('', message, extra)
this.currentAssert = Test.prototype.error
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.origin = cleanYamlObject(extraFromError(er))
extra.found = er
return this.fail(message, extra)
}
equal (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should be equal', message, extra)
this.currentAssert = Test.prototype.equal
if (found === wanted) {
return this.pass(message, extra)
}
const objects = found &&
wanted &&
typeof found === 'object' &&
typeof wanted === 'object'
if (objects) {
const s = strict(found, wanted, this.compareOptions)
if (!s.match)
extra.diff = s.diff
else {
extra.found = found
extra.wanted = wanted
extra.note = 'object identities differ'
}
} else {
extra.found = found
extra.wanted = wanted
}
extra.compare = '==='
return this.fail(message, extra)
}
not (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should not be equal', message, extra)
this.currentAssert = Test.prototype.not
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) {
[message, extra] = normalizeMessageExtra('should be equivalent', message, extra)
this.currentAssert = Test.prototype.same
const s = same(found, wanted, this.compareOptions)
if (!s.match)
extra.diff = s.diff
else {
extra.found = found
extra.wanted = wanted
}
return this.ok(s.match, message, extra)
}
notSame (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should not be equivalent', message, extra)
this.currentAssert = Test.prototype.notSame
extra.found = found
extra.doNotWant = wanted
const s = same(found, wanted, this.compareOptions)
return this.notOk(s.match, message, extra)
}
strictSame (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should be equivalent strictly', message, extra)
this.currentAssert = Test.prototype.strictSame
const s = strict(found, wanted, this.compareOptions)
if (!s.match)
extra.diff = s.diff
else {
extra.found = found
extra.wanted = wanted
}
return this.ok(s.match, message, extra)
}
strictNotSame (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should not be equivalent strictly', message, extra)
this.currentAssert = Test.prototype.strictNotSame
extra.found = found
extra.doNotWant = wanted
const s = strict(found, wanted, this.compareOptions)
return this.notOk(s.match, message, extra)
}
get fullname () {
const main = process.argv.slice(1).join(' ').trim()
return (this.parent ? this.parent.fullname
: main.indexOf(cwd) === 0 ? main.substr(cwd.length + 1)
: path.basename(main)).replace(/\\/g, '/') +
' ' + (this.name || '').trim()
}
get snapshotFile () {
return this[_getSnapshot]().file
}
set snapshotFile (file) {
// if we're using the parent's snapshot, we're setting a new file
// so we need to un-hook from the parent's snapshot object
const usingParent = this.parent && file !== this.parent.snapshotFile
if (usingParent) {
this[_snapshot] = new Snapshot()
}
this[_getSnapshot]().file = file
}
get testdirName () {
const re = /[^a-zA-Z0-9\._\-]+/ig
if (!this.parent) {
const main = findMainScript('TAP')
// put in a prefix in the dirname so do not inadvertently run it
// on a subsequent tap invocation, if it was saved.
const dir = path.dirname(main)
const base = 'tap-testdir-' + (path.basename(main).replace(/\.[^.]+$/, '')
+ ' ' + process.argv.slice(2).join(' ')).trim()
return dir + path.sep + base.replace(re, '-')
}
return this.parent.testdirName + '-' +
(this.name || 'unnamed test').replace(re, '-')
}
testdir (fixture) {
const {rmdirRecursiveSync} = settings
const dir = this.testdirName
rmdirRecursiveSync(dir)
if (!this.saveFixture)
this[_createdFixture] = dir
Fixture.make(dir, fixture || {})
return dir
}
fixture (type, content) {
return new Fixture(type, content)
}
mock (module, mocks) {
const {file} = stack.at(Test.prototype.mock)
const resolved = path.resolve(file)
return Mock.get(resolved, module, mocks)
}
matchSnapshot (found, message, extra) {
[message, extra] = normalizeMessageExtra('must match snapshot', message, extra)
this.currentAssert = Test.prototype.matchSnapshot
// use notOk because snap doesn't return a truthy value
const m = this.fullname + ' > ' + message
if (typeof found !== 'string') {
found = (this.formatSnapshot || formatSnapshotDefault)(found)
if (typeof found !== 'string')
found = formatSnapshotDefault(found)
}
found = this.cleanSnapshot(found)
return this.writeSnapshot
? this.notOk(this[_getSnapshot]().snap(found, m),
message, extra)
: this.equal(found, this[_getSnapshot]().read(m),
message, extra)
}
[_getSnapshot] () {
if (this[_snapshot])
return this[_snapshot]
if (this.parent) {
const parentSnapshot = this.parent[_getSnapshot]()
// very rare for the parent to not have one.
/* istanbul ignore else */
if (parentSnapshot)
return this[_snapshot] = parentSnapshot
}
return this[_snapshot] = new Snapshot()
}
cleanSnapshot (string) {
return string
}
has (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should contain all provided fields', message, extra)
this.currentAssert = Test.prototype.has
const s = has(found, wanted, this.compareOptions)
if (!s.match)
extra.diff = s.diff
else {
extra.found = found
extra.pattern = wanted
}
return this.ok(s.match, message, extra)
}
notHas (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should not contain all provided fields', message, extra)
this.currentAssert = Test.prototype.notHas
extra.found = found
extra.pattern = wanted
const s = has(found, wanted, this.compareOptions)
return this.notOk(s.match, message, extra)
}
hasStrict (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should contain all provided fields strictly', message, extra)
this.currentAssert = Test.prototype.hasStrict
const s = hasStrict(found, wanted, this.compareOptions)
if (!s.match)
extra.diff = s.diff
else {
extra.found = found
extra.pattern = wanted
}
return this.ok(s.match, message, extra)
}
notHasStrict (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should not contain all provided fields strictly', message, extra)
this.currentAssert = Test.prototype.notHasStrict
extra.found = found
extra.pattern = wanted
const s = hasStrict(found, wanted, this.compareOptions)
return this.notOk(s.match, message, extra)
}
hasProp (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should have the named property', message, extra)
this.currentAssert = Test.prototype.hasProp
extra.found = found
extra.pattern = wanted
if (typeof wanted !== 'string') {
return this.fail('property name must be a string', extra)
}
try {
const s = (wanted in found) && found[wanted] !== undefined
return this.ok(s, message, extra)
} catch (e) {
return this.fail(e.message, extra)
}
}
hasOwnProp (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should have the named own property', message, extra)
this.currentAssert = Test.prototype.hasOwnProp
extra.found = found
extra.pattern = wanted
if (typeof wanted !== 'string') {
return this.fail('property name must be a string', extra)
}
try {
const s = hasOwn(found, wanted) && found[wanted] !== undefined
return this.ok(s, message, extra)
} catch (e) {
return this.fail(e.message, extra)
}
}
hasProps (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should have the named properties', message, extra)
this.currentAssert = Test.prototype.hasProps
extra.found = found
extra.pattern = wanted
// explicitly do not allow String objects, because regular strings not allowed
// even though they are technically iterable "objects"
if (!wanted || typeof wanted !== 'object' || !wanted[Symbol.iterator] || wanted instanceof String) {
return this.fail('property list must be iterable object', extra)
}
for (const prop of wanted) {
if (typeof prop !== 'string') {
return this.fail('property name must be a string', extra)
}
try {
const s = (prop in found) && found[prop] !== undefined
if (!s) {
return this.fail(message, extra)
}
} catch (e) {
return this.fail(e.message, extra)
}
}
return this.pass(message, extra)
}
hasOwnProps (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should have the named own properties', message, extra)
this.currentAssert = Test.prototype.hasOwnProps
extra.found = found
extra.pattern = wanted
// explicitly do not allow String objects, because regular strings not allowed
// even though they are technically iterable "objects"
if (!wanted || typeof wanted !== 'object' || !wanted[Symbol.iterator] || wanted instanceof String) {
return this.fail('property list must be iterable object', extra)
}
for (const prop of wanted) {
if (typeof prop !== 'string') {
return this.fail('property name must be a string', extra)
}
try {
const s = hasOwn(found, prop) && found[prop] !== undefined
if (!s) {
return this.fail(message, extra)
}
} catch (e) {
return this.fail(e.message, extra)
}
}
return this.pass(message, extra)
}
match (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should match pattern provided', message, extra)
this.currentAssert = Test.prototype.match
const s = match(found, wanted, this.compareOptions)
if (!s.match)
extra.diff = s.diff
else
extra.found = found
extra.pattern = wanted
return this.ok(s.match, message, extra)
}
notMatch (found, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('should not match pattern provided', message, extra)
this.currentAssert = Test.prototype.notMatch
extra.found = found
extra.pattern = wanted
const s = match(found, wanted, this.compareOptions)
return this.notOk(s.match, message, extra)
}
type (obj, klass, message, extra) {
this.currentAssert = Test.prototype.type
const name = typeof klass === 'function' ?
klass.name || '(anonymous constructor)'
: klass;
[message, extra] = normalizeMessageExtra(`type is ${name}`, message, extra)
// 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)
}
expectUncaughtException (...args) {
let [, ...rest] = this.throwsArgs('expect uncaughtException', ...args)
this[_expectUncaught].push(rest)
}
throwsArgs (defaultMessage, ...args) {
let fn, wanted, message, extra
for (let i = 0; i < args.length; i++) {
const arg = args[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
}
}
}
[message, extra] = normalizeMessageExtra(defaultMessage, message, extra)
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
}
}
return [fn, wanted, message, extra]
}
throws (...args) {
this.currentAssert = Test.prototype.throws
const [fn, wanted, message, extra] =
this.throwsArgs('expected to throw', ...args)
if (typeof fn !== 'function') {
extra.todo = 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) && er
: this.pass(message, extra) && er
}
}
doesNotThrow (fn, message, extra) {
[message, extra] = normalizeMessageExtra('', message, extra)
this.currentAssert = Test.prototype.doesNotThrow
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 = 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)
}
}
before (fn) {
this.currentAssert = Test.prototype.before
if (this.occupied || this[_printedResult])
throw new Error('t.before() called after starting tests')
if (this[_beforeCalled])
throw new Error('called t.before() more than once')
this[_beforeCalled] = true
// if it throws, we let it kill the test
const ret = fn.call(this)
if (ret && typeof ret.then === 'function')
this.waitOn(ret, w => {
if (w.rejected) {
// sort of a mini bailout, just for this one test
// drop everything from the queue, quit right away
this.queue.length = 0
this.threw(w.value)
this.planEnd = -1
this.count = 1
this.end()
}
})
}
waitOn (promise, cb, expectReject) {
const w = new Waiter(promise, w => {
assert.equal(this.occupied, w)
cb(w)
this.occupied = null
this.process()
}, expectReject)
this.queue.push(w)
this.process()
return w.promise
}
// like throws, but rejects a returned promise instead
// also, can pass in a promise instead of a function
rejects (...args) {
this.currentAssert = Test.prototype.rejects
let fn, wanted, extra, promise, message
for (let i = 0; i < args.length; i++) {
const arg = args[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 = extra.todo || true
this.pass(message, extra)
return Promise.resolve(this)
}
if (!promise)
promise = fn()
if (!promise || typeof promise.then !== 'function') {
this.fail(message, extra)
return Promise.resolve(this)
}
extra.at = stack.at(this.currentAssert)
return this.waitOn(promise, w => {
if (!w.rejected) {
extra.found = w.value
return this.fail(message, extra)
}
const er = w.value
// 'name' is a getter.
if (er && er.name) {
Object.defineProperty(er, 'name', {
value: er.name + '',
enumerable: true,
configurable: true,
writable: true
})
}
const actual = isRegExp(wanted) && er ? er.message : er
return wanted ? this.match(actual, wanted, message, extra)
: this.pass(message, extra)
}, true)
}
resolves (promise, message, extra) {
[message, extra] = normalizeMessageExtra('expect resolving Promise', message, extra)
this.currentAssert = Test.prototype.resolves
if (typeof promise === 'function')
promise = promise()
extra.at = stack.at(this.currentAssert)
if (!promise || typeof promise.then !== 'function') {
this.fail(message, extra)
return Promise.resolve(this)
}
return this.waitOn(promise, w => {
extra.found = w.value
this.ok(w.resolved, message, extra)
})
}
resolveMatch (promise, wanted, message, extra) {
[message, extra] = normalizeMessageExtra('expect resolving Promise', message, extra)
this.currentAssert = Test.prototype.resolveMatch
extra.at = stack.at(this.currentAssert)
if (typeof promise === 'function')
promise = promise()
if (!promise || typeof promise.then !== 'function') {
this.fail(message, extra)
return Promise.resolve(this)
}
return this.waitOn(promise, w => {
extra.found = w.value
return w.rejected ? this.fail(message, extra)
: this.match(w.value, wanted, message, extra)
})
}
resolveMatchSnapshot (promise, message, extra) {
[message, extra] = normalizeMessageExtra('expect resolving Promise', message, extra)
this.currentAssert = Test.prototype.resolveMatch
extra.at = stack.at(this.currentAssert)
if (typeof promise === 'function')
promise = promise()
if (!promise || typeof promise.then !== 'function') {
this.fail(message, extra)
return Promise.resolve(this)
}
return this.waitOn(promise, w => {
extra.found = w.value
return w.rejected ? this.fail(message, extra)
: this.matchSnapshot(w.value, message, 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'
module.exports = Test
|