290 lines
6.9 KiB
JavaScript
290 lines
6.9 KiB
JavaScript
'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;
|
|
}
|