blob: 2585dab7e2981a4f6130f26514d81de4fff7b284 [file] [log] [blame]
'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;
}