| 'use strict'; |
| |
| var PassThrough = require('readable-stream/passthrough'); |
| var split = require('split'); |
| var trim = require('trim'); |
| var util = require('util'); |
| var EventEmitter = require('events').EventEmitter; |
| var reemit = require('re-emitter'); |
| |
| var expr = require('./lib/utils/regexes'); |
| var parseLine = require('./lib/parse-line'); |
| var error = require('./lib/error'); |
| |
| function Parser() { |
| if (!(this instanceof Parser)) { |
| return new Parser(); |
| } |
| |
| EventEmitter.call(this); |
| |
| this.results = { |
| tests: [], |
| asserts: [], |
| versions: [], |
| results: [], |
| comments: [], |
| plans: [], |
| pass: [], |
| fail: [], |
| errors: [], |
| }; |
| 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) { |
| |
| var 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) |
| ) |
| { |
| var comment = { |
| type: 'comment', |
| raw: line, |
| test: this.testNumber |
| }; |
| this.emit('comment', comment); |
| this.results.comments.push(comment); |
| } |
| |
| // Invalid line |
| if (!parsed) { |
| 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) { |
| |
| // 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 |
| var 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) { |
| var lastAssert = this.results.fail[this.results.fail.length - 1]; |
| var m = splitFirst(trim(line), (':')); |
| |
| // Rebuild raw error output |
| this.lastAsserRawErrorString += line + '\n'; |
| |
| if (m[0] === 'stack') { |
| this.writingErrorStackOutput = true; |
| return; |
| } |
| |
| var 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(')', ''); |
| |
| var values = msg.split(':'); |
| var 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; |
| } |
| } |
| }; |
| |
| Parser.prototype._handleEnd = function _handleEnd() { |
| var plan = this.results.plans.length ? this.results.plans[0] : null; |
| var count = this.results.asserts.length; |
| var first = count && this.results.asserts.reduce(firstAssertion); |
| var last = count && this.results.asserts.reduce(lastAssertion); |
| |
| 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 () {}; |
| |
| var stream = new PassThrough(); |
| var parser = Parser(); |
| reemit(parser, stream, [ |
| 'test', 'assert', 'version', 'result', 'pass', 'fail', 'comment', 'plan' |
| ]); |
| |
| stream |
| .pipe(split()) |
| .on('data', function (data) { |
| |
| if (!data) { |
| return; |
| } |
| |
| var line = data.toString(); |
| parser.handleLine(line); |
| }) |
| .on('close', function () { |
| parser._handleEnd(); |
| |
| stream.emit('output', parser.results); |
| |
| done(null, parser.results); |
| }) |
| .on('error', done); |
| |
| return stream; |
| }; |
| |
| module.exports.Parser = Parser; |
| |
| function isErrorOutputStart (line) { |
| |
| return line.indexOf(' ---') === 0; |
| } |
| |
| function isErrorOutputEnd (line) { |
| |
| return line.indexOf(' ...') === 0; |
| } |
| |
| function splitFirst(str, pattern) { |
| |
| var parts = str.split(pattern); |
| if (parts.length <= 1) { |
| return parts; |
| } |
| |
| return [parts[0], parts.slice(1).join(pattern)]; |
| } |
| |
| function isRawTapTestStatus (str) { |
| |
| var 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; |
| } |