HOME


Mini Shell 1.0
Redirecting to https://devs.lapieza.net/iniciar-sesion Redirecting to https://devs.lapieza.net/iniciar-sesion.
DIR: /proc/1784574/task/1784574/root/usr/share/nodejs/tap-parser/
Upload File :
Current File : //proc/1784574/task/1784574/root/usr/share/nodejs/tap-parser/index.js
'use strict'

// Transforms a stream of TAP into a stream of result objects
// and string comments.  Emits "results" event with summary.
const MiniPass = require('minipass')

// this isn't for performance or anything, it just confuses vim's
// brace-matching to have these in the middle of functions, and
// i'm too lazy to dig into vim-javascript to fix it.
const OPEN_BRACE_EOL = /\{\s*$/
const SPACE_OPEN_BRACE_EOL = / \{$/

// used by the Parser.parse() method
const etoa = require('events-to-array')

// used by Parser.stringify() and Parser.parse() in flattening mode
const getId = () => {
  const id = () => id.current++
  id.current = 1
  return id
}

const yaml = require('tap-yaml')

// every line outside of a yaml block is one of these things, or
// a comment, or garbage.
const lineTypes = {
  testPoint: /^(not )?ok(?: ([0-9]+))?(?:(?: -)?( .*?))?(\{?)\n$/,
  pragma: /^pragma ([+-])([a-zA-Z0-9_-]+)\n$/,
  bailout: /^bail out!(.*)\n$/i,
  version: /^TAP version ([0-9]+)\n$/i,
  childVersion: /^(    )+TAP version ([0-9]+)\n$/i,
  plan: /^([0-9]+)\.\.([0-9]+)(?:\s+(?:#\s*(.*)))?\n$/,
  subtest: /^# Subtest(?:: (.*))?\n$/,
  subtestIndent: /^    # Subtest(?:: (.*))?\n$/,
  comment: /^\s*#.*\n$/
}

const lineType = line => {
  for (let t in lineTypes) {
    const match = line.match(lineTypes[t])
    if (match)
      return [t, match]
  }
  return null
}

const parseDirective = line => {
  if (!line.trim())
    return false

  line = line.replace(OPEN_BRACE_EOL, '').trim()
  const time = line.match(/^time=((?:[1-9][0-9]*|0)(?:\.[0-9]+)?)(ms|s)$/i)
  if (time) {
    let n = +time[1]
    if (time[2] === 's') {
      // JS does weird things with floats.  Round it off a bit.
      n *= 1000000
      n = Math.round(n)
      n /= 1000
    }
    return [ 'time', n ]
  }

  const type = line.match(/^(todo|skip)(?:\S*)\b(.*)$/i)
  if (!type)
    return false

  return [
    type[1].toLowerCase(),
    type[2].trim() || true,
  ]
}

class Result {
  constructor (parsed, count) {
    const ok = !parsed[1]
    const id = +(parsed[2] || 0)
    let buffered = parsed[4]
    this.ok = ok
    if (parsed[2])
      this.id = id

    let rest = parsed[3] || ''
    let name
    // We know at this point the parsed result cannot contain \n,
    // so we can leverage that as a placeholder.
    // first, replace any PAIR of \ chars with \n
    // then, split on any # that is not preceeded by \
    // the first of these is definitely the description
    // the rest is the directive, if recognized, otherwise
    // we just lump it onto the description, but escaped.
    // then any \n chars in either are turned into \ (just one)

    // escape \ with \
    rest = rest.replace(/(\\\\)/g, '\n')

    rest = rest.split(/(?<=\s|^)(?<!\\)#/g)
    name = rest.shift().replace(/\\#/g, '#').replace(/\n/g, '\\')
    rest = rest.join('#').replace(/\\#/g, '#').replace(/\n/g, '\\')

    // now, let's see if there's a directive in there.
    const dir = parseDirective(rest.trim())
    if (!dir)
      name += (rest ? '#' + rest : '') + buffered
    else {
      // handle buffered subtests with todo/skip on them, like
      // ok 1 - bar # todo foo {\n
      const dirKey = dir[0]
      const dirValue = dir[1]
      this[dirKey] = dirValue
    }

    if (OPEN_BRACE_EOL.test(name)) {
      name = name.replace(OPEN_BRACE_EOL, '')
      buffered = '{'
    }

    if (buffered === '{')
      this.buffered = true

    if (name)
      this.name = name.trim()
  }
}

class Parser extends MiniPass {
  static parse (str, options = {}) {
    const { flat = false } = options
    const ignore = [
      'pipe',
      'unpipe',
      'prefinish',
      'finish',
      'line',
      'pass',
      'fail',
      'todo',
      'skip',
      'result',
    ]
    if (flat)
      ignore.push('assert', 'child', 'plan', 'complete')
    const parser = new Parser(options)
    const events = etoa(parser, ignore)
    if (flat) {
      const id = getId()
      parser.on('result', res => {
        const name = []
        if (res.fullname)
          name.push(res.fullname)
        if (res.name)
          name.push(res.name)
        res.name = name.join(' > ').trim()
        res.fullname = ''
        res.id = id()
        events.push(['assert', res])
      })
      parser.on('complete', res => {
        if (!res.bailout)
          events.push(['plan', { end: id.current - 1, start: 1 }])
        events.push(['complete', res])
      })
    }

    parser.end(str)
    return events
  }

  static stringify (msg, { flat = false, indent = '', id = getId() } = {}) {
    const ind = flat ? '' : indent
    return ind + msg.map(item => {
      switch (item[0]) {
        case 'child':
          const comment = item[1][0]
          const child = item[1].slice(1)
          return Parser.stringify([comment], { flat, indent: '', id }) +
            Parser.stringify(child, { flat, indent: '    ', id })

        case 'version':
          return 'TAP version ' + item[1] + '\n'

        case 'plan':
          if (flat) {
            if (indent !== '')
              return ''
            item[1].start = 1
            item[1].end = id.current - 1
          }
          return item[1].start + '..' + item[1].end
            + (item[1].comment ? ' # ' + esc(item[1].comment) : '') + '\n'

        case 'pragma':
          return 'pragma ' + (item[2] ? '+' : '-') + item[1] + '\n'

        case 'bailout':
          return 'Bail out!' + (item[1] ? (' ' + esc(item[1])) : '') + '\n'

        case 'assert':
          const res = item[1]
          if (flat) {
            res.id = id()
            const name = []
            if (res.fullname)
              name.push(res.fullname)
            if (res.name)
              name.push(res.name)
            res.name = name.join(' > ').trim()
          }
          return (res.ok ? '' : 'not ') + 'ok' +
            (res.id !== undefined ? ' ' + res.id : '' ) +
            (res.name
              ? ' - ' + esc(res.name).replace(SPACE_OPEN_BRACE_EOL, '')
              : '') +
            (res.skip ? ' # SKIP' +
              (res.skip === true ? '' : ' ' + esc(res.skip)) : '') +
            (res.todo ? ' # TODO' +
              (res.todo === true ? '' : ' ' + esc(res.todo)) : '') +
            (res.time ? ' # time=' + res.time + 'ms' : '') +
            '\n' +
            (res.diag ?
               '  ---\n  ' +
               yaml.stringify(res.diag).split('\n').join('\n  ').trim() +
               '\n  ...\n'
               : '')

        case 'extra':
        case 'comment':
          return item[1]
      }
    }).join('').split('\n').join('\n' + ind).trim() + '\n'
  }

  constructor (options, onComplete) {
    if (typeof options === 'function') {
      onComplete = options
      options = {}
    }

    options = options || {}
    super(options)
    this.resume()

    if (onComplete)
      this.on('complete', onComplete)

    this.time = null
    this.name = options.name || ''
    this.comments = []
    this.results = null
    this.braceLevel = null
    this.parent = options.parent || null
    this.closingTestPoint = this.parent && options.closingTestPoint
    this.root = options.parent ? this.parent.root : this
    this.failures = []
    if (options.passes)
      this.passes = []
    this.level = options.level || 0

    this.pointsSeen = new Map()
    this.buffer = ''
    this.bail = !!options.bail
    this.bailingOut = false
    this.bailedOut = false
    this.syntheticBailout = false
    this.syntheticPlan = false
    this.omitVersion = !!options.omitVersion
    this.planStart = -1
    this.planEnd = -1
    this.planComment = ''
    this.yamlish = ''
    this.yind = ''
    this.child = null
    this.previousChild = null
    this.current = null
    this.maybeSubtest = null
    this.extraQueue = []
    this.buffered = options.buffered || null
    this.aborted = false
    this.preserveWhitespace = options.preserveWhitespace || false

    this.count = 0
    this.pass = 0
    this.fail = 0
    this.todo = 0
    this.skip = 0
    this.ok = true

    this.strict = options.strict || false
    this.pragmas = { strict: this.strict }

    this.postPlan = false
  }

  get fullname () {
    return ((this.parent ? this.parent.fullname + ' ' : '') +
      (this.name || '')).trim()
  }

  tapError (error, line) {
    if (line)
      this.emit('line', line)
    this.ok = false
    this.fail ++
    if (typeof error === 'string') {
      error = {
        tapError: error
      }
    }
    this.failures.push(error)
  }

  parseTestPoint (testPoint, line) {
    // need to hold off on this when we have a child so we can
    // associate the closing test point with the test.
    if (!this.child)
      this.emitResult()

    if (this.bailedOut)
      return

    const resId = testPoint[2]

    const res = new Result(testPoint, this.count)

    if (resId && this.planStart !== -1) {
      const lessThanStart = res.id < this.planStart
      const greaterThanEnd = res.id > this.planEnd
      if (lessThanStart || greaterThanEnd) {
        if (lessThanStart)
          res.tapError = 'id less than plan start'
        else
          res.tapError = 'id greater than plan end'
        res.plan = { start: this.planStart, end: this.planEnd }
        this.tapError(res)
      }
    }

    if (resId && this.pointsSeen.has(res.id)) {
      res.tapError = 'test point id ' + resId + ' appears multiple times'
      res.previous = this.pointsSeen.get(res.id)
      this.tapError(res)
    } else if (resId) {
      this.pointsSeen.set(res.id, res)
    }

    if (this.child) {
      if (!this.child.closingTestPoint)
        this.child.closingTestPoint = res
      this.emitResult()
      // can only bail out here in the case of a child with broken diags
      // anything else would have bailed out already.
      if (this.bailedOut)
        return
    }

    this.emit('line', line)

    if (!res.skip && !res.todo)
      this.ok = this.ok && res.ok

    // hold onto it, because we might get yamlish diagnostics
    this.current = res
  }

  nonTap (data, didLine) {
    if (this.bailingOut && /^( {4})*\}\n$/.test(data))
      return

    if (this.strict) {
      const err = {
        tapError: 'Non-TAP data encountered in strict mode',
        data: data
      }
      this.tapError(err)
      if (this.parent)
        this.parent.tapError(err)
    }

    // emit each line, then the extra as a whole
    if (!didLine)
      data.split('\n').slice(0, -1).forEach(line => {
        line += '\n'
        if (this.current || this.extraQueue.length)
          this.extraQueue.push(['line', line])
        else
          this.emit('line', line)
      })

    this.emitExtra(data)
  }

  emitExtra (data, fromChild) {
    if (this.parent)
      this.parent.emitExtra(
        data.replace(/\n$/, '').replace(/^/gm, '    ') + '\n', true
      )
    else if (!fromChild && (this.current || this.extraQueue.length))
      this.extraQueue.push(['extra', data])
    else
      this.emit('extra', data)
  }

  plan (start, end, comment, line) {
    // not allowed to have more than one plan
    if (this.planStart !== -1) {
      this.nonTap(line)
      return
    }

    // can't put a plan in a child.
    if (this.child || this.yind) {
      this.nonTap(line)
      return
    }

    this.emitResult()
    if (this.bailedOut)
      return

    // 1..0 is a special case. Otherwise, end must be >= start
    if (end < start && end !== 0 && start !== 1) {
      if (this.strict)
        this.tapError({
          tapError: 'plan end cannot be less than plan start',
          plan: { start, end },
        }, line)
      else
        this.nonTap(line)
      return
    }

    this.planStart = start
    this.planEnd = end
    const p = { start: start, end: end }
    if (comment)
      this.planComment = p.comment = comment

    // This means that the plan is coming at the END of all the tests
    // Plans MUST be either at the beginning or the very end.  We treat
    // plans like '1..0' the same, since they indicate that no tests
    // will be coming.
    if (this.count !== 0 || this.planEnd === 0) {
      const seen = new Set()
      for (const [id, res] of this.pointsSeen.entries()) {
        const tapError = id < start ? 'id less than plan start'
          : id > end ? 'id greater than plan end'
          : null
        if (tapError) {
          seen.add(tapError)
          res.tapError = tapError
          res.plan = { start, end }
          this.tapError(res)
        }
      }
      this.postPlan = true
    }

    this.emit('line', line)
    this.emit('plan', p)
  }

  resetYamlish () {
    this.yind = ''
    this.yamlish = ''
  }

  // that moment when you realize it's not what you thought it was
  yamlGarbage () {
    const yamlGarbage = this.yind + '---\n' + this.yamlish
    this.emitResult()
    if (this.bailedOut)
      return
    this.nonTap(yamlGarbage, true)
  }

  yamlishLine (line) {
    if (line === this.yind + '...\n') {
      // end the yaml block
      this.processYamlish()
    } else {
      this.yamlish += line
    }
  }

  processYamlish () {
    const yamlish = this.yamlish
    this.resetYamlish()

    let diags
    try {
      diags = yaml.parse(yamlish)
    } catch (er) {
      this.nonTap(this.yind + '---\n' + yamlish + this.yind + '...\n', true)
      return
    }

    this.current.diag = diags
    // we still don't emit the result here yet, to support diags
    // that come ahead of buffered subtests.
  }

  write (chunk, encoding, cb) {
    if (this.aborted)
      return

    if (typeof encoding === 'string' && encoding !== 'utf8')
      chunk = Buffer.from(chunk, encoding)

    if (Buffer.isBuffer(chunk))
      chunk += ''

    if (typeof encoding === 'function') {
      cb = encoding
      encoding = null
    }

    this.buffer += chunk
    do {
      const match = this.buffer.match(/^.*\r?\n/)
      if (!match)
        break

      this.buffer = this.buffer.substring(match[0].length)
      this.parse(match[0])
    } while (this.buffer.length)

    if (cb)
      process.nextTick(cb)

    return true
  }

  end (chunk, encoding, cb) {
    if (chunk) {
      if (typeof encoding === 'function') {
        cb = encoding
        encoding = null
      }
      this.write(chunk, encoding)
    }

    if (this.buffer)
      this.write('\n')

    // if we have yamlish, means we didn't finish with a ...
    if (this.yamlish)
      this.yamlGarbage()

    this.emitResult()

    if (this.syntheticBailout && this.level === 0) {
      this.syntheticBailout = false
      let reason = this.bailedOut
      if (reason === true)
        reason = ''
      else
        reason = ' ' + reason
      this.emit('line', 'Bail out!' + reason + '\n')
    }

    let skipAll

    if (this.planEnd === 0 && this.planStart === 1) {
      skipAll = true
      if (this.count === 0) {
        this.ok = true
      } else {
        this.tapError('Plan of 1..0, but test points encountered')
      }
    } else if (!this.bailedOut && this.planStart === -1) {
      if (this.count === 0 && !this.syntheticPlan) {
        this.syntheticPlan = true
        if (this.buffered) {
          this.planStart = 1
          this.planEnd = 0
        } else
          this.plan(1, 0, 'no tests found', '1..0 # no tests found\n')
        skipAll = true
      } else {
        this.tapError('no plan')
      }
    } else if (this.ok && this.count !== (this.planEnd - this.planStart + 1)) {
      this.tapError('incorrect number of tests')
    }

    this.emitComplete(skipAll)
    if (cb)
      process.nextTick(cb)

    return this
  }

  emitComplete (skipAll) {
    if (!this.results) {
      const res = this.results = new FinalResults(!!skipAll, this)

      if (!res.bailout) {
        // comment a bit at the end so we know what happened.
        // but don't repeat these comments if they're already present.
        if (res.plan.end !== res.count)
          this.emitComment('test count(' + res.count +
                           ') != plan(' + res.plan.end + ')', false, true)

        if (res.fail > 0 && !res.ok)
          this.emitComment('failed ' + res.fail +
                           (res.count > 1 ? ' of ' + res.count + ' tests'
                            : ' test'),
                           false, true)

        if (res.todo > 0)
          this.emitComment('todo: ' + res.todo, false, true)

        if (res.skip > 0)
          this.emitComment('skip: ' + res.skip, false, true)
      }

      this.emit('complete', this.results)
    }
  }

  version (version, line) {
    // If version is specified, must be at the very beginning.
    if (version >= 13 &&
        this.planStart === -1 &&
        this.count === 0 &&
        !this.current) {
      this.emit('line', line)
      this.emit('version', version)
    } else
      this.nonTap(line)
  }

  pragma (key, value, line) {
    // can't put a pragma in a child or yaml block
    if (this.child) {
      this.nonTap(line)
      return
    }

    this.emitResult()
    if (this.bailedOut)
      return
    // only the 'strict' pragma is currently relevant
    if (key === 'strict') {
      this.strict = value
    }
    this.pragmas[key] = value
    this.emit('line', line)
    this.emit('pragma', key, value)
  }

  bailout (reason, synthetic) {
    this.syntheticBailout = synthetic

    if (this.bailingOut)
      return

    // Guard because emitting a result can trigger a forced bailout
    // if the harness decides that failures should be bailouts.
    this.bailingOut = reason || true

    if (!synthetic)
      this.emitResult()
    else
      this.current = null

    this.bailedOut = this.bailingOut
    this.ok = false
    if (!synthetic) {
      // synthetic bailouts get emitted on end
      let line = 'Bail out!'
      if (reason)
        line += ' ' + reason
      this.emit('line', line + '\n')
    }
    this.emit('bailout', reason)
    if (this.parent) {
      this.end()
      this.parent.bailout(reason, true)
    }
  }

  clearExtraQueue () {
    for (let c = 0; c < this.extraQueue.length; c++) {
      this.emit(this.extraQueue[c][0], this.extraQueue[c][1])
    }
    this.extraQueue.length = 0
  }

  endChild () {
    if (this.child && (!this.bailingOut || this.child.count)) {
      if (this.child.closingTestPoint)
        this.child.time = this.child.closingTestPoint.time || null
      this.previousChild = this.child
      this.child.end()
      this.child = null
    }
  }

  emitResult () {
    if (this.bailedOut)
      return

    this.endChild()
    this.resetYamlish()

    if (!this.current)
      return this.clearExtraQueue()

    const res = this.current
    this.current = null

    this.count++
    if (res.ok) {
      this.pass++
      if (this.passes)
        this.passes.push(res)
    } else {
      this.fail++
      if (!res.todo && !res.skip) {
        this.ok = false
        this.failures.push(res)
      }
    }

    if (res.skip)
      this.skip++

    if (res.todo)
      this.todo++

    this.emitAssert(res)
    if (this.bail && !res.ok && !res.todo && !res.skip && !this.bailingOut) {
      this.maybeChild = null
      const ind = new Array(this.level + 1).join('    ')
      let p
      for (p = this; p.parent; p = p.parent);
      const bailName = res.name ? ' ' + res.name : ''
      p.parse(ind + 'Bail out!' + bailName + '\n')
    }
    this.clearExtraQueue()
  }

  // TODO: We COULD say that any "relevant tap" line that's indented
  // by 4 spaces starts a child test, and just call it 'unnamed' if
  // it does not have a prefix comment.  In that case, any number of
  // 4-space indents can be plucked off to try to find a relevant
  // TAP line type, and if so, start the unnamed child.
  startChild (line) {
    const maybeBuffered = this.current && this.current.buffered
    const unindentStream = !maybeBuffered && this.maybeChild
    const indentStream = !maybeBuffered && !unindentStream &&
      lineTypes.subtestIndent.test(line)
    const unnamed = !maybeBuffered && !unindentStream && !indentStream

    // If we have any other result waiting in the wings, we need to emit
    // that now.  A buffered test emits its test point at the *end* of
    // the child subtest block, so as to match streamed test semantics.
    if (!maybeBuffered)
      this.emitResult()

    if (this.bailedOut)
      return

    this.child = new Parser({
      bail: this.bail,
      parent: this,
      level: this.level + 1,
      buffered: maybeBuffered,
      closingTestPoint: maybeBuffered && this.current,
      preserveWhitespace: this.preserveWhitespace,
      omitVersion: true,
      strict: this.strict
    })

    this.child.on('complete', results => {
      if (!results.ok)
        this.ok = false
    })

    this.child.on('line', l => {
      if (l.trim() || this.preserveWhitespace)
        l = '    ' + l
      this.emit('line', l)
    })

    // Canonicalize the parsing result of any kind of subtest
    // if it's a buffered subtest or a non-indented Subtest directive,
    // then synthetically emit the Subtest comment
    line = line.substring(4)
    let subtestComment
    if (indentStream) {
      subtestComment = line
      line = null
    } else if (maybeBuffered) {
      subtestComment = '# Subtest: ' + this.current.name + '\n'
    } else {
      subtestComment = this.maybeChild || '# Subtest\n'
    }

    this.maybeChild = null
    this.child.name = subtestComment.substring('# Subtest: '.length).trim()

    // at some point, we may wish to move 100% to preferring
    // the Subtest comment on the parent level.  If so, uncomment
    // this line, and remove the child.emitComment below.
    // this.emit('comment', subtestComment)
    if (!this.child.buffered)
      this.emit('line', subtestComment)
    this.emit('child', this.child)
    this.child.emitComment(subtestComment, true)
    if (line)
      this.child.parse(line)
  }

  abort (message, extra) {
    if (this.child) {
      const b = this.child.buffered
      this.child.abort(message, extra)
      extra = null
      if (b)
        this.write('\n}\n')
    }

    let dump
    if (extra && Object.keys(extra).length) {
      try {
        dump = yaml.stringify(extra).trimRight()
      } catch (er) {}
    }

    let y
    if (dump)
      y = '  ---\n  ' + dump.split('\n').join('\n  ') + '\n  ...\n'
    else
      y = '\n'

    let n = (this.count || 0) + 1
    if (this.current)
      n += 1

    if (this.planEnd !== -1 && this.planEnd < n && this.parent) {
      // skip it, let the parent do this.
      this.aborted = true
      return
    }

    let ind = '' // new Array(this.level + 1).join('    ')
    message = message.replace(/[\n\r\s\t]/g, ' ')
    let point = '\nnot ok ' + n + ' - ' + message + '\n' + y

    if (this.planEnd === -1)
      point += '1..' + n + '\n'

    this.write(point)
    this.aborted = true
    this.end()
  }

  emitAssert (res) {
    res.fullname = this.fullname

    this.emit('assert', res)

    // see if we need to surface to the top level
    if (this.child || this.previousChild) {
      const c = this.child || this.previousChild
      this.previousChild = null
      if (res.name === c.name &&
          res.ok === c.results.ok &&
          c.results.count &&
          !res.todo && !res.skip) {
        // just procedural, ignore it
        return
      }
    }

    // surface result to the top level parser
    this.root.emit('result', res)
    if (res.skip)
      this.root.emit('skip', res)
    else if (res.todo)
      this.root.emit('todo', res)
    else if (!res.ok)
      this.root.emit('fail', res)
    else
      this.root.emit('pass', res)
  }

  emitComment (line, skipLine, noDuplicate) {
    if (line.trim().charAt(0) !== '#')
      line = '# ' + line

    if (line.slice(-1) !== '\n')
      line += '\n'

    if (noDuplicate && this.comments.indexOf(line) !== -1)
      return

    this.comments.push(line)
    const dir = parseDirective(line.replace(/^\s*#\s*/, '').trim())
    if (dir[0] === 'time' && typeof dir[1] === 'number')
      this.time = dir[1]

    if (this.current || this.extraQueue.length) {
      // no way to get here with skipLine being true
      this.extraQueue.push(['line', line])
      this.extraQueue.push(['comment', line])
    } else {
      if (!skipLine)
        this.emit('line', line)
      this.emit('comment', line)
    }
  }

  parse (line) {
    // normalize line endings
    line = line.replace(/\r\n$/, '\n')

    // sometimes empty lines get trimmed, but are still part of
    // a subtest or a yaml block.  Otherwise, nothing to parse!
    if (line === '\n') {
      if (this.child)
        line = '    ' + line
      else if (this.yind)
        line = this.yind + line
    }

    // If we're bailing out, then the only thing we want to see is the
    // end of a buffered child test.  Anything else should be ignored.
    // But!  if we're bailing out a nested child, and ANOTHER nested child
    // comes after that one, then we don't want the second child's } to
    // also show up, or it looks weird.
    if (this.bailingOut) {
      if (!/^\s*}\n$/.test(line))
        return
      else if (!this.braceLevel || line.length < this.braceLevel)
        this.braceLevel = line.length
      else
        return
    }

    // This allows omitting even parsing the version if the test is
    // an indented child test.  Several parsers get upset when they
    // see an indented version field.
    if (this.omitVersion && lineTypes.version.test(line) && !this.yind)
      return

    // check to see if the line is indented.
    // if it is, then it's either a subtest, yaml, or garbage.
    const indent = line.match(/^[ \t]*/)[0]
    if (indent) {
      this.parseIndent(line, indent)
      return
    }

    // In any case where we're going to emitResult, that can trigger
    // a bailout, so we need to only emit the line once we know that
    // isn't happening, to prevent cases where there's a bailout, and
    // then one more line of output.  That'll also prevent the case
    // where the test point is emitted AFTER the line that follows it.

    // buffered subtests must end with a }
    if (this.child && this.child.buffered && line === '}\n') {
      this.endChild()
      this.emit('line', line)
      this.emitResult()
      return
    }

    // just a \n, emit only if we care about whitespace
    const validLine = this.preserveWhitespace || line.trim() || this.yind
    if (line === '\n')
      return validLine && this.emit('line', line)

    // buffered subtest with diagnostics
    if (this.current && line === '{\n' &&
        this.current.diag &&
        !this.current.buffered &&
        !this.child) {
      this.emit('line', line)
      this.current.buffered = true
      return
    }

    // now we know it's not indented, so if it's either valid tap
    // or garbage.  Get the type of line.
    const type = lineType(line)
    if (!type) {
      this.nonTap(line)
      return
    }

    if (type[0] === 'comment') {
      this.emitComment(line)
      return
    }

    // if we have any yamlish, it's garbage now.  We tolerate non-TAP and
    // comments in the midst of yaml (though, perhaps, that's questionable
    // behavior), but any actual TAP means that the yaml block was just
    // not valid.
    if (this.yind)
      this.yamlGarbage()

    // If it's anything other than a comment or garbage, then any
    // maybeChild is just an unsatisfied promise.
    if (this.maybeChild) {
      this.emitComment(this.maybeChild)
      this.maybeChild = null
    }

    // nothing but comments can come after a trailing plan
    if (this.postPlan) {
      this.nonTap(line)
      return
    }

    // ok, now it's maybe a thing
    if (type[0] === 'bailout') {
      this.bailout(unesc(type[1][1].trim()), false)
      return
    }

    if (type[0] === 'pragma') {
      const pragma = type[1]
      this.pragma(pragma[2], pragma[1] === '+', line)
      return
    }

    if (type[0] === 'version') {
      const version = type[1]
      this.version(parseInt(version[1], 10), line)
      return
    }

    if (type[0] === 'plan') {
      const plan = type[1]
      this.plan(+plan[1], +plan[2], unesc((plan[3] || '')).trim(), line)
      return
    }

    // streamed subtests will end when this test point is emitted
    if (type[0] === 'testPoint') {
      // note: it's weird, but possible, to have a testpoint ending in
      // { before a streamed subtest which ends with a test point
      // instead of a }.  In this case, the parser gets confused, but
      // also, even beginning to handle that means doing a much more
      // involved multi-line parse.  By that point, the subtest block
      // has already been emitted as a 'child' event, so it's too late
      // to really do the optimal thing.  The only way around would be
      // to buffer up everything and do a multi-line parse.  This is
      // rare and weird, and a multi-line parse would be a bigger
      // rewrite, so I'm allowing it as it currently is.
      this.parseTestPoint(type[1], line)
      return
    }

    // We already detected nontap up above, so the only case left
    // should be a `# Subtest:` comment.  Ignore for coverage, but
    // include the error here just for good measure.
    /* istanbul ignore else */
    if (type[0] === 'subtest') {
      // this is potentially a subtest.  Not indented.
      // hold until later.
      this.maybeChild = line
    } else {
      throw new Error('Unhandled case: ' + type[0])
    }
  }

  parseIndent (line, indent) {
    // still belongs to the child, so pass it along.
    if (this.child && line.substring(0, 4) === '    ') {
      line = line.substring(4)
      this.child.write(line)
      return
    }

    // one of:
    // - continuing yaml block
    // - starting yaml block
    // - ending yaml block
    // - body of a new child subtest that was previously introduced
    // - An indented subtest directive
    // - A comment, or garbage

    // continuing/ending yaml block
    if (this.yind) {
      if (line.indexOf(this.yind) === 0) {
        this.emit('line', line)
        this.yamlishLine(line)
        return
      } else {
        // oops!  that was not actually yamlish, I guess.
        // this is a case where the indent is shortened mid-yamlish block
        // treat existing yaml as garbage, continue parsing this line
        this.yamlGarbage()
      }
    }


    // start a yaml block under a test point
    if (this.current && !this.yind && line === indent + '---\n') {
      this.yind = indent
      this.emit('line', line)
      return
    }

    // at this point, not yamlish, and not an existing child test.
    // We may have already seen an unindented Subtest directive, or
    // a test point that ended in { indicating a buffered subtest
    // Child tests are always indented 4 spaces.
    if (line.substring(0, 4) === '    ') {
      if (this.maybeChild ||
          this.current && this.current.buffered ||
          lineTypes.subtestIndent.test(line)) {
        this.startChild(line)
        return
      }

      // It's _something_ indented, if the indentation is divisible by
      // 4 spaces, and the result is actual TAP of some sort, then do
      // a child subtest for it as well.
      //
      // This will lead to some ambiguity in cases where there are multiple
      // levels of non-signaled subtests, but a Subtest comment in the
      // middle of them, which may or may not be considered "indented"
      // See the subtest-no-comment-mid-comment fixture for an example
      // of this.  As it happens, the preference is towards an indented
      // Subtest comment as the interpretation, which is the only possible
      // way to resolve this, since otherwise there's no way to distinguish
      // between an anonymous subtest with a non-indented Subtest comment,
      // and an indented Subtest comment.
      const s = line.match(/( {4})+(.*\n)$/)
      if (s[2].charAt(0) !== ' ') {
        // integer number of indentations.
        const type = lineType(s[2])
        if (type) {
          if (type[0] === 'comment') {
            this.emit('line', line)
            this.emitComment(line)
          } else {
            // it's relevant!  start as an "unnamed" child subtest
            this.startChild(line)
          }
          return
        }
      }
    }

    // at this point, it's either a non-subtest comment, or garbage.

    if (lineTypes.comment.test(line)) {
      this.emitComment(line)
      return
    }

    this.nonTap(line)
  }
}

// turn \ into \\ and # into \#, for stringifying back to TAP
const esc = str => str
  .replace(/\\/g, '\\\\')
  .replace(/#/g, '\\#')
  .trim()

const unesc = str => str
  .replace(/(\\\\)/g, '\u0000')
  .replace(/\\#/g, '#')
  .replace(/\u0000/g, '\\')
  .trim()

class FinalResults {
  constructor (skipAll, self) {
    this.ok = self.ok
    this.count = self.count
    this.pass = self.pass
    this.fail = self.fail || 0
    this.bailout = self.bailedOut || false
    this.todo = self.todo || 0
    this.skip = skipAll ? self.count : self.skip || 0
    this.plan = new FinalPlan(skipAll, self)
    this.failures = self.failures
    this.time = self.time
    if (self.passes)
      this.passes = self.passes
  }
}

class FinalPlan {
  constructor (skipAll, self) {
    this.start = self.planStart === -1 ? null : self.planStart
    this.end = self.planStart === -1 ? null : self.planEnd
    this.skipAll = skipAll
    this.skipReason = skipAll ? self.planComment : ''
    this.comment = self.planComment || ''
  }
}

module.exports = Parser