OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-booking / node_modules / brittle / cmd.js

252 lines · javascript ref:
#!/usr/bin/env node

const path = require('path')
const os = require('os')
const { command, flag, rest } = require('paparam')
const Globbie = require('globbie')
const { spawn } = require('child_process')
const process = require('process')
const TracingPromise = require('./lib/tracing-promise')
const pkg = require('./package')

const args = (process.env.BRITTLE || '').split(/\s|,/g).map(s => s.trim()).filter(s => s).concat(process.argv.slice(2))
const cmd = command('brittle',
  flag('--version|-v', 'Print the current version'),
  flag('--solo, -s', 'Engage solo mode'),
  flag('--bail, -b', 'Bail out on first assert failure'),
  flag('--coverage, -c', 'Turn on coverage'),
  flag('--cov-dir <dir>', 'Configure coverage output directory (default: ./coverage)'),
  flag('--trace', 'Trace all active promises and print them if the test fails'),
  flag('--timeout, -t <timeout>', 'Set the test timeout in milliseconds (default: 30000)'),
  flag('--runner, -r <runner>', 'Generates an out file that contains all target tests'),
  flag('--mine, -m <miners>', 'Keep running the tests in <miners> processes until they fail.'),
  flag('--unstealth, -u', 'Print out assertions even if stealth is used'),
  rest('<files>')
).parse(args)
if (!cmd) process.exit(0)

const { version } = cmd.flags
if (version) {
  console.log(`v${pkg.version}`)
  process.exit(0)
}

const argv = cmd.flags

const files = []
for (const g of cmd.rest || []) {
  const glob = new Globbie(g, { sync: true })
  const matches = glob.match()

  if (matches.length === 0) {
    if (g[0] === '-') continue
    console.error(`Error: no files found when resolving ${g}`)
    process.exit(1)
  }

  files.push(...matches)
}

if (files.length === 0) {
  console.error('Error: No test files were specified')
  process.exit(1)
}

const { solo, bail, timeout, coverage, covDir, mine, trace, unstealth } = argv

process.title = 'brittle'

if (trace && !mine) {
  TracingPromise.enable()
  process.on('exit', function (code, signal) {
    if (!code && !signal) return
    console.error()
    console.error('Printing tracing info since the tests failed:')
    console.error()
    TracingPromise.print()
  })
}

if (argv.runner) {
  const fs = require('fs')

  if (argv.runner === true) {
    console.error('--runner must be a path to the generated test runner')
    process.exit(2)
  }

  const out = path.resolve(argv.runner)
  const dir = path.dirname(out)

  let s = ''

  s += 'runTests()\n\nasync function runTests () {\n  const test = (await import(\'brittle\')).default\n\n'

  if (bail || solo || unstealth || timeout) {
    s += `  test.configure({ bail: ${!!bail}, solo: ${!!solo}, unstealth: ${!!unstealth}, timeout: ${timeout} })\n`
  }

  s += '  test.pause()\n\n'

  for (const f of files) {
    const t = path.resolve(f)
    if (t === out) continue

    let r = path.relative(dir, t).replace(/\\/g, '/')
    if (r[0] !== '.') r = './' + r
    s += '  await import(\'' + r + '\')\n'
  }

  s = s.trimRight()

  s += '\n\n  test.resume()\n}\n'
  s = '// This runner is auto-generated by Brittle\n\n' + s

  try {
    fs.mkdirSync(dir)
  } catch {}

  fs.writeFileSync(out, s)
  process.exit(0)
}

if (coverage && process.env.BRITTLE_COVERAGE !== 'false') require('bare-cov')({ dir: covDir })

if (mine) startMining().catch()
else start().catch(onerror)

function onerror (err) {
  console.error(err.stack)
  process.exit(1)
}

async function start () {
  const brittle = require('./')

  if (bail || solo || unstealth || timeout) {
    brittle.configure({ bail, solo, unstealth, timeout: timeout ? Number(timeout) : undefined })
  }

  brittle.pause()

  for (const f of files) {
    await import('file://' + path.resolve(f))
  }

  brittle.resume()
}

async function startMining () {
  const args = [__filename]
    .concat(solo ? ['--solo'] : [])
    .concat(bail ? ['--bail'] : [])
    .concat(unstealth ? ['--unstealth'] : [])
    .concat(trace ? ['--trace'] : [])
    .concat(timeout ? ['--timeout', timeout + ''] : [])
    .concat(files)

  const running = new Set()
  const max = Number(argv.mine) || 1

  let runs = 0
  let bailed = false
  let newline = false

  const interval = setInterval(function () {
    console.log('Still mining... Total runs: ' + runs)
    newline = true
  }, 1000)

  bump()

  process.once('SIGINT', bail)
  process.once('SIGTERM', bail)

  function bail () {
    bailed = true
    clearInterval(interval)
    for (const r of running) r.kill()
  }

  async function bump () {
    if (running.size >= max || bailed) return

    const r = run()
    running.add(r)

    const { exitCode, signalCode, output } = await r.promise
    running.delete(r)
    runs++

    if (bailed) return

    if (!exitCode && !signalCode) {
      bump()
      bump()
      return
    }

    bailed = true

    clearInterval(interval)

    if (newline) console.log()
    if (exitCode) console.log('Runner failed with exit code ' + exitCode + '!')
    else console.log('Runner failed with signal code ' + signalCode + ' (' + signalToName(signalCode) + ')!')

    console.log('Shutting down the rest and printing output...')

    for (const r of running) {
      r.kill()
      await r.promise
    }

    console.log('Done! The tests took ' + runs + ' runs to fail.')
    console.log()

    for (const { stdout, data } of output) {
      if (stdout) process.stdout.write(data)
      else process.stderr.write(data)
    }

    clearInterval(interval)
    process.exitCode = exitCode || 1
  }

  function run () {
    const p = spawn(process.execPath, args)

    const output = []

    p.stdout.on('data', (data) => output.push({ stdout: true, data }))
    p.stderr.on('data', (data) => output.push({ stdout: false, data }))

    const stdoutClosed = new Promise(resolve => p.stdout.on('close', resolve))
    const stderrClosed = new Promise(resolve => p.stderr.on('close', resolve))

    const promise = new Promise((resolve) => {
      p.on('exit', async (exitCode, signalCode) => {
        await stdoutClosed
        await stderrClosed
        resolve({
          exitCode,
          signalCode,
          output
        })
      })
    })

    return {
      promise,
      kill: () => p.kill()
    }
  }
}

function signalToName (code) {
  for (const [k, v] of Object.entries(os.constants.signals)) {
    if (v === code) return k
  }
  return null
}