#!/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
}