#!/usr/bin/env node

// default to no color if requested via standard environment var
if (process.env.NO_COLOR === '1') {
  process.env.TAP_COLORS = '0'
  process.env.FORCE_COLOR = '0'
}

const signalExit = require('signal-exit')
const opener = require('opener')
const node = process.execPath
const fs = require('fs')
const fg = require('foreground-child')
const {spawn, spawnSync} = require('child_process')
const nycBin = require.resolve(
  'nyc/' + require('nyc/package.json').bin.nyc
)
const glob = require('glob')
const isexe = require('isexe')
const yaml = require('tap-yaml')
const path = require('path')
const exists = require('fs-exists-cached').sync

const maybeResolve = id => {
  try {
    return require.resolve(id)
  } catch (er) {}
}
const tsNode = maybeResolve('ts-node/register')
const flowNode = maybeResolve('flow-remove-types/register')
const jsx = require.resolve('./jsx.js')
const coverallsBin = maybeResolve('coveralls/bin/coveralls.js')

const which = require('which')
const {ProcessDB} = require('istanbul-lib-processinfo')
const rimraf = require('rimraf').sync
const {Repl} = require('../lib/repl.js')


/* istanbul ignore next */
const debug = process.env.TAP_DEBUG === '1'
  || /\btap\b/.test(process.env.NODE_DEBUG) ? (...args) => {
    const {format} = require('util')
    const prefix = `TAP ${process.pid} RUN: `
    const msg = format(...args).trim()
    console.error(prefix + msg.split('\n').join(`\n${prefix}`))
  } : () => {}

const filesFromTest = exports.filesFromTest = (index, testFile) => {
  const set = index.externalIds[testFile]
  return !set ? null
    : Object.keys(index.files).filter(file =>
      index.files[file].includes(set.root) ||
      set.children.some(c => index.files[file].includes(c)))
}

// returns a function that tells whether a given file should be run,
// because one or more of its deps have changed.
const getChangedFilter = exports.getChangedFilter = options => {
  if (!options.changed)
    return () => true

  if (!options.coverage)
    throw new Error('--changed requires coverage to be enabled')

  const indexFile = '.nyc_output/processinfo/index.json'

  if (!fs.existsSync(indexFile))
    return () => true

  const indexDate = fs.statSync(indexFile).mtime
  const index = JSON.parse(fs.readFileSync(indexFile, 'utf8'))

  return testFile => {
    if (fs.statSync(testFile).mtime > indexDate)
      return true

    const files = filesFromTest(index, testFile)

    // if not found, probably a test file not run last time, so run it
    if (!files)
      return true

    // if the file is gone, that's a pretty big change.
    return files.some(f => !fs.existsSync(f) || fs.statSync(f).mtime > indexDate)
  }
}

const defaultFiles = options => new Promise((res, rej) => {
  debug('try to get default files')
  const findit = require('findit')
  const good = strToRegExp(options['test-regex'])
  const bad = strToRegExp(options['test-ignore'])
  const fileFilter = f => {
    f = f.replace(/\\/g, '/')
    debug('fileFilter', f)
    const parts = f.split('/')
    const include = good.test(f) &&
      !bad.test(f) &&
      !parts.includes('node_modules') &&
      !parts.includes('tap-snapshots') &&
      !parts.includes('fixtures') &&
      !parts.includes('.git') &&
      !parts.includes('.hg') &&
      !parts.some(p => /^tap-testdir-/.test(p))
    debug('include?', f, include)
    return include
  }

  const addFile = files => f => fileFilter(f) && files.push(f)

  // search in any folder that isn't node_modules, .git, or tap generated
  // these can get pretty huge, so just walking them at all is costly
  const entryFilter =
    /^((node_modules|tap-snapshots|.git|.hg|fixtures)$|tap-testdir-)/
  fs.readdir(process.cwd(), (er, entries) => {
    debug('readdir cwd', er, entries)
    Promise.all(entries.filter(entry => !entryFilter.test(entry))
      .map(entry => new Promise((res, rej) => {
        fs.lstat(entry, (er, stat) => {
          debug('lstat', entry, er, stat)
          // It's pretty unusual to have a file in cwd you can't even stat
          /* istanbul ignore next */
          if (er)
            return rej(er)
          if (stat.isFile())
            return res(fileFilter(entry) ? [entry] : [])
          if (!stat.isDirectory())
            return res([])
          const finder = findit(entry)
          const files = []
          finder.on('file', addFile(files))
          finder.on('end', () => res(files))
          finder.on('error', /* istanbul ignore next */ er => rej(er))
        })
      }))).then(a => res(a.reduce((a, i) => a.concat(i), []))).catch(rej)
  })
})

const main = options =>
  mainAsync(options).catch(er => onError(er))

const mainAsync = async options => {
  debug('main', options)

  if (require.main !== module)
    return debug('not main module, do not run tests')

  const rc = parseRcFile(options.rcfile)
  debug('rc options', rc)
  options._.update(rc)

  const pj = parsePackageJson()
  debug('package.json options', pj)
  options._.update(pj)

  if (options.files.length && !options._.length)
    options._.push(...options.files)

  // tell chalk if we want color or not.
  if (!options.color) {
    process.env.NO_COLOR = '1'
    delete process.env.FORCE_COLOR
  } else {
    delete process.env.NO_COLOR
    process.env.FORCE_COLOR = '3'
  }

  if (options.reporter === null)
    options.reporter = options.color ? 'base' : 'tap'

  if (options['dump-config']) {
    console.log(yaml.stringify(Object.keys(options).filter(k =>
      k !== 'dump-config' && k !== '_' && !/^[A-Z_]+$/.test(k)
    ).sort().reduce((set, k) =>
      (set[k] = options[k], set), {})))
    return
  }

  if (options.versions) {
    const {libtap, tapParser, tapYaml, tcompare} = require('libtap/versions')
    return console.log(yaml.stringify({
      tap: require('../package.json').version,
      libtap,
      'tap-parser': tapParser,
      nyc: require('nyc/package.json').version,
      'tap-yaml': tapYaml,
      treport: require('treport/package.json').version,
      tcompare
    }))
  }

  if (options.version)
    return console.log(require('../package.json').version)

  if (options['parser-version'])
    return console.log(require('libtap/versions').tapParser)

  if (options['nyc-version'])
    return console.log(require('nyc/package.json').version)

  if (options['nyc-help'])
    return nycHelp()

  process.stdout.on('error', er => {
    /* istanbul ignore else */
    if (er.code === 'EPIPE')
      process.exit()
    else
      throw er
  })

  if (options['libtap-settings'])
    process.env.TAP_LIBTAP_SETTINGS = path.resolve(options['libtap-settings'])

  require('../settings.js')

  // we test this directly, not from here.
  /* istanbul ignore next */
  if (options.watch)
    return new Repl(options, process.stdin, process.stdout)

  options.grep = options.grep.map(strToRegExp)

  // this is only testable by escaping from the covered environment
  /* istanbul ignore next */
  if (fs.existsSync('.nyc_output') &&
      options._.length === 0 &&
      (options['coverage-report'] &&
        options._.explicit.has('coverage-report') ||
      options['check-coverage'] &&
        options._.explicit.has('check-coverage')))
    return runCoverageReportOnly(options)

  try {
    debug('try to get default files?', options._.length === 0)
    if (options._.length === 0)
      options._.push.apply(options._, await defaultFiles(options))
    debug('added default files', options._)
  } /* istanbul ignore next */ catch (er) /* istanbul ignore next */ {
    // This gets tested on Mac, but not Linux/travis, and is
    // somewhat challenging to do in a cross-platform way.
    /* istanbul ignore next */
    return require('../lib/tap.js').threw(er)
  }

  options.files = globFiles(options._)
  debug('after globbing', options.files)

  if (options['output-dir'] !== null)
    require('../settings.js').mkdirRecursiveSync(options['output-dir'])

  if (options.files.length === 1 && options.files[0] === '-') {
    debug('do stdin only')
    setupTapEnv(options)
    stdinOnly(options)
    return
  }

  options.saved = readSaveFile(options)
  options.changedFilter = getChangedFilter(options)

  /* istanbul ignore next */
  if (options.coverage && !process.env.NYC_CONFIG)
    respawnWithCoverage(options)
  else {
    setupTapEnv(options)
    runTests(options)
  }
}

/* istanbul ignore next */
const nycReporter = options =>
  options['coverage-report'] === false ? ['--silent']
  : options['coverage-report'].map(cr =>
    cr === 'html' ? '--reporter=lcov' : `--reporter=${cr}`)

const defaultNycExcludes = [
  'coverage/**',
  'packages/*/test/**',
  'test/**',
  'test{,-*}.js',
  '**/*{.,-}test.js',
  '**/__tests__/**',
  '**/{ava,babel,jest,nyc,rollup,webpack}.config.js',
]

/* istanbul ignore next */
const runNyc = (cmd, programArgs, options, spawnOpts) => {
  const reporter = nycReporter(options)

  // note: these are forced to be numeric in the option parsing phase
  const branches = Math.min(options.branches, 100)
  const lines = Math.min(options.lines, 100)
  const functions = Math.min(options.functions, 100)
  const statements = Math.min(options.statements, 100)
  const excludes = defaultNycExcludes.concat(options.files).map(f =>
    '--exclude=' + f)
  if (options.before)
    excludes.push('--exclude=' + options.before)
  if (options.after)
    excludes.push('--exclude=' + options.after)

  const args = [
    nycBin,
    ...cmd,
    ...(options['show-process-tree'] ? ['--show-process-tree'] : []),
    ...excludes,
    '--produce-source-map',
    '--cache=true',
    '--branches=' + branches,
    '--watermarks.branches=' + branches,
    '--watermarks.branches=' + (branches + (100 - branches)/2),
    '--functions=' + functions,
    '--watermarks.functions=' + functions,
    '--watermarks.functions=' + (functions + (100 - functions)/2),
    '--lines=' + lines,
    '--watermarks.lines=' + lines,
    '--watermarks.lines=' + (lines + (100 - lines)/2),
    '--statements=' + statements,
    '--watermarks.statements=' + statements,
    '--watermarks.statements=' + (statements + (100 - statements)/2),
    ...reporter,
    '--extension=.js',
    '--extension=.jsx',
    '--extension=.mjs',
    '--extension=.cjs',
    '--extension=.ts',
    '--extension=.tsx',
    ...(options['nyc-arg'] || []),
  ]
  if (options['check-coverage'])
    args.push('--check-coverage')

  args.push.apply(args, programArgs)

  if (spawnOpts)
    return fg(node, args, spawnOpts)

  // fake it
  process.argv = [
    node,
    ...args,
  ]
  require(nycBin)

  if (reporter.includes('--reporter=lcov') && options.browser)
    process.on('exit', () => openHtmlCoverageReport(options))
}

/* istanbul ignore next */
const runCoverageReportOnly = options => {
  runNyc(['report'], [], options)
  if (process.env.COVERALLS_REPO_TOKEN ||
      process.env.__TAP_COVERALLS_TEST__) {
    return pipeToCoveralls()
  }
}

/* istanbul ignore next */
const pipeToCoveralls = async () => {
  if (!coverallsBin) {
    return
  }
  const reporter = spawn(node, [nycBin, 'report', '--reporter=text-lcov'], {
    stdio: [ 0, 'pipe', 2 ]
  })

  const bin = process.env.__TAP_COVERALLS_TEST__
    || require.resolve('coveralls/bin/coveralls.js')

  const ca = spawn(node, [bin], { stdio: ['pipe', 1, 2] })
  reporter.stdout.pipe(ca.stdin)
  await new Promise(resolve => ca.on('close', resolve))
}

/* istanbul ignore next */
const respawnWithCoverage = options => {
  debug('respawn with coverage')
  // If we have a coverage map, then include nothing by default here.
  runNyc(options['coverage-map'] ? [
    '--include=',
    '--no-exclude-after-remap',
    ...(options.saved || options.changed ? ['--no-clean'] : []),
  ] : [], [
    '--',
    node,
    ...process.execArgv,
    ...process.argv.slice(1)
  ], options)
}

/* istanbul ignore next */
const openHtmlCoverageReport = (options, code, signal) => {
  opener('coverage/lcov-report/index.html')
  if (signal) {
    setTimeout(() => {}, 200)
    process.kill(process.pid, signal)
  } else if (code) {
    process.exitCode = code
  }
}

const nycHelp = () => fg(node, [nycBin, '--help'])

// export for easier testing
const setupTapEnv = exports.setupTapEnv = options => {
  process.env.TAP_TIMEOUT = options.timeout
  if (options.color)
    process.env.TAP_COLORS = '1'
  else
    process.env.TAP_COLORS = '0'

  if (options.snapshot)
    process.env.TAP_SNAPSHOT = '1'

  if (options.bail)
    process.env.TAP_BAIL = '1'

  if (options['save-fixture'])
    process.env.TAP_SAVE_FIXTURE = '1'

  if (options.invert)
    process.env.TAP_GREP_INVERT = '1'

  if (options.grep.length)
    process.env.TAP_GREP = options.grep.map(p => p.toString())
      .join('\n')

  if (options.only)
    process.env.TAP_ONLY = '1'
}

const globFiles = files => Array.from(files.reduce((acc, f) =>
  acc.concat(f === '-' ? f : glob.sync(f, { nonull: true })), [])
  .reduce((set, f) => {
    set.add(f)
    return set
  }, new Set()))

// caller-callsite requires that the call stack includes at least one
// spot where "this" is defined, so we bind this function to an object
// in this completely weird way, or else import-jsx doesn't work.
const makeReporter = exports.makeReporter = function (tap, options) {
  const treportTypes = require('treport/types')
  const tapMochaReporter = require('tap-mocha-reporter')
  // if it's a treport type, use that
  const reporter = options.reporter
  if (reporter === 'tap')
    tap.pipe(process.stdout)
  else if (treportTypes.includes(reporter))
    require('treport')(tap, reporter)
  else if (tapMochaReporter.types.includes(reporter))
    tap.pipe(new tapMochaReporter(options.reporter))
  else {
    // might be a child process or a module
    try {
      which.sync(reporter)
      // it's a cli reporter!
      const c = spawn(reporter, options['reporter-arg'], {
        stdio: ['pipe', 1, 2]
      })
      tap.pipe(c.stdin)
    } catch (_) {
      // resolve to cwd if it's a relative path
      const rmod = /^\.\.?[\\\/]/.test(reporter) ? path.resolve(reporter) : reporter
      // it'll often be jsx, and this is harmless if it isn't.
      const R = require('@isaacs/import-jsx')(rmod)
      if (typeof R !== 'function' || !R.prototype)
        throw new Error(
          `Invalid reporter: non-class exported by ${reporter}`)
      else if (R.prototype.isReactComponent)
        require('treport')(tap, R)
      else if (R.prototype.write && R.prototype.end)
        tap.pipe(new R(...(options['reporter-arg'])))
      else
        throw new Error(
          `Invalid reporter: not a stream or react component ${reporter}`)
    }
  }
}.bind({})

const stdinOnly = options => {
  // if we didn't specify any files, then just passthrough
  // to the reporter, so we don't get '/dev/stdin' in the suite list.
  // We have to pause() before piping to switch streams2 into old-mode
  const tap = require('../lib/tap.js')
  tap.writeSnapshot = false
  tap.stdinOnly()
  makeReporter(tap, options)

  if (options['output-file'] !== null)
    process.stdin.pipe(fs.createWriteStream(options['output-file']))
  if (options['output-dir'] !== null)
    process.stdin.pipe(fs.createWriteStream(options['output-dir'] +
      '/stdin.tap'))
  process.stdin.resume()
}

const readSaveFile = options => {
  if (options.save)
    try {
      const s = fs.readFileSync(options.save, 'utf8').trim()
      if (s)
        return s.split('\n')
    } catch (er) {}

  return null
}

const saveFails = (options, tap) => {
  if (!options.save)
    return

  let fails = []
  const successes = []
  tap.on('result', res => {
    // we will continue to re-run todo tests, even though they're
    // not technically "failures".
    if (!res.ok && !res.extra.skip)
      fails.push(res.extra.file)
    else
      successes.push(res.extra.file)
  })

  const save = () => {
    fails = fails.reduce((set, f) => {
      f = f.replace(/\\/g, '/')
      /* istanbul ignore else */
      if (set.indexOf(f) === -1)
        set.push(f)
      return set
    }, [])

    if (!fails.length)
      rimraf(options.save)
    else
      try {
        fs.writeFileSync(options.save, fails.join('\n') + '\n')
      } catch (er) {}
  }

  tap.on('bailout', () => {
    // add any pending test files to the fails list.
    fails.push.apply(fails, options.files.filter(file =>
      successes.indexOf(file) === -1))
    save()
  })

  tap.on('end', save)
}

const filesMatch = (a, b) =>
  a && b && path.resolve(a) === path.resolve(b)

const filterFiles = exports.filterFiles = (files, options, parallelOk) =>
  files.filter(file =>
    path.basename(file) === 'tap-parallel-ok' ?
      ((parallelOk[path.resolve(path.dirname(file))] = true), false)
    : path.basename(file) === 'tap-parallel-not-ok' ?
      parallelOk[path.resolve(path.dirname(file))] = false
    // don't include the --before and --after scripts as files,
    // so they're not run as tests if they would be found normally.
    // This allows test/setup.js and test/teardown.js for example.
    : filesMatch(file, options.before) ? false
    : filesMatch(file, options.after) ? false
    : options.saved && options.saved.length ? onSavedList(options.saved, file)
    : options.changed ? options.changedFilter(file)
    : true
  )

// check if the file is on the list, or if it's a parent dir of
// any items that are on the list.
const onSavedList = (saved, file) =>
  saved.indexOf(file) !== -1 ? true
  : saved.some(f => f.indexOf(file + '/') === 0)

const isParallelOk = (parallelOk, file) => {
  const dir = path.resolve(path.dirname(file))
  return (dir in parallelOk) ? parallelOk[dir]
    : exists(dir + '/tap-parallel-ok')
    ? parallelOk[dir] = true
    : exists(dir + '/tap-parallel-not-ok')
    ? parallelOk[dir] = false
    : dir.length >= process.cwd().length
    ? isParallelOk(parallelOk, dir)
    : true
}

const getEnv = options => options['test-env'].reduce((env, kv) => {
  const split = kv.split('=')
  const key = split.shift()
  if (!split.length)
    delete env[key]
  else
    env[key] = split.join('=')
  return env
}, {...process.env})

// the test that checks this escapes from NYC, so it'll never show up
/* istanbul ignore next */
const coverageMapOverride = (env, file, coverageMap) => {
  if (coverageMap) {
    /* istanbul ignore next */
    env.NYC_CONFIG_OVERRIDE = JSON.stringify({
      include: coverageMap(file) || ''
    })
  }
}

const runAllFiles = (options, env, tap, processDB) => {
  debug('run all files')
  let doStdin = false
  let parallelOk = Object.create(null)

  if (options['output-dir'] !== null) {
    tap.on('spawn', t => {
      const dir = options['output-dir'] + '/' + path.dirname(t.name)
      require('../settings.js').mkdirRecursiveSync(dir)
      const file = dir + '/' + path.basename(t.name) + '.tap'
      t.proc.stdout.pipe(fs.createWriteStream(file))
    })
    tap.on('stdin', t => {
      const file = options['output-dir'] + '/stdin.tap'
      t.stream.pipe(fs.createWriteStream(file))
    })
  }

  options.files = filterFiles(options.files, options, parallelOk)

  if (options.files.length === 0 && !doStdin && !options.changed) {
    tap.fail('no tests specified')
  }

  /* istanbul ignore next */
  const coverageMap = options['coverage-map']
    ? require(path.resolve(options['coverage-map']))
    : null

  const seen = new Set()
  for (let i = 0; i < options.files.length; i++) {
    const file = options.files[i]
    if (seen.has(file))
      continue

    debug('run file', file)
    seen.add(file)

    // Pick up stdin after all the other files are handled.
    if (file === '-') {
      doStdin = true
      continue
    }

    let st
    try {
      st = fs.statSync(file)
    } catch (er) {
      tap.test(file, t => t.threw(er))
      continue
    }

    if (st.isDirectory()) {
      debug('is a directory', file)
      const dir = filterFiles(fs.readdirSync(file).map(f =>
        file.replace(/[\/\\]+$/, '') + '/' + f), options, parallelOk)
      options.files.splice(i, 1, ...dir)
      i--
    } else {
      const opt = { env, file, processDB }

      coverageMapOverride(env, file, coverageMap)

      if (options.timeout)
        opt.timeout = options.timeout * 1000

      if (options.jobs > 1)
        opt.buffered = isParallelOk(parallelOk, file) !== false

      if (options.flow && flowNode)
        options['node-arg'].push('-r', flowNode)

      if (options.ts && tsNode && /\.tsx?$/.test(file)) {
        debug('typescript file', file)
        const compilerOpts = JSON.parse(env.TS_NODE_COMPILER_OPTIONS || '{}')
        if (options.jsx)
          compilerOpts.jsx = 'react'

        opt.env = {
          ...env,
          TS_NODE_COMPILER_OPTIONS: JSON.stringify(compilerOpts),
        }
        const args = [
          '-r', tsNode,
          ...options['node-arg'],
          file,
          ...options['test-arg']
        ]
        tap.spawn(node, args, opt, file)
      } else if (options.jsx && /\.jsx$/.test(file)) {
        debug('jsx file', file)
        const args = [
          ...(options['node-arg']),
          jsx,
          file,
          ...(options['test-arg']),
        ]
        tap.spawn(node, args, opt, file)
      } else if (/\.jsx$|\.tsx?$|\.[mc]?js$/.test(file)) {
        debug('js file', file)
        /* istanbul ignore next - version specific behavior */
        const experimental = /^v10\./.test(process.version) && /\.mjs$/.test(file)
          ? ['--experimental-modules'] : []

        const args = [
          ...options['node-arg'],
          ...experimental,
          file,
          ...options['test-arg']
        ]
        tap.spawn(node, args, opt, file)
      } else if (/\.tap$/.test(file)) {
        debug('tap file', file)
        tap.spawn('cat', [file], opt, file)
      } else if (isexe.sync(options.files[i])) {
        debug('executable', file)
        tap.spawn(options.files[i], options['test-arg'], opt, file)
      } else {
        debug('not a test file', file)
      }
    }
  }

  if (doStdin)
    tap.stdin()

  debug('scheduled all files for execution')
}

const runTests = options => {
  debug('run tests')
  // At this point, we know we need to use the tap root,
  // because there are 1 or more files to spawn.
  const tap = require('../lib/tap.js')
  tap.writeSnapshot = false
  if (options.comments) {
    const onComment = c => {
      if (!c.match(/^# (timeout=[0-9]+|time=[0-9\.]+m?s|Subtest(: .+)?)\n$/))
        console.error(c.substr(2).trim())
    }
    const onChild = p => {
      p.on('comment', onComment)
      p.on('child', onChild)
    }
    tap.parser.on('comment', onComment)
    tap.parser.on('child', onChild)
  }

  tap.runOnly = false

  // greps are passed to children, but not the runner itself
  tap.grep = []
  tap.jobs = options.jobs

  const env = getEnv(options)

  /* istanbul ignore next */
  const processDB = options.coverage && process.env.NYC_CONFIG
    ? new ProcessDB() : null

  // run --before before everything, and --after as the very last thing
  runBeforeAfter(options, env, tap, processDB)

  tap.patchProcess()

  // if not -Rtap, then output what the user wants.
  // otherwise just dump to stdout
  /* istanbul ignore next */
  makeReporter(tap, options)

  // need to replay the first version line, because the previous
  // line will have flushed it out to stdout or the reporter already.
  if (options['output-file'] !== null)
    tap.pipe(fs.createWriteStream(options['output-file'])).write('TAP version 13\n')

  saveFails(options, tap)

  runAllFiles(options, env, tap, processDB)

  /* istanbul ignore next */
  if (process.env.COVERALLS_REPO_TOKEN ||
      process.env.__TAP_COVERALLS_TEST__) {
    tap.teardown(() => pipeToCoveralls())
  }

  tap.end()
  debug('called tap.end()')
}

const beforeAfter = (env, args) => {
  const {status, signal} = spawnSync(process.execPath, args, {
    env,
    stdio: 'inherit',
  })

  if (status || signal) {
    const script = args.length === 1 ? args[0] : args[2]
    const msg = `\n# failed ${script}\n# code=${status} signal=${signal}\n`
    console.error(msg)
    process.exitCode = status || 1
    process.exit(status || 1)
  }
}

const runBeforeAfter = (options, env, tap, processDB) => {
  // Have to write the index before running a script so that this
  // process is included in the DB, or else it'll crash when it
  // tries to get the parent info.
  /* istanbul ignore next */
  if (processDB && (options.before || options.after))
    processDB.writeIndex()

  if (options.before) {
    if (options.ts && tsNode && /\.tsx?$/.test(options.before)) {
      beforeAfter(env, ['-r', tsNode, ...options['node-arg'], options.before])
    } else {
      beforeAfter(env, [...options['node-arg'], options.before])
    }
  }

  if (options.after) {
    /* istanbul ignore next - run after istanbul's report */
    signalExit(() => {
      if (options.ts && tsNode && /\.tsx?$/.test(options.after)) {
        beforeAfter(env, ['-r', tsNode, ...options['node-arg'], options.after])
      } else {
        beforeAfter(env, [...options['node-arg'], options.after])
      }
    }, { alwaysLast: true })
  }
}

const parsePackageJson = () => {
  try {
    return JSON.parse(fs.readFileSync('package.json', 'utf8')).tap || {}
  } catch (er) {
    return {}
  }
}

const parseRcFile = path => {
  let contents
  try {
    contents = fs.readFileSync(path, 'utf8')
  } catch (_) {
    try {
      contents = fs.readFileSync(path + '.yaml', 'utf8')
    } catch (_) {
      try {
        contents = fs.readFileSync(path + '.yml', 'utf8')
      } catch (_) {}
    }
  }
  if (!contents) {
    // if no dotfile exists, just return an empty object
    return {}
  }
  return yaml.parse(contents)
}

const strToRegExp = g => {
  const p = g.match(/^\/(.*)\/([a-z]*)$/)
  g = p ? p[1] : g
  const flags = p ? p[2] : ''
  return new RegExp(g, flags)
}

const onError = er => {
  /* istanbul ignore else - parse errors are the only ones we ever expect */
  if (er.name.match(/^AssertionError/) && !er.generatedMessage) {
    console.error('Error: ' + er.message)
    console.error('Run `tap --help` for usage information')
    process.exit(1)
  } else {
    console.error(er)
    process.exit(1)
  }
}

try {
  require('./jack.js')(main)
} catch (er) {
  onError(er)
}
