UNPKG

@small-tech/tap-out

Version:
323 lines (255 loc) 7.89 kB
'use strict' const PassThrough = require('stream').PassThrough const split = require('split') const util = require('util') const EventEmitter = require('events').EventEmitter const reemit = require('re-emitter') const parseLine = require('./lib/parse-line') const error = require('./lib/error') function trim (str) { return str.trim() } function Parser() { if (!(this instanceof Parser)) { return new Parser() } EventEmitter.call(this) this.results = { tests: [], asserts: [], versions: [], results: [], comments: [], plans: [], pass: [], fail: [], errors: [], bailOuts: [], } this.testNumber = 0 this.previousLine = '' this.currentNextLineError = null this.writingErrorOutput = false this.writingErrorStackOutput = false this.tmpErrorOutput = '' } util.inherits(Parser, EventEmitter) Parser.prototype.handleLine = function handleLine(line) { const parsed = parseLine(line) // This will handle all the error stuff this._handleError(line) // This is weird, but it's the only way to distinguish a // console.log type output from an error output if ( !this.writingErrorOutput && !parsed && !isErrorOutputEnd(line) && !isRawTapTestStatus(line) ) { const comment = { type: 'comment', raw: line, test: this.testNumber } this.emit('comment', comment) this.results.comments.push(comment) } // Invalid line if (!parsed) { this.previousLine = line return } // Handle tests if (parsed.type === 'test') { this.testNumber += 1 parsed.number = this.testNumber } // Handle asserts if (parsed.type === 'assert') { parsed.test = this.testNumber this.results[parsed.ok ? 'pass' : 'fail'].push(parsed) if (parsed.ok) { // No need to have the error object // in a passing assertion delete parsed.error this.emit('pass', parsed) } } if (!isOkLine(this.previousLine)) { this.emit(parsed.type, parsed) this.results[parsed.type + 's'].push(parsed) } // This is all so we can determine if the "# ok" output on the last line // should be skipped function isOkLine (previousLine) { return line === '# ok' && previousLine.indexOf('# pass') > -1 } this.previousLine = line } Parser.prototype._handleError = function _handleError(line) { let lastAssert // Start of error output if (isErrorOutputStart(line)) { this.writingErrorOutput = true this.lastAsserRawErrorString = '' } // End of error output else if (isErrorOutputEnd(line)) { this.writingErrorOutput = false this.currentNextLineError = null this.writingErrorStackOutput = false // Emit error here so it has the full error message with it lastAssert = this.results.fail[this.results.fail.length - 1] if (this.tmpErrorOutput) { lastAssert.error.stack = this.tmpErrorOutput this.lastAsserRawErrorString += this.tmpErrorOutput + '\n' this.tmpErrorOutput = '' } // right-trimmed raw error string lastAssert.error.raw = this.lastAsserRawErrorString.replace(/\s+$/g, '') this.emit('fail', lastAssert) } // Append to stack else if (this.writingErrorStackOutput) { this.tmpErrorOutput += trim(line) + '\n' } // Not the beginning of the error message but it's the body else if (this.writingErrorOutput) { const m = splitFirst(trim(line), (':')) lastAssert = this.results.fail[this.results.fail.length - 1] // Rebuild raw error output this.lastAsserRawErrorString += line + '\n' if (m[0] === 'stack') { this.writingErrorStackOutput = true return } let msg = trim((m[1] || '').replace(/['"]+/g, '')) if (m[0] === 'at') { // Example string: Object.async.eachSeries (/Users/scott/www/modules/nash/node_modules/async/lib/async.js:145:20) msg = (msg .split(' ')[1] || '') .replace('(', '') .replace(')', '') const values = msg.split(':') const file = values.slice(0, values.length-2).join(':') msg = { file: file, line: values[values.length-2], character: values[values.length-1] } } // This is a plan failure if (lastAssert.name === 'plan != count') { lastAssert.type = 'plan' delete lastAssert.error.at lastAssert.error.operator = 'count' // Need to set this value if (m[0] === 'actual') { lastAssert.error.actual = trim(m[1]) } } // outputting expected/actual object or array if (this.currentNextLineError) { lastAssert.error[this.currentNextLineError] = trim(line) this.currentNextLineError = null } else if (trim(m[1]) === '|-') { this.currentNextLineError = m[0] } else { lastAssert.error[m[0]] = msg } } // Emit fail when error on previous line had no diagnostics else if (this.previousLine && isFailAssertionLine(this.previousLine)) { lastAssert = this.results.fail[this.results.fail.length - 1] this.emit('fail', lastAssert) } } Parser.prototype._handleEnd = function _handleEnd() { const plan = this.results.plans.length ? this.results.plans[0] : null const count = this.results.asserts.length const first = count && this.results.asserts.reduce(firstAssertion) const last = count && this.results.asserts.reduce(lastAssertion) // Emit fail when error on previous line had no diagnostics if (this.previousLine && isFailAssertionLine(this.previousLine)) { const lastAssert = this.results.fail[this.results.fail.length - 1] this.emit('fail', lastAssert) } if (!plan) { if (count > 0) { this.results.errors.push(error('no plan provided')) } return } if (this.results.fail.length > 0) { return } if (count !== (plan.to - plan.from + 1)) { this.results.errors.push(error('incorrect number of assertions made')) } else if (first && first.number !== plan.from) { this.results.errors.push(error('first assertion number does not equal the plan start')) } else if (last && last.number !== plan.to) { this.results.errors.push(error('last assertion number does not equal the plan end')) } } module.exports = function (done) { done = done || function () {} const stream = new PassThrough() const parser = Parser() reemit(parser, stream, [ 'test', 'assert', 'version', 'result', 'pass', 'fail', 'comment', 'plan', 'bailOut' ]) const write = stream.write const end = stream.end const splitStream = split() stream.write = function() { write.apply(stream, arguments) splitStream.write.apply(splitStream, arguments) } stream.end = function() { end.apply(stream, arguments) splitStream.end.apply(splitStream, arguments) } splitStream .on('data', function (data) { if (!data) { return } const line = data.toString() parser.handleLine(line) }) .on('end', function () { parser._handleEnd() stream.emit('output', parser.results) done(null, parser.results) }) .on('error', done) return stream } module.exports.Parser = Parser function isFailAssertionLine (line) { return line.indexOf('not ok') === 0 } function isErrorOutputStart (line) { return line.indexOf(' ---') === 0 } function isErrorOutputEnd (line) { return line === ' ...' } function splitFirst(str, pattern) { const parts = str.split(pattern) if (parts.length <= 1) { return parts } return [parts[0], parts.slice(1).join(pattern)] } function isRawTapTestStatus (str) { const rawTapTestStatusRegex = new RegExp('(\\d+)(\\.)(\\.)(\\d+)') return rawTapTestStatusRegex.exec(str) } function firstAssertion(first, assert) { return assert.number < first.number ? assert : first } function lastAssertion(last, assert) { return assert.number > last.number ? assert : last }