582fea7b3f6bf571dc78268df1c742ea2782948a.svn-base 8.51 KB
'use strict'

const MiniPass = require('minipass')

const extraFromError = require('./extra-from-error.js')
const assert = require('assert')

const Domain = require('async-hook-domain')
const {AsyncResource} = require('async_hooks')
const util = require('util')

class TapWrap extends AsyncResource {
  constructor (test) {
    super('tap.' + test.constructor.name)
    this.test = test

    // Polyfill for Node.js 8 and before
    /* istanbul ignore next */
    if (!this.runInAsyncScope)
      this.runInAsyncScope = (fn, thisArg, ...args) => {
        this.emitBefore()
        try {
          return Reflect.apply(fn, thisArg, args)
        } finally {
          this.emitAfter()
        }
      }
  }
}

/* istanbul ignore next */
const INSPECT = util.inspect.custom || 'inspect'

const Parser = require('tap-parser')

const ownOr = require('own-or')
const ownOrEnv = require('own-or-env')
const hrtime = require('browser-process-hrtime')

class Base extends MiniPass {
  constructor (options) {
    options = options || {}
    super(options)

    this.started = false

    // establish the wrapper resource to limit the domain to this one object
    this.hook = new TapWrap(this)
    this.hook.runInAsyncScope(() =>
      this.hookDomain = new Domain((er, type) => {
        if (!er || typeof er !== 'object')
          er = { error: er }
        er.tapCaught = type
        this.threw(er)
      }))

    this.start = 0
    this.hrtime = null
    this.time = null
    this.timer = null
    this.readyToProcess = false
    this.options = options
    this.grep = ownOr(options, 'grep', [])
    this.grepInvert = ownOr(options, 'grepInvert', false)
    this.parent = ownOr(options, 'parent', null)
    this.bail = ownOrEnv(options, 'bail', 'TAP_BAIL', true)
    this.saveFixture = ownOrEnv(options, 'saveFixture', 'TAP_SAVE_FIXTURE', true)
    const name = (ownOr(options, 'name', '') || '').replace(/[\n\r\s\t]/g, ' ')
    Object.defineProperty(this, 'name', {
      value: name,
      writable: false,
      enumerable: true,
      configurable: false,
    })
    this.indent = ownOr(options, 'indent', '')
    this.silent = !!options.silent
    this.buffered = !!options.buffered || !!options.silent
    this.finished = false
    this.strict = ownOrEnv(options, 'strict', 'TAP_STRICT', true)
    this.omitVersion = !!options.omitVersion
    this.preserveWhitespace = ownOr(options, 'preserveWhitespace', true)
    this.jobs = +ownOrEnv(options, 'jobs', 'TAP_JOBS') || 0
    this.runOnly = ownOrEnv(options, 'runOnly', 'TAP_ONLY', true)
    this.setupParser(options)
    this.finished = false
    this.output = ''
    this.results = null
    this.bailedOut = false
    this.childId = +ownOrEnv(options, 'childId', 'TAP_CHILD_ID')
      || /* istanbul ignore next */ 0
    const skip = ownOr(options, 'skip', false)
    const todo = ownOr(options, 'todo', false)
    if (skip || todo)
      this.main = Base.prototype.main

    this.counts = {
      total: 0,
      pass: 0,
      fail: 0,
      skip: 0,
      todo: 0,
    }

    const ctx = ownOr(options, 'context', null)
    delete options.context
    this.context = typeof ctx === 'object' || ctx instanceof Object
      ? Object.create(ctx) : ctx

    this.lists = {
      fail: [],
      todo: [],
      skip: [],
    }

    const doDebug = typeof options.debug === 'boolean' ? options.debug
      : /\btap\b/i.test(process.env.NODE_DEBUG || '')

    if (doDebug)
      this.debug = debug(this.name)
  }

  passing () {
    return this.parser.ok
  }

  setTimeout (n, quiet) {
    if (!this.hrtime)
      this.hrtime = hrtime()

    if (!this.start)
      this.start = Date.now()

    if (!n) {
      clearTimeout(this.timer)
      this.timer = null
    } else {
      if (this.timer)
        clearTimeout(this.timer)

      this.timer = setTimeout(() => this.timeout(), n)
      /* istanbul ignore else */
      if (this.timer.unref) {
        this.timer.duration = n
        this.timer.unref()
      }
    }
  }

  threw (er, extra, proxy) {
    this.hook.emitDestroy()
    this.hookDomain.destroy()
    if (!er || typeof er !== 'object')
      er = { error: er }
    if (this.name && !proxy)
      er.test = this.name

    const message = er.message

    if (!extra)
      extra = extraFromError(er, extra, this.options)

    if (this.results) {
      this.results.ok = false
      if (this.parent)
        this.parent.threw(er, extra, true)
      else if (!er.stack)
        console.error(er)
      else {
        if (message)
          er.message = message
        delete extra.stack
        delete extra.at
        console.error('%s: %s', er.name || 'Error', message)
        console.error(er.stack.split(/\n/).slice(1).join('\n'))
        console.error(extra)
      }
    } else
      this.parser.ok = false

    return extra
  }

  timeout (options) {
    this.setTimeout(false)
    const er = new Error('timeout!')
    options = options || {}
    options.expired = options.expired || this.name
    this.emit('timeout', this.threw(new Error('timeout!'), options))
  }

  runMain (cb) {
    this.started = true
    this.hook.runInAsyncScope(this.main, this, cb)
  }

  main (cb) {
    cb()
  }

  online (line) {
    this.debug('LINE %j', line)
    return this.write(this.indent + line)
  }

  write (c, e) {
    assert.equal(typeof c, 'string')
    assert.equal(c.substr(-1), '\n')

    if (this.buffered) {
      this.output += c
      return true
    }

    return super.write(c, e)
  }

  onbail (reason) {
    this.bailedOut = reason || true
    this.emit('bailout', reason)
  }

  oncomplete (results) {
    if (this.hrtime) {
      this.hrtime = hrtime(this.hrtime)
      this.time = results.time ||
        Math.round(this.hrtime[0] * 1e6 + this.hrtime[1] / 1e3) / 1e3
    }

    this.debug('ONCOMPLETE %j %j', this.name, results)

    if (this.results)
      Object.keys(this.results)
        .forEach(k => results[k] = this.results[k])

    this.results = results
    this.emit('complete', results)
    const failures = results.failures
      .filter(f => f.tapError)
      .map(f => {
        delete f.diag
        delete f.ok
        return f
      })

    if (failures.length)
      this.options.failures = failures

    this.onbeforeend()
    // if we're piping, and buffered, then it means we need to hold off
    // on emitting 'end' and calling ondone() until the pipes clear out.
    if (this.pipes.length && this.buffer.length)
      super.end()
    else
      this.emit('end')
  }

  onbeforeend () {}
  ondone () {}

  emit (ev, data) {
    if (ev === 'end') {
      const ret = super.emit(ev, data)
      this.ondone()
      this.hook.emitDestroy()
      this.hookDomain.destroy()
      return ret
    } else
      return super.emit(ev, data)
  }

  setupParser (options) {
    this.parser = options.parser || new Parser({
      bail: this.bail,
      strict: this.strict,
      omitVersion: this.omitVersion,
      preserveWhitespace: this.preserveWhitespace,
      name: this.name,
    })
    this.parser.on('line', l => this.online(l))
    this.parser.once('bailout', reason => this.onbail(reason))
    this.parser.on('complete', result => this.oncomplete(result))

    this.parser.on('result', () => this.counts.total++)
    this.parser.on('pass', res => this.counts.pass++)
    this.parser.on('todo', res => {
      this.counts.todo++
      this.lists.todo.push(res)
    })
    this.parser.on('skip', res => {
      this.counts.skip++
      this.lists.skip.push(res)
    })
    this.parser.on('fail', res => {
      this.counts.fail++
      this.lists.fail.push(res)
    })
  }

  [INSPECT] () {
    return this.constructor.name + ' ' + util.inspect({
      name: this.name,
      time: this.time,
      hrtime: this.hrtime,
      jobs: this.jobs,
      buffered: this.buffered,
      occupied: this.occupied,
      pool: this.pool,
      queue: this.queue,
      subtests: this.subtests,
      output: this.output,
      skip: ownOr(this.options, 'skip', false),
      todo: ownOr(this.options, 'todo', false),
      only: ownOr(this.options, 'only', false),
      results: this.results,
      options: [
        'autoend',
        'command',
        'args',
        'stdio',
        'env',
        'cwd',
        'exitCode',
        'signal',
        'expired',
        'timeout',
        'at',
        'skip',
        'todo',
        'only',
        'runOnly'
      ].filter(k => this.options[k] !== undefined)
        .reduce((s, k) => (s[k] = this.options[k], s), {})
    })
  }

  debug () {}

}

const debug = name => (...args) => {
  const prefix = `TAP ${process.pid} ${name}: `
  const msg = util.format(...args).trim()
  console.error(prefix + msg.split('\n').join(`\n${prefix}`))
}

module.exports = Base