tap-arc
Version:
spec-like TAP reporter
211 lines (180 loc) • 7.08 kB
JavaScript
import TapReader from 'tap-reader'
import JSON5 from 'json5'
import stripAnsi from 'strip-ansi'
import createDiffer from './_make-diff.js'
import createPrinter from './_printer.js'
const { parse } = JSON5
const tapeCommentPrefixes = [ 'tests ', 'pass ', 'skip', 'todo', 'fail ', 'failed ', 'ok', 'test count' ]
export default function createParser (input, output, options = {}) {
const { debug, pessimistic, showDiff, tap, verbose } = options
const _ = createPrinter(output, options)
const { print: P, prettyMs } = _
const { diffArray, diffObject, diffString } = createDiffer(_)
const reader = TapReader({ input, bail: pessimistic })
const cwd = process.cwd()
const start = Date.now()
const counter = { pass: 0, fail: 0, skip: 0, todo: 0 }
reader.on('comment', ({ comment }) => {
if (!tapeCommentPrefixes.some((c) => comment.startsWith(c))) {
// "comment" is generally a test group name
P(`\n${_.title(comment)}`)
}
})
reader.on('other', ({ line }) => {
// typically `console.log` output from the test or program it tests
const stripped = stripAnsi(line).trim()
const justAnsi = stripped.length === 0 && line.length > 0
if (!justAnsi) P(_.italic(line), 0)
})
reader.on('pass', (test) => {
counter.pass++
if (test.skip) counter.skip++
if (test.todo) counter.todo++
P(_.pass(test), 2)
})
reader.on('fail', (test) => {
counter.fail++
if (test.skip) counter.skip++
if (test.todo) counter.todo++
P(_.fail(test), 2)
if (test.diag) {
const { at, operator, stack } = test.diag
let { actual, expected } = test.diag
const indent = 4
function printBoth (e, a) {
P(`Actual: ${JSON.stringify(a)}`, indent)
P(`Expected: ${JSON.stringify(e)}`, indent)
}
if (actual === 'undefined') actual = undefined
if (expected === 'undefined') expected = undefined
if (actual && expected && actual === expected) { // shallow test output; can't be diffed
P(`${_.expected('Expected')} did not match ${_.actual('actual')}.`, indent)
P(_.dim('TAP output cannot be diffed'), indent)
P(actual, indent)
}
else if (operator === 'equal' || operator === 'deepEqual') {
// try parsing for JS types
if (typeof actual === 'string')
try { actual = parse(actual) }
catch (_e) { _e }
if (typeof expected === 'string')
try { expected = parse(expected) }
catch (_e) { _e }
const aType = typeof actual
const eType = typeof expected
let sharedType = aType === eType ? `${aType}` : null
if (sharedType === 'object') {
if (Array.isArray(actual) && Array.isArray(expected)) sharedType = 'array'
else if (Array.isArray(actual) && !Array.isArray(expected)) sharedType = null
else if (!Array.isArray(actual) && Array.isArray(expected)) sharedType = null
}
if (sharedType) {
if ([ 'number', 'bigint', 'boolean', 'symbol', 'function', 'undefined' ].includes(sharedType))
P(`Expected ${_.expected(expected)} but got ${_.actual(actual)}`, indent)
else if (showDiff && sharedType === 'array')
diffArray(actual, expected).forEach(line => P(line, indent))
else if (showDiff && sharedType === 'object')
diffObject(actual, expected).forEach(line => P(line, indent))
else if (showDiff && sharedType === 'string')
diffString(actual, expected).forEach(line => P(line, indent))
else
printBoth(expected, actual)
}
else { // mixed types
printBoth(expected, actual)
}
}
else {
switch (operator) {
case 'notEqual':
P('Expected values to differ', indent)
break
case 'notDeepEqual':
P('Expected values to differ', indent)
break
case 'ok':
P(`Expected ${_.expected('truthy')} but got ${_.actual(actual)}`, indent)
break
case 'match':
P(`Expected "${_.actual(actual)}" to match ${_.expected(expected)}`, indent)
break
case 'doesNotMatch':
P(`Expected "${_.actual(actual)}" to not match ${_.expected(expected)}`, indent)
break
case 'throws':
if (
actual
&& typeof actual !== 'undefined'
&& expected
&& typeof expected !== 'undefined'
) // this weird combination is throws with expected/assertion
P(`Expected ${_.expected(expected)} to match "${_.actual(actual.message || actual)}"`, indent)
else if (actual && typeof actual !== 'undefined') // this combination is usually "doesNotThrow"
P(`Expected to not throw, received "${_.actual(actual)}"`, indent)
else
P('Expected to throw', indent)
break
case 'error':
P(`Expected error to be ${_.expected('falsy')}`, indent)
break
case 'fail':
P('Explicit fail', indent)
break
default:
printBoth(expected, actual)
break
}
}
if (at) P(_.dim(`At: ${at.replace(cwd, '')}`), indent)
if (stack && verbose)
stack.split('\n').forEach((s) => {
P(_.dim(s.trim().replace(cwd, '')), indent)
})
}
})
reader.on('done', ({ summary, plan, passing, failures, ok }) => {
if (summary && plan && summary.total < plan.end) {
P(_.realBad(`\nExpected ${plan.end} tests, parsed ${summary.total}`))
reader.emit('badCount', { summary, plan })
}
if (!ok) {
if (summary.fail > 0) {
const singular = summary.fail === 1
let failureSummary = '\n'
failureSummary += _.bad('Failed tests:')
failureSummary += ` There ${singular ? 'was' : 'were'} `
failureSummary += _.bad(summary.fail)
failureSummary += ` failure${singular ? '' : 's'}\n`
P(failureSummary)
for (const test in failures) P(_.fail(failures[test]), 2)
}
}
P(`\ntotal: ${summary.total}`)
// if (result.bailout) P(_.realBad('BAILED!'))
if (summary.pass > 0) P(_.good(`passing: ${summary.pass}`))
if (summary.fail > 0) P(_.bad(`failing: ${summary.fail}`))
if (summary.skip > 0) P(_.dim(`skipped: ${summary.skip}`))
if (summary.todo > 0) P(_.dim(`todo: ${summary.todo}`))
P(`${_.dim.italic(prettyMs(start))}\n`) // maybe output.end()?
if (debug) {
P('tap-reader result:')
P(JSON.stringify({ summary, passing, failures, ok }, null, 2))
P('tap-arc internal counters:')
P(JSON.stringify(counter, null, 2))
}
})
if (verbose) {
reader.on('version', ({ version }) => {
P(`${_.strong('TAP version:')} ${version}`)
})
reader.on('plan', ({ start, end }) => {
P(`${_.strong('Plan:')} start=${start} end=${end}`)
})
}
if (tap) {
reader.on('line', ({ line }) => {
P(line.trim())
})
}
return reader
}