tap-parser
Advanced tools
Comparing version 5.4.0 to 6.0.0
1533
index.js
@@ -0,25 +1,14 @@ | ||
'use strict' | ||
// Transforms a stream of TAP into a stream of result objects | ||
// and string comments. Emits "results" event with summary. | ||
var Writable = require('stream').Writable | ||
/* istanbul ignore if */ | ||
if (!Writable) { | ||
try { | ||
Writable = require('readable-stream').Writable | ||
} catch (er) { | ||
throw new Error('Please install "readable-stream" to use this module ' + | ||
'with Node.js v0.8 and before') | ||
} | ||
} | ||
const MiniPass = require('minipass') | ||
var yaml = require('js-yaml') | ||
var util = require('util') | ||
var assert = require('assert') | ||
const yaml = require('js-yaml') | ||
const util = require('util') | ||
const assert = require('assert') | ||
util.inherits(Parser, Writable) | ||
module.exports = Parser | ||
// every line outside of a yaml block is one of these things, or | ||
// a comment, or garbage. | ||
var lineTypes = { | ||
const lineTypes = { | ||
testPoint: /^(not )?ok(?: ([0-9]+))?(?:(?: -)?( .*?))?(\{?)\n$/, | ||
@@ -36,7 +25,7 @@ pragma: /^pragma ([+-])([a-z]+)\n$/, | ||
var lineTypeNames = Object.keys(lineTypes) | ||
const lineTypeNames = Object.keys(lineTypes) | ||
function lineType (line) { | ||
for (var t in lineTypes) { | ||
var match = line.match(lineTypes[t]) | ||
const lineType = line => { | ||
for (let t in lineTypes) { | ||
const match = line.match(lineTypes[t]) | ||
if (match) | ||
@@ -48,3 +37,3 @@ return [t, match] | ||
function parseDirective (line) { | ||
const parseDirective = line => { | ||
if (!line.trim()) | ||
@@ -54,5 +43,5 @@ return false | ||
line = line.replace(/\{\s*$/, '').trim() | ||
var time = line.match(/^time=((?:[1-9][0-9]*|0)(?:\.[0-9]+)?)(ms|s)$/i) | ||
const time = line.match(/^time=((?:[1-9][0-9]*|0)(?:\.[0-9]+)?)(ms|s)$/i) | ||
if (time) { | ||
var n = +time[1] | ||
let n = +time[1] | ||
if (time[2] === 's') { | ||
@@ -67,3 +56,3 @@ // JS does weird things with floats. Round it off a bit. | ||
var type = line.match(/^(todo|skip)\b/i) | ||
const type = line.match(/^(todo|skip)\b/i) | ||
if (!type) | ||
@@ -75,293 +64,273 @@ return false | ||
function Result (parsed, count) { | ||
var ok = !parsed[1] | ||
var id = +(parsed[2] || count + 1) | ||
var buffered = parsed[4] | ||
this.ok = ok | ||
this.id = id | ||
class Result { | ||
constructor (parsed, count) { | ||
const ok = !parsed[1] | ||
const id = +(parsed[2] || count + 1) | ||
let buffered = parsed[4] | ||
this.ok = ok | ||
this.id = id | ||
var rest = parsed[3] || '' | ||
var name | ||
rest = rest.replace(/([^\\]|^)((?:\\\\)*)#/g, '$1\n$2').split('\n') | ||
name = rest.shift() | ||
rest = rest.filter(function (r) { return r.trim() }).join('#') | ||
let rest = parsed[3] || '' | ||
let name | ||
rest = rest.replace(/([^\\]|^)((?:\\\\)*)#/g, '$1\n$2').split('\n') | ||
name = rest.shift() | ||
rest = rest.filter(r => r.trim()).join('#') | ||
// now, let's see if there's a directive in there. | ||
var dir = parseDirective(rest.trim()) | ||
if (!dir) | ||
name += (rest ? '#' + rest : '') + buffered | ||
else { | ||
// handle buffered subtests with todo/skip on them, like | ||
// ok 1 - bar # todo foo {\n | ||
var dirKey = dir[0] | ||
var dirValue = dir[1] | ||
this[dirKey] = dirValue | ||
} | ||
// now, let's see if there's a directive in there. | ||
const dir = parseDirective(rest.trim()) | ||
if (!dir) | ||
name += (rest ? '#' + rest : '') + buffered | ||
else { | ||
// handle buffered subtests with todo/skip on them, like | ||
// ok 1 - bar # todo foo {\n | ||
const dirKey = dir[0] | ||
const dirValue = dir[1] | ||
this[dirKey] = dirValue | ||
} | ||
if (/\{\s*$/.test(name)) { | ||
name = name.replace(/\{\s*$/, '') | ||
buffered = '{' | ||
} | ||
if (/\{\s*$/.test(name)) { | ||
name = name.replace(/\{\s*$/, '') | ||
buffered = '{' | ||
} | ||
if (buffered === '{') | ||
this.buffered = true | ||
if (buffered === '{') | ||
this.buffered = true | ||
if (name) | ||
this.name = name.trim() | ||
return this | ||
if (name) | ||
this.name = name.trim() | ||
} | ||
} | ||
function Parser (options, onComplete) { | ||
if (typeof options === 'function') { | ||
onComplete = options | ||
options = {} | ||
} | ||
class Parser extends MiniPass { | ||
constructor (options, onComplete) { | ||
if (typeof options === 'function') { | ||
onComplete = options | ||
options = {} | ||
} | ||
if (!(this instanceof Parser)) | ||
return new Parser(options, onComplete) | ||
options = options || {} | ||
super(options) | ||
this.resume() | ||
options = options || {} | ||
if (onComplete) | ||
this.on('complete', onComplete) | ||
if (onComplete) | ||
this.on('complete', onComplete) | ||
this.comments = [] | ||
this.results = null | ||
this.braceLevel = null | ||
this.parent = options.parent || null | ||
this.failures = [] | ||
if (options.passes) | ||
this.passes = [] | ||
this.level = options.level || 0 | ||
Writable.call(this) | ||
this.buffer = '' | ||
this.bail = !!options.bail | ||
this.bailingOut = false | ||
this.bailedOut = false | ||
this.syntheticBailout = false | ||
this.syntheticPlan = false | ||
this.omitVersion = !!options.omitVersion | ||
this.planStart = -1 | ||
this.planEnd = -1 | ||
this.planComment = '' | ||
this.yamlish = '' | ||
this.yind = '' | ||
this.child = null | ||
this.current = null | ||
this.maybeSubtest = null | ||
this.extraQueue = [] | ||
this.buffered = options.buffered || null | ||
this.aborted = false | ||
this.preserveWhitespace = options.preserveWhitespace || false | ||
this.comments = [] | ||
this.results = null | ||
this.braceLevel = null | ||
this.parent = options.parent || null | ||
this.failures = [] | ||
if (options.passes) | ||
this.passes = [] | ||
this.level = options.level || 0 | ||
this.count = 0 | ||
this.pass = 0 | ||
this.fail = 0 | ||
this.todo = 0 | ||
this.skip = 0 | ||
this.ok = true | ||
this.buffer = '' | ||
this.bail = !!options.bail | ||
this.bailingOut = false | ||
this.bailedOut = false | ||
this.syntheticBailout = false | ||
this.syntheticPlan = false | ||
this.omitVersion = !!options.omitVersion | ||
this.planStart = -1 | ||
this.planEnd = -1 | ||
this.planComment = '' | ||
this.yamlish = '' | ||
this.yind = '' | ||
this.child = null | ||
this.current = null | ||
this.maybeSubtest = null | ||
this.extraQueue = [] | ||
this.buffered = options.buffered || null | ||
this.aborted = false | ||
this.preserveWhitespace = options.preserveWhitespace || false | ||
this.strict = options.strict || false | ||
this.pragmas = { strict: this.strict } | ||
this.count = 0 | ||
this.pass = 0 | ||
this.fail = 0 | ||
this.todo = 0 | ||
this.skip = 0 | ||
this.ok = true | ||
this.postPlan = false | ||
} | ||
this.strict = options.strict || false | ||
this.pragmas = { strict: this.strict } | ||
Parser.prototype.tapError = function (error, line) { | ||
if (line) | ||
this.emit('line', line) | ||
this.ok = false | ||
this.fail ++ | ||
if (typeof error === 'string') { | ||
error = { | ||
tapError: error | ||
} | ||
this.postPlan = false | ||
} | ||
this.failures.push(error) | ||
} | ||
Parser.prototype.parseTestPoint = function (testPoint, line) { | ||
this.emitResult() | ||
if (this.bailedOut) | ||
return | ||
this.emit('line', line) | ||
var res = new Result(testPoint, this.count) | ||
if (this.planStart !== -1) { | ||
var lessThanStart = +res.id < this.planStart | ||
var greaterThanEnd = +res.id > this.planEnd | ||
if (lessThanStart || greaterThanEnd) { | ||
if (lessThanStart) | ||
res.tapError = 'id less than plan start' | ||
else | ||
res.tapError = 'id greater than plan end' | ||
res.plan = { start: this.planStart, end: this.planEnd } | ||
this.tapError(res) | ||
tapError (error, line) { | ||
if (line) | ||
this.emit('line', line) | ||
this.ok = false | ||
this.fail ++ | ||
if (typeof error === 'string') { | ||
error = { | ||
tapError: error | ||
} | ||
} | ||
this.failures.push(error) | ||
} | ||
if (res.id) { | ||
if (!this.first || res.id < this.first) | ||
this.first = res.id | ||
if (!this.last || res.id > this.last) | ||
this.last = res.id | ||
} | ||
parseTestPoint (testPoint, line) { | ||
this.emitResult() | ||
if (this.bailedOut) | ||
return | ||
if (!res.skip && !res.todo) | ||
this.ok = this.ok && res.ok | ||
this.emit('line', line) | ||
const res = new Result(testPoint, this.count) | ||
if (this.planStart !== -1) { | ||
const lessThanStart = +res.id < this.planStart | ||
const greaterThanEnd = +res.id > this.planEnd | ||
if (lessThanStart || greaterThanEnd) { | ||
if (lessThanStart) | ||
res.tapError = 'id less than plan start' | ||
else | ||
res.tapError = 'id greater than plan end' | ||
res.plan = { start: this.planStart, end: this.planEnd } | ||
this.tapError(res) | ||
} | ||
} | ||
// hold onto it, because we might get yamlish diagnostics | ||
this.current = res | ||
} | ||
if (res.id) { | ||
if (!this.first || res.id < this.first) | ||
this.first = res.id | ||
if (!this.last || res.id > this.last) | ||
this.last = res.id | ||
} | ||
Parser.prototype.nonTap = function (data, didLine) { | ||
if (this.bailingOut && /^( {4})*\}\n$/.test(data)) | ||
return | ||
if (!res.skip && !res.todo) | ||
this.ok = this.ok && res.ok | ||
if (this.strict) { | ||
var err = { | ||
tapError: 'Non-TAP data encountered in strict mode', | ||
data: data | ||
} | ||
this.tapError(err) | ||
if (this.parent) | ||
this.parent.tapError(err) | ||
// hold onto it, because we might get yamlish diagnostics | ||
this.current = res | ||
} | ||
// emit each line, then the extra as a whole | ||
if (!didLine) | ||
data.split('\n').slice(0, -1).forEach(function (line) { | ||
line += '\n' | ||
if (this.current || this.extraQueue.length) | ||
this.extraQueue.push(['line', line]) | ||
else | ||
this.emit('line', line) | ||
}, this) | ||
nonTap (data, didLine) { | ||
if (this.bailingOut && /^( {4})*\}\n$/.test(data)) | ||
return | ||
if (this.current || this.extraQueue.length) | ||
this.extraQueue.push(['extra', data]) | ||
else | ||
this.emit('extra', data) | ||
} | ||
if (this.strict) { | ||
const err = { | ||
tapError: 'Non-TAP data encountered in strict mode', | ||
data: data | ||
} | ||
this.tapError(err) | ||
if (this.parent) | ||
this.parent.tapError(err) | ||
} | ||
Parser.prototype.plan = function (start, end, comment, line) { | ||
// not allowed to have more than one plan | ||
if (this.planStart !== -1) { | ||
this.nonTap(line) | ||
return | ||
} | ||
// emit each line, then the extra as a whole | ||
if (!didLine) | ||
data.split('\n').slice(0, -1).forEach(line => { | ||
line += '\n' | ||
if (this.current || this.extraQueue.length) | ||
this.extraQueue.push(['line', line]) | ||
else | ||
this.emit('line', line) | ||
}) | ||
// can't put a plan in a child. | ||
if (this.child || this.yind) { | ||
this.nonTap(line) | ||
return | ||
if (this.current || this.extraQueue.length) | ||
this.extraQueue.push(['extra', data]) | ||
else | ||
this.emit('extra', data) | ||
} | ||
this.emitResult() | ||
if (this.bailedOut) | ||
return | ||
plan (start, end, comment, line) { | ||
// not allowed to have more than one plan | ||
if (this.planStart !== -1) { | ||
this.nonTap(line) | ||
return | ||
} | ||
// 1..0 is a special case. Otherwise, end must be >= start | ||
if (end < start && end !== 0 && start !== 1) { | ||
if (this.strict) | ||
this.tapError({ | ||
tapError: 'plan end cannot be less than plan start', | ||
plan: { | ||
start: start, | ||
end: end | ||
} | ||
}, line) | ||
else | ||
// can't put a plan in a child. | ||
if (this.child || this.yind) { | ||
this.nonTap(line) | ||
return | ||
} | ||
return | ||
} | ||
this.planStart = start | ||
this.planEnd = end | ||
var p = { start: start, end: end } | ||
if (comment) | ||
this.planComment = p.comment = comment | ||
this.emitResult() | ||
if (this.bailedOut) | ||
return | ||
// This means that the plan is coming at the END of all the tests | ||
// Plans MUST be either at the beginning or the very end. We treat | ||
// plans like '1..0' the same, since they indicate that no tests | ||
// will be coming. | ||
if (this.count !== 0 || this.planEnd === 0) | ||
this.postPlan = true | ||
// 1..0 is a special case. Otherwise, end must be >= start | ||
if (end < start && end !== 0 && start !== 1) { | ||
if (this.strict) | ||
this.tapError({ | ||
tapError: 'plan end cannot be less than plan start', | ||
plan: { | ||
start: start, | ||
end: end | ||
} | ||
}, line) | ||
else | ||
this.nonTap(line) | ||
return | ||
} | ||
this.emit('line', line) | ||
this.emit('plan', p) | ||
} | ||
this.planStart = start | ||
this.planEnd = end | ||
const p = { start: start, end: end } | ||
if (comment) | ||
this.planComment = p.comment = comment | ||
Parser.prototype.resetYamlish = function () { | ||
this.yind = '' | ||
this.yamlish = '' | ||
} | ||
// This means that the plan is coming at the END of all the tests | ||
// Plans MUST be either at the beginning or the very end. We treat | ||
// plans like '1..0' the same, since they indicate that no tests | ||
// will be coming. | ||
if (this.count !== 0 || this.planEnd === 0) | ||
this.postPlan = true | ||
// that moment when you realize it's not what you thought it was | ||
Parser.prototype.yamlGarbage = function () { | ||
var yamlGarbage = this.yind + '---\n' + this.yamlish | ||
this.emitResult() | ||
if (this.bailedOut) | ||
return | ||
this.nonTap(yamlGarbage, true) | ||
} | ||
this.emit('line', line) | ||
this.emit('plan', p) | ||
} | ||
Parser.prototype.yamlishLine = function (line) { | ||
if (line === this.yind + '...\n') { | ||
// end the yaml block | ||
this.processYamlish() | ||
} else { | ||
this.yamlish += line | ||
resetYamlish () { | ||
this.yind = '' | ||
this.yamlish = '' | ||
} | ||
} | ||
Parser.prototype.processYamlish = function () { | ||
var yamlish = this.yamlish | ||
this.resetYamlish() | ||
// that moment when you realize it's not what you thought it was | ||
yamlGarbage () { | ||
const yamlGarbage = this.yind + '---\n' + this.yamlish | ||
this.emitResult() | ||
if (this.bailedOut) | ||
return | ||
this.nonTap(yamlGarbage, true) | ||
} | ||
try { | ||
var diags = yaml.safeLoad(yamlish) | ||
} catch (er) { | ||
this.nonTap(this.yind + '---\n' + yamlish + this.yind + '...\n', true) | ||
return | ||
yamlishLine (line) { | ||
if (line === this.yind + '...\n') { | ||
// end the yaml block | ||
this.processYamlish() | ||
} else { | ||
this.yamlish += line | ||
} | ||
} | ||
this.current.diag = diags | ||
// we still don't emit the result here yet, to support diags | ||
// that come ahead of buffered subtests. | ||
} | ||
processYamlish () { | ||
const yamlish = this.yamlish | ||
this.resetYamlish() | ||
Parser.prototype.write = function (chunk, encoding, cb) { | ||
if (this.aborted) | ||
return | ||
let diags | ||
try { | ||
diags = yaml.safeLoad(yamlish) | ||
} catch (er) { | ||
this.nonTap(this.yind + '---\n' + yamlish + this.yind + '...\n', true) | ||
return | ||
} | ||
if (typeof encoding === 'string' && encoding !== 'utf8') | ||
chunk = new Buffer(chunk, encoding) | ||
if (Buffer.isBuffer(chunk)) | ||
chunk += '' | ||
if (typeof encoding === 'function') { | ||
cb = encoding | ||
encoding = null | ||
this.current.diag = diags | ||
// we still don't emit the result here yet, to support diags | ||
// that come ahead of buffered subtests. | ||
} | ||
this.buffer += chunk | ||
do { | ||
var match = this.buffer.match(/^.*\r?\n/) | ||
if (!match) | ||
break | ||
write (chunk, encoding, cb) { | ||
if (this.aborted) | ||
return | ||
this.buffer = this.buffer.substr(match[0].length) | ||
this.parse(match[0]) | ||
} while (this.buffer.length) | ||
if (typeof encoding === 'string' && encoding !== 'utf8') | ||
chunk = new Buffer(chunk, encoding) | ||
if (cb) | ||
process.nextTick(cb) | ||
return true | ||
} | ||
if (Buffer.isBuffer(chunk)) | ||
chunk += '' | ||
Parser.prototype.end = function (chunk, encoding, cb) { | ||
if (chunk) { | ||
if (typeof encoding === 'function') { | ||
@@ -371,608 +340,640 @@ cb = encoding | ||
} | ||
this.write(chunk, encoding) | ||
} | ||
if (this.buffer) | ||
this.write('\n') | ||
this.buffer += chunk | ||
do { | ||
const match = this.buffer.match(/^.*\r?\n/) | ||
if (!match) | ||
break | ||
// if we have yamlish, means we didn't finish with a ... | ||
if (this.yamlish) | ||
this.yamlGarbage() | ||
this.buffer = this.buffer.substr(match[0].length) | ||
this.parse(match[0]) | ||
} while (this.buffer.length) | ||
this.emitResult() | ||
if (cb) | ||
process.nextTick(cb) | ||
if (this.syntheticBailout && this.level === 0) { | ||
var reason = this.bailedOut | ||
if (reason === true) | ||
reason = '' | ||
else | ||
reason = ' ' + reason | ||
this.emit('line', 'Bail out!' + reason + '\n') | ||
return true | ||
} | ||
var skipAll | ||
end (chunk, encoding, cb) { | ||
if (chunk) { | ||
if (typeof encoding === 'function') { | ||
cb = encoding | ||
encoding = null | ||
} | ||
this.write(chunk, encoding) | ||
} | ||
if (this.planEnd === 0 && this.planStart === 1) { | ||
skipAll = true | ||
if (this.count === 0) { | ||
this.ok = true | ||
} else { | ||
this.tapError('Plan of 1..0, but test points encountered') | ||
if (this.buffer) | ||
this.write('\n') | ||
// if we have yamlish, means we didn't finish with a ... | ||
if (this.yamlish) | ||
this.yamlGarbage() | ||
this.emitResult() | ||
if (this.syntheticBailout && this.level === 0) { | ||
let reason = this.bailedOut | ||
if (reason === true) | ||
reason = '' | ||
else | ||
reason = ' ' + reason | ||
// reason += ' Parser(384 l=' + this.level + ')' | ||
this.emit('line', 'Bail out!' + reason + '\n') | ||
} | ||
} else if (!this.bailedOut && this.planStart === -1) { | ||
if (this.count === 0 && !this.syntheticPlan) { | ||
this.syntheticPlan = true | ||
this.plan(1, 0, 'no tests found', '1..0 # no tests found\n') | ||
let skipAll | ||
if (this.planEnd === 0 && this.planStart === 1) { | ||
skipAll = true | ||
} else { | ||
this.tapError('no plan') | ||
if (this.count === 0) { | ||
this.ok = true | ||
} else { | ||
this.tapError('Plan of 1..0, but test points encountered') | ||
} | ||
} else if (!this.bailedOut && this.planStart === -1) { | ||
if (this.count === 0 && !this.syntheticPlan) { | ||
this.syntheticPlan = true | ||
this.plan(1, 0, 'no tests found', '1..0 # no tests found\n') | ||
skipAll = true | ||
} else { | ||
this.tapError('no plan') | ||
} | ||
} else if (this.ok && this.count !== (this.planEnd - this.planStart + 1)) { | ||
this.tapError('incorrect number of tests') | ||
} | ||
} else if (this.ok && this.count !== (this.planEnd - this.planStart + 1)) { | ||
this.tapError('incorrect number of tests') | ||
} | ||
if (this.ok && !skipAll && this.first !== this.planStart) { | ||
this.tapError('first test id does not match plan start') | ||
} | ||
if (this.ok && !skipAll && this.first !== this.planStart) { | ||
this.tapError('first test id does not match plan start') | ||
} | ||
if (this.ok && !skipAll && this.last !== this.planEnd) { | ||
this.tapError('last test id does not match plan end') | ||
if (this.ok && !skipAll && this.last !== this.planEnd) { | ||
this.tapError('last test id does not match plan end') | ||
} | ||
this.emitComplete(skipAll) | ||
if (cb) | ||
process.nextTick(cb) | ||
} | ||
Writable.prototype.end.call(this, null, null, cb) | ||
this.emitComplete(skipAll) | ||
} | ||
emitComplete (skipAll) { | ||
if (!this.results) { | ||
const res = this.results = new FinalResults(!!skipAll, this) | ||
Parser.prototype.emitComplete = function (skipAll) { | ||
if (!this.results) { | ||
var res = this.results = new FinalResults(!!skipAll, this) | ||
if (!res.bailout) { | ||
// comment a bit at the end so we know what happened. | ||
// but don't repeat these comments if they're already present. | ||
if (res.plan.end !== res.count) | ||
this.emitComment('test count(' + res.count + | ||
') != plan(' + res.plan.end + ')', false, true) | ||
if (!res.bailout) { | ||
// comment a bit at the end so we know what happened. | ||
// but don't repeat these comments if they're already present. | ||
if (res.plan.end !== res.count) | ||
this.emitComment('test count(' + res.count + | ||
') != plan(' + res.plan.end + ')', false, true) | ||
if (res.fail > 0 && !res.ok) | ||
this.emitComment('failed ' + res.fail + | ||
(res.count > 1 ? ' of ' + res.count + ' tests' | ||
: ' test'), | ||
false, true) | ||
if (res.fail > 0 && !res.ok) | ||
this.emitComment('failed ' + res.fail + | ||
(res.count > 1 ? ' of ' + res.count + ' tests' | ||
: ' test'), | ||
false, true) | ||
if (res.todo > 0) | ||
this.emitComment('todo: ' + res.todo, false, true) | ||
if (res.todo > 0) | ||
this.emitComment('todo: ' + res.todo, false, true) | ||
if (res.skip > 0) | ||
this.emitComment('skip: ' + res.skip, false, true) | ||
} | ||
if (res.skip > 0) | ||
this.emitComment('skip: ' + res.skip, false, true) | ||
this.emit('complete', this.results) | ||
} | ||
} | ||
this.emit('complete', this.results) | ||
version (version, line) { | ||
// If version is specified, must be at the very beginning. | ||
if (version >= 13 && | ||
this.planStart === -1 && | ||
this.count === 0 && | ||
!this.current) { | ||
this.emit('line', line) | ||
this.emit('version', version) | ||
} else | ||
this.nonTap(line) | ||
} | ||
} | ||
function FinalResults (skipAll, self) { | ||
this.ok = self.ok | ||
this.count = self.count | ||
this.pass = self.pass | ||
this.fail = self.fail || 0 | ||
this.bailout = self.bailedOut || false | ||
this.todo = self.todo || 0 | ||
this.skip = skipAll ? self.count : self.skip || 0 | ||
this.plan = new FinalPlan(skipAll, self) | ||
this.failures = self.failures | ||
if (self.passes) | ||
this.passes = self.passes | ||
} | ||
pragma (key, value, line) { | ||
// can't put a pragma in a child or yaml block | ||
if (this.child) { | ||
this.nonTap(line) | ||
return | ||
} | ||
function FinalPlan (skipAll, self) { | ||
this.start = self.planStart === -1 ? null : self.planStart | ||
this.end = self.planStart === -1 ? null : self.planEnd | ||
this.skipAll = skipAll | ||
this.skipReason = skipAll ? self.planComment : '' | ||
this.comment = self.planComment || '' | ||
} | ||
Parser.prototype.version = function (version, line) { | ||
// If version is specified, must be at the very beginning. | ||
if (version >= 13 && | ||
this.planStart === -1 && | ||
this.count === 0 && | ||
!this.current) { | ||
this.emitResult() | ||
if (this.bailedOut) | ||
return | ||
// only the 'strict' pragma is currently relevant | ||
if (key === 'strict') { | ||
this.strict = value | ||
} | ||
this.pragmas[key] = value | ||
this.emit('line', line) | ||
this.emit('version', version) | ||
} else | ||
this.nonTap(line) | ||
} | ||
Parser.prototype.pragma = function (key, value, line) { | ||
// can't put a pragma in a child or yaml block | ||
if (this.child) { | ||
this.nonTap(line) | ||
return | ||
this.emit('pragma', key, value) | ||
} | ||
this.emitResult() | ||
if (this.bailedOut) | ||
return | ||
// only the 'strict' pragma is currently relevant | ||
if (key === 'strict') { | ||
this.strict = value | ||
} | ||
this.pragmas[key] = value | ||
this.emit('line', line) | ||
this.emit('pragma', key, value) | ||
} | ||
bailout (reason, synthetic) { | ||
// console.trace('bailout') | ||
this.syntheticBailout = synthetic | ||
Parser.prototype.bailout = function (reason, synthetic) { | ||
this.syntheticBailout = synthetic | ||
if (this.bailingOut) | ||
return | ||
if (this.bailingOut) | ||
return | ||
// Guard because emitting a result can trigger a forced bailout | ||
// if the harness decides that failures should be bailouts. | ||
this.bailingOut = reason || true | ||
// Guard because emitting a result can trigger a forced bailout | ||
// if the harness decides that failures should be bailouts. | ||
this.bailingOut = reason || true | ||
if (!synthetic) | ||
this.emitResult() | ||
else | ||
this.current = null | ||
if (!synthetic) | ||
this.emitResult() | ||
else | ||
this.current = null | ||
this.bailedOut = this.bailingOut | ||
this.ok = false | ||
if (!synthetic) { | ||
// synthetic bailouts get emitted on end | ||
var line = 'Bail out!' | ||
if (reason) | ||
line += ' ' + reason | ||
this.emit('line', line + '\n') | ||
this.bailedOut = this.bailingOut | ||
this.ok = false | ||
if (!synthetic) { | ||
// synthetic bailouts get emitted on end | ||
let line = 'Bail out!' | ||
if (reason) | ||
line += ' ' + reason | ||
// reason += ' Parser(524 s=' + synthetic + ')' | ||
this.emit('line', line + '\n') | ||
} | ||
this.emit('bailout', reason) | ||
if (this.parent) { | ||
this.end() | ||
this.parent.bailout(reason, true) | ||
} | ||
} | ||
this.emit('bailout', reason) | ||
if (this.parent) { | ||
this.end() | ||
this.parent.bailout(reason, true) | ||
} | ||
} | ||
Parser.prototype.clearExtraQueue = function () { | ||
for (var c = 0; c < this.extraQueue.length; c++) { | ||
this.emit(this.extraQueue[c][0], this.extraQueue[c][1]) | ||
clearExtraQueue () { | ||
for (let c = 0; c < this.extraQueue.length; c++) { | ||
this.emit(this.extraQueue[c][0], this.extraQueue[c][1]) | ||
} | ||
this.extraQueue.length = 0 | ||
} | ||
this.extraQueue.length = 0 | ||
} | ||
Parser.prototype.endChild = function () { | ||
if (this.child) { | ||
this.child.end() | ||
this.child = null | ||
endChild () { | ||
if (this.child) { | ||
this.child.end() | ||
this.child = null | ||
} | ||
} | ||
} | ||
Parser.prototype.emitResult = function () { | ||
if (this.bailedOut) | ||
return | ||
emitResult () { | ||
if (this.bailedOut) | ||
return | ||
this.endChild() | ||
this.resetYamlish() | ||
this.endChild() | ||
this.resetYamlish() | ||
if (!this.current) | ||
return this.clearExtraQueue() | ||
if (!this.current) | ||
return this.clearExtraQueue() | ||
var res = this.current | ||
this.current = null | ||
const res = this.current | ||
this.current = null | ||
this.count++ | ||
if (res.ok) { | ||
this.pass++ | ||
if (this.passes) | ||
this.passes.push(res) | ||
} else { | ||
this.fail++ | ||
if (!res.todo && !res.skip) { | ||
this.ok = false | ||
this.failures.push(res) | ||
this.count++ | ||
if (res.ok) { | ||
this.pass++ | ||
if (this.passes) | ||
this.passes.push(res) | ||
} else { | ||
this.fail++ | ||
if (!res.todo && !res.skip) { | ||
this.ok = false | ||
this.failures.push(res) | ||
} | ||
} | ||
} | ||
if (res.skip) | ||
this.skip++ | ||
if (res.skip) | ||
this.skip++ | ||
if (res.todo) | ||
this.todo++ | ||
if (res.todo) | ||
this.todo++ | ||
this.emit('assert', res) | ||
if (this.bail && !res.ok && !res.todo && !res.skip && !this.bailingOut) { | ||
this.maybeChild = null | ||
var ind = new Array(this.level + 1).join(' ') | ||
for (var p = this; p.parent; p = p.parent); | ||
var bailName = res.name ? ' # ' + res.name : '' | ||
p.parse(ind + 'Bail out!' + bailName + '\n') | ||
this.emit('assert', res) | ||
if (this.bail && !res.ok && !res.todo && !res.skip && !this.bailingOut) { | ||
this.maybeChild = null | ||
const ind = new Array(this.level + 1).join(' ') | ||
let p | ||
for (p = this; p.parent; p = p.parent); | ||
const bailName = res.name ? ' # ' + res.name : '' | ||
// bailName += ' Parser(586)' | ||
p.parse(ind + 'Bail out!' + bailName + '\n') | ||
} | ||
this.clearExtraQueue() | ||
} | ||
this.clearExtraQueue() | ||
} | ||
// TODO: We COULD say that any "relevant tap" line that's indented | ||
// by 4 spaces starts a child test, and just call it 'unnamed' if | ||
// it does not have a prefix comment. In that case, any number of | ||
// 4-space indents can be plucked off to try to find a relevant | ||
// TAP line type, and if so, start the unnamed child. | ||
Parser.prototype.startChild = function (line) { | ||
var maybeBuffered = this.current && this.current.buffered | ||
var unindentStream = !maybeBuffered && this.maybeChild | ||
var indentStream = !maybeBuffered && !unindentStream && | ||
lineTypes.subtestIndent.test(line) | ||
var unnamed = !maybeBuffered && !unindentStream && !indentStream | ||
// TODO: We COULD say that any "relevant tap" line that's indented | ||
// by 4 spaces starts a child test, and just call it 'unnamed' if | ||
// it does not have a prefix comment. In that case, any number of | ||
// 4-space indents can be plucked off to try to find a relevant | ||
// TAP line type, and if so, start the unnamed child. | ||
startChild (line) { | ||
const maybeBuffered = this.current && this.current.buffered | ||
const unindentStream = !maybeBuffered && this.maybeChild | ||
const indentStream = !maybeBuffered && !unindentStream && | ||
lineTypes.subtestIndent.test(line) | ||
const unnamed = !maybeBuffered && !unindentStream && !indentStream | ||
// If we have any other result waiting in the wings, we need to emit | ||
// that now. A buffered test emits its test point at the *end* of | ||
// the child subtest block, so as to match streamed test semantics. | ||
if (!maybeBuffered) | ||
this.emitResult() | ||
// If we have any other result waiting in the wings, we need to emit | ||
// that now. A buffered test emits its test point at the *end* of | ||
// the child subtest block, so as to match streamed test semantics. | ||
if (!maybeBuffered) | ||
this.emitResult() | ||
if (this.bailedOut) | ||
return | ||
if (this.bailedOut) | ||
return | ||
this.child = new Parser({ | ||
bail: this.bail, | ||
parent: this, | ||
level: this.level + 1, | ||
buffered: maybeBuffered, | ||
preserveWhitespace: this.preserveWhitespace, | ||
omitVersion: true, | ||
strict: this.strict | ||
}) | ||
this.child = new Parser({ | ||
bail: this.bail, | ||
parent: this, | ||
level: this.level + 1, | ||
buffered: maybeBuffered, | ||
preserveWhitespace: this.preserveWhitespace, | ||
omitVersion: true, | ||
strict: this.strict | ||
}) | ||
var self = this | ||
this.child.on('complete', function (results) { | ||
if (!results.ok) | ||
self.ok = false | ||
}) | ||
this.child.on('complete', results => { | ||
if (!results.ok) | ||
this.ok = false | ||
}) | ||
this.child.on('line', function (l) { | ||
if (this.syntheticPlan) | ||
return | ||
this.child.on('line', l => { | ||
if (l.trim() || this.preserveWhitespace) | ||
l = ' ' + l | ||
this.emit('line', l) | ||
}) | ||
if (l.trim() || self.preserveWhitespace) | ||
l = ' ' + l | ||
self.emit('line', l) | ||
}) | ||
// Canonicalize the parsing result of any kind of subtest | ||
// if it's a buffered subtest or a non-indented Subtest directive, | ||
// then synthetically emit the Subtest comment | ||
line = line.substr(4) | ||
let subtestComment | ||
if (indentStream) { | ||
subtestComment = line | ||
line = null | ||
} else if (maybeBuffered) { | ||
subtestComment = '# Subtest: ' + this.current.name + '\n' | ||
} else { | ||
subtestComment = this.maybeChild || '# Subtest: (anonymous)\n' | ||
} | ||
// Canonicalize the parsing result of any kind of subtest | ||
// if it's a buffered subtest or a non-indented Subtest directive, | ||
// then synthetically emit the Subtest comment | ||
line = line.substr(4) | ||
var subtestComment | ||
if (indentStream) { | ||
subtestComment = line | ||
line = null | ||
} else if (maybeBuffered) { | ||
subtestComment = '# Subtest: ' + this.current.name + '\n' | ||
} else { | ||
subtestComment = this.maybeChild || '# Subtest: (anonymous)\n' | ||
this.maybeChild = null | ||
this.child.name = subtestComment.substr('# Subtest: '.length).trim() | ||
// at some point, we may wish to move 100% to preferring | ||
// the Subtest comment on the parent level. If so, uncomment | ||
// this line, and remove the child.emitComment below. | ||
// this.emit('comment', subtestComment) | ||
if (!this.child.buffered) | ||
this.emit('line', subtestComment) | ||
this.emit('child', this.child) | ||
this.child.emitComment(subtestComment, true) | ||
if (line) | ||
this.child.parse(line) | ||
} | ||
this.maybeChild = null | ||
this.child.name = subtestComment.substr('# Subtest: '.length).trim() | ||
abort (message, extra) { | ||
if (this.child) { | ||
const b = this.child.buffered | ||
this.child.abort(message, extra) | ||
extra = null | ||
if (b) | ||
this.write('\n}\n') | ||
} | ||
// at some point, we may wish to move 100% to preferring | ||
// the Subtest comment on the parent level. If so, uncomment | ||
// this line, and remove the child.emitComment below. | ||
// this.emit('comment', subtestComment) | ||
if (!this.child.buffered) | ||
this.emit('line', subtestComment) | ||
this.emit('child', this.child) | ||
this.child.emitComment(subtestComment, true) | ||
if (line) | ||
this.child.parse(line) | ||
} | ||
let dump | ||
if (extra && Object.keys(extra).length) { | ||
try { | ||
dump = yaml.safeDump(extra).trimRight() | ||
} catch (er) {} | ||
} | ||
Parser.prototype.abort = function (message, extra) { | ||
if (this.child) { | ||
var b = this.child.buffered | ||
this.child.abort(message, extra) | ||
extra = null | ||
if (b) | ||
this.write('\n}\n') | ||
} | ||
let y | ||
if (dump) | ||
y = ' ---\n ' + dump.split('\n').join('\n ') + '\n ...\n' | ||
else | ||
y = '\n' | ||
let n = (this.count || 0) + 1 | ||
if (this.current) | ||
n += 1 | ||
var dump | ||
if (extra && Object.keys(extra).length) { | ||
try { | ||
dump = yaml.safeDump(extra).trimRight() | ||
} catch (er) {} | ||
} | ||
if (this.planEnd !== -1 && this.planEnd < n && this.parent) { | ||
// skip it, let the parent do this. | ||
this.aborted = true | ||
return | ||
} | ||
var y | ||
if (dump) | ||
y = ' ---\n ' + dump.split('\n').join('\n ') + '\n ...\n' | ||
else | ||
y = '\n' | ||
var n = (this.count || 0) + 1 | ||
if (this.current) | ||
n += 1 | ||
let ind = '' // new Array(this.level + 1).join(' ') | ||
message = message.replace(/[\n\r\s\t]/g, ' ') | ||
let point = '\nnot ok ' + n + ' - ' + message + '\n' + y | ||
if (this.planEnd !== -1 && this.planEnd < n && this.parent) { | ||
// skip it, let the parent do this. | ||
if (this.planEnd === -1) | ||
point += '1..' + n + '\n' | ||
this.write(point) | ||
this.aborted = true | ||
return | ||
this.end() | ||
} | ||
var ind = '' // new Array(this.level + 1).join(' ') | ||
message = message.replace(/[\n\r\s\t]/g, ' ') | ||
var point = '\nnot ok ' + n + ' - ' + message + '\n' + y | ||
emitComment (line, skipLine, noDuplicate) { | ||
if (line.trim().charAt(0) !== '#') | ||
line = '# ' + line | ||
if (this.planEnd === -1) | ||
point += '1..' + n + '\n' | ||
if (line.slice(-1) !== '\n') | ||
line += '\n' | ||
this.write(point) | ||
this.aborted = true | ||
this.end() | ||
} | ||
if (noDuplicate && this.comments.indexOf(line) !== -1) | ||
return | ||
Parser.prototype.emitComment = function (line, skipLine, noDuplicate) { | ||
if (line.trim().charAt(0) !== '#') | ||
line = '# ' + line | ||
this.comments.push(line) | ||
if (this.current || this.extraQueue.length) { | ||
// no way to get here with skipLine being true | ||
this.extraQueue.push(['line', line]) | ||
this.extraQueue.push(['comment', line]) | ||
} else { | ||
if (!skipLine) | ||
this.emit('line', line) | ||
this.emit('comment', line) | ||
} | ||
} | ||
if (line.slice(-1) !== '\n') | ||
line += '\n' | ||
parse (line) { | ||
// normalize line endings | ||
line = line.replace(/\r\n$/, '\n') | ||
if (noDuplicate && this.comments.indexOf(line) !== -1) | ||
return | ||
// sometimes empty lines get trimmed, but are still part of | ||
// a subtest or a yaml block. Otherwise, nothing to parse! | ||
if (line === '\n') { | ||
if (this.child) | ||
line = ' ' + line | ||
else if (this.yind) | ||
line = this.yind + line | ||
} | ||
this.comments.push(line) | ||
if (this.current || this.extraQueue.length) { | ||
// no way to get here with skipLine being true | ||
this.extraQueue.push(['line', line]) | ||
this.extraQueue.push(['comment', line]) | ||
} else { | ||
if (!skipLine) | ||
this.emit('line', line) | ||
this.emit('comment', line) | ||
} | ||
} | ||
// If we're bailing out, then the only thing we want to see is the | ||
// end of a buffered child test. Anything else should be ignored. | ||
// But! if we're bailing out a nested child, and ANOTHER nested child | ||
// comes after that one, then we don't want the second child's } to | ||
// also show up, or it looks weird. | ||
if (this.bailingOut) { | ||
if (!/^\s*}\n$/.test(line)) | ||
return | ||
else if (!this.braceLevel || line.length < this.braceLevel) | ||
this.braceLevel = line.length | ||
else | ||
return | ||
} | ||
Parser.prototype.parse = function (line) { | ||
// normalize line endings | ||
line = line.replace(/\r\n$/, '\n') | ||
// This allows omitting even parsing the version if the test is | ||
// an indented child test. Several parsers get upset when they | ||
// see an indented version field. | ||
if (this.omitVersion && lineTypes.version.test(line) && !this.yind) | ||
return | ||
// sometimes empty lines get trimmed, but are still part of | ||
// a subtest or a yaml block. Otherwise, nothing to parse! | ||
if (line === '\n') { | ||
if (this.child) | ||
line = ' ' + line | ||
else if (this.yind) | ||
line = this.yind + line | ||
} | ||
// If we're bailing out, then the only thing we want to see is the | ||
// end of a buffered child test. Anything else should be ignored. | ||
// But! if we're bailing out a nested child, and ANOTHER nested child | ||
// comes after that one, then we don't want the second child's } to | ||
// also show up, or it looks weird. | ||
if (this.bailingOut) { | ||
if (!/^\s*}\n$/.test(line)) | ||
// check to see if the line is indented. | ||
// if it is, then it's either a subtest, yaml, or garbage. | ||
const indent = line.match(/^[ \t]*/)[0] | ||
if (indent) { | ||
this.parseIndent(line, indent) | ||
return | ||
else if (!this.braceLevel || line.length < this.braceLevel) | ||
this.braceLevel = line.length | ||
else | ||
return | ||
} | ||
} | ||
// This allows omitting even parsing the version if the test is | ||
// an indented child test. Several parsers get upset when they | ||
// see an indented version field. | ||
if (this.omitVersion && lineTypes.version.test(line) && !this.yind) | ||
return | ||
// In any case where we're going to emitResult, that can trigger | ||
// a bailout, so we need to only emit the line once we know that | ||
// isn't happening, to prevent cases where there's a bailout, and | ||
// then one more line of output. That'll also prevent the case | ||
// where the test point is emitted AFTER the line that follows it. | ||
// check to see if the line is indented. | ||
// if it is, then it's either a subtest, yaml, or garbage. | ||
var indent = line.match(/^[ \t]*/)[0] | ||
if (indent) { | ||
this.parseIndent(line, indent) | ||
return | ||
} | ||
// buffered subtests must end with a } | ||
if (this.child && this.child.buffered && line === '}\n') { | ||
this.endChild() | ||
this.emit('line', line) | ||
this.emitResult() | ||
return | ||
} | ||
// In any case where we're going to emitResult, that can trigger | ||
// a bailout, so we need to only emit the line once we know that | ||
// isn't happening, to prevent cases where there's a bailout, and | ||
// then one more line of output. That'll also prevent the case | ||
// where the test point is emitted AFTER the line that follows it. | ||
// just a \n, emit only if we care about whitespace | ||
const validLine = this.preserveWhitespace || line.trim() || this.yind | ||
if (line === '\n') | ||
return validLine && this.emit('line', line) | ||
// buffered subtests must end with a } | ||
if (this.child && this.child.buffered && line === '}\n') { | ||
this.endChild() | ||
this.emit('line', line) | ||
this.emitResult() | ||
return | ||
} | ||
// buffered subtest with diagnostics | ||
if (this.current && line === '{\n' && | ||
!this.current.buffered && | ||
!this.child) { | ||
this.emit('line', line) | ||
this.current.buffered = true | ||
return | ||
} | ||
// just a \n, emit only if we care about whitespace | ||
var validLine = this.preserveWhitespace || line.trim() || this.yind | ||
if (line === '\n') | ||
return validLine && this.emit('line', line) | ||
// now we know it's not indented, so if it's either valid tap | ||
// or garbage. Get the type of line. | ||
const type = lineType(line) | ||
if (!type) { | ||
this.nonTap(line) | ||
return | ||
} | ||
// buffered subtest with diagnostics | ||
if (this.current && line === '{\n' && | ||
!this.current.buffered && | ||
!this.child) { | ||
this.emit('line', line) | ||
this.current.buffered = true | ||
return | ||
} | ||
if (type[0] === 'comment') { | ||
this.emitComment(line) | ||
return | ||
} | ||
// now we know it's not indented, so if it's either valid tap | ||
// or garbage. Get the type of line. | ||
var type = lineType(line) | ||
if (!type) { | ||
this.nonTap(line) | ||
return | ||
} | ||
// if we have any yamlish, it's garbage now. We tolerate non-TAP and | ||
// comments in the midst of yaml (though, perhaps, that's questionable | ||
// behavior), but any actual TAP means that the yaml block was just | ||
// not valid. | ||
if (this.yind) | ||
this.yamlGarbage() | ||
if (type[0] === 'comment') { | ||
this.emitComment(line) | ||
return | ||
} | ||
// If it's anything other than a comment or garbage, then any | ||
// maybeChild is just an unsatisfied promise. | ||
if (this.maybeChild) { | ||
this.emitComment(this.maybeChild) | ||
this.maybeChild = null | ||
} | ||
// if we have any yamlish, it's garbage now. We tolerate non-TAP and | ||
// comments in the midst of yaml (though, perhaps, that's questionable | ||
// behavior), but any actual TAP means that the yaml block was just | ||
// not valid. | ||
if (this.yind) | ||
this.yamlGarbage() | ||
// nothing but comments can come after a trailing plan | ||
if (this.postPlan) { | ||
this.nonTap(line) | ||
return | ||
} | ||
// If it's anything other than a comment or garbage, then any | ||
// maybeChild is just an unsatisfied promise. | ||
if (this.maybeChild) { | ||
this.emitComment(this.maybeChild) | ||
this.maybeChild = null | ||
} | ||
// ok, now it's maybe a thing | ||
if (type[0] === 'bailout') { | ||
this.bailout(type[1][1].trim(), false) | ||
return | ||
} | ||
// nothing but comments can come after a trailing plan | ||
if (this.postPlan) { | ||
this.nonTap(line) | ||
return | ||
} | ||
if (type[0] === 'pragma') { | ||
const pragma = type[1] | ||
this.pragma(pragma[2], pragma[1] === '+', line) | ||
return | ||
} | ||
// ok, now it's maybe a thing | ||
if (type[0] === 'bailout') { | ||
this.bailout(type[1][1].trim(), false) | ||
return | ||
} | ||
if (type[0] === 'version') { | ||
const version = type[1] | ||
this.version(parseInt(version[1], 10), line) | ||
return | ||
} | ||
if (type[0] === 'pragma') { | ||
var pragma = type[1] | ||
this.pragma(pragma[2], pragma[1] === '+', line) | ||
return | ||
} | ||
if (type[0] === 'plan') { | ||
const plan = type[1] | ||
this.plan(+plan[1], +plan[2], (plan[3] || '').trim(), line) | ||
return | ||
} | ||
if (type[0] === 'version') { | ||
var version = type[1] | ||
this.version(parseInt(version[1], 10), line) | ||
return | ||
} | ||
// streamed subtests will end when this test point is emitted | ||
if (type[0] === 'testPoint') { | ||
// note: it's weird, but possible, to have a testpoint ending in | ||
// { before a streamed subtest which ends with a test point | ||
// instead of a }. In this case, the parser gets confused, but | ||
// also, even beginning to handle that means doing a much more | ||
// involved multi-line parse. By that point, the subtest block | ||
// has already been emitted as a 'child' event, so it's too late | ||
// to really do the optimal thing. The only way around would be | ||
// to buffer up everything and do a multi-line parse. This is | ||
// rare and weird, and a multi-line parse would be a bigger | ||
// rewrite, so I'm allowing it as it currently is. | ||
this.parseTestPoint(type[1], line) | ||
return | ||
} | ||
if (type[0] === 'plan') { | ||
var plan = type[1] | ||
this.plan(+plan[1], +plan[2], (plan[3] || '').trim(), line) | ||
return | ||
// We already detected nontap up above, so the only case left | ||
// should be a `# Subtest:` comment. Ignore for coverage, but | ||
// include the error here just for good measure. | ||
/* istanbul ignore else */ | ||
if (type[0] === 'subtest') { | ||
// this is potentially a subtest. Not indented. | ||
// hold until later. | ||
this.maybeChild = line | ||
} else { | ||
throw new Error('Unhandled case: ' + type[0]) | ||
} | ||
} | ||
// streamed subtests will end when this test point is emitted | ||
if (type[0] === 'testPoint') { | ||
// note: it's weird, but possible, to have a testpoint ending in | ||
// { before a streamed subtest which ends with a test point | ||
// instead of a }. In this case, the parser gets confused, but | ||
// also, even beginning to handle that means doing a much more | ||
// involved multi-line parse. By that point, the subtest block | ||
// has already been emitted as a 'child' event, so it's too late | ||
// to really do the optimal thing. The only way around would be | ||
// to buffer up everything and do a multi-line parse. This is | ||
// rare and weird, and a multi-line parse would be a bigger | ||
// rewrite, so I'm allowing it as it currently is. | ||
this.parseTestPoint(type[1], line) | ||
return | ||
} | ||
parseIndent (line, indent) { | ||
// still belongs to the child, so pass it along. | ||
if (this.child && line.substr(0, 4) === ' ') { | ||
line = line.substr(4) | ||
this.child.write(line) | ||
return | ||
} | ||
// We already detected nontap up above, so the only case left | ||
// should be a `# Subtest:` comment. Ignore for coverage, but | ||
// include the error here just for good measure. | ||
/* istanbul ignore else */ | ||
if (type[0] === 'subtest') { | ||
// this is potentially a subtest. Not indented. | ||
// hold until later. | ||
this.maybeChild = line | ||
} else { | ||
throw new Error('Unhandled case: ' + type[0]) | ||
} | ||
} | ||
// one of: | ||
// - continuing yaml block | ||
// - starting yaml block | ||
// - ending yaml block | ||
// - body of a new child subtest that was previously introduced | ||
// - An indented subtest directive | ||
// - A comment, or garbage | ||
Parser.prototype.parseIndent = function (line, indent) { | ||
// still belongs to the child, so pass it along. | ||
if (this.child && line.substr(0, 4) === ' ') { | ||
line = line.substr(4) | ||
this.child.write(line) | ||
return | ||
} | ||
// continuing/ending yaml block | ||
if (this.yind) { | ||
if (line.indexOf(this.yind) === 0) { | ||
this.emit('line', line) | ||
this.yamlishLine(line) | ||
return | ||
} else { | ||
// oops! that was not actually yamlish, I guess. | ||
// this is a case where the indent is shortened mid-yamlish block | ||
// treat existing yaml as garbage, continue parsing this line | ||
this.yamlGarbage() | ||
} | ||
} | ||
// one of: | ||
// - continuing yaml block | ||
// - starting yaml block | ||
// - ending yaml block | ||
// - body of a new child subtest that was previously introduced | ||
// - An indented subtest directive | ||
// - A comment, or garbage | ||
// continuing/ending yaml block | ||
if (this.yind) { | ||
if (line.indexOf(this.yind) === 0) { | ||
// start a yaml block under a test point | ||
if (this.current && !this.yind && line === indent + '---\n') { | ||
this.yind = indent | ||
this.emit('line', line) | ||
this.yamlishLine(line) | ||
return | ||
} else { | ||
// oops! that was not actually yamlish, I guess. | ||
// this is a case where the indent is shortened mid-yamlish block | ||
// treat existing yaml as garbage, continue parsing this line | ||
this.yamlGarbage() | ||
} | ||
} | ||
// at this point, not yamlish, and not an existing child test. | ||
// We may have already seen an unindented Subtest directive, or | ||
// a test point that ended in { indicating a buffered subtest | ||
// Child tests are always indented 4 spaces. | ||
if (line.substr(0, 4) === ' ') { | ||
if (this.maybeChild || | ||
this.current && this.current.buffered || | ||
lineTypes.subtestIndent.test(line)) { | ||
this.startChild(line) | ||
return | ||
} | ||
// start a yaml block under a test point | ||
if (this.current && !this.yind && line === indent + '---\n') { | ||
this.yind = indent | ||
this.emit('line', line) | ||
return | ||
} | ||
// It's _something_ indented, if the indentation is divisible by | ||
// 4 spaces, and the result is actual TAP of some sort, then do | ||
// a child subtest for it as well. | ||
// | ||
// This will lead to some ambiguity in cases where there are multiple | ||
// levels of non-signaled subtests, but a Subtest comment in the | ||
// middle of them, which may or may not be considered "indented" | ||
// See the subtest-no-comment-mid-comment fixture for an example | ||
// of this. As it happens, the preference is towards an indented | ||
// Subtest comment as the interpretation, which is the only possible | ||
// way to resolve this, since otherwise there's no way to distinguish | ||
// between an anonymous subtest with a non-indented Subtest comment, | ||
// and an indented Subtest comment. | ||
const s = line.match(/( {4})+(.*\n)$/) | ||
if (s[2].charAt(0) !== ' ') { | ||
// integer number of indentations. | ||
const type = lineType(s[2]) | ||
if (type) { | ||
if (type[0] === 'comment') { | ||
this.emit('line', line) | ||
this.emitComment(line) | ||
} else { | ||
// it's relevant! start as an "unnamed" child subtest | ||
this.startChild(line) | ||
} | ||
return | ||
} | ||
} | ||
} | ||
// at this point, not yamlish, and not an existing child test. | ||
// We may have already seen an unindented Subtest directive, or | ||
// a test point that ended in { indicating a buffered subtest | ||
// Child tests are always indented 4 spaces. | ||
if (line.substr(0, 4) === ' ') { | ||
if (this.maybeChild || | ||
this.current && this.current.buffered || | ||
lineTypes.subtestIndent.test(line)) { | ||
this.startChild(line) | ||
// at this point, it's either a non-subtest comment, or garbage. | ||
if (lineTypes.comment.test(line)) { | ||
this.emitComment(line) | ||
return | ||
} | ||
// It's _something_ indented, if the indentation is divisible by | ||
// 4 spaces, and the result is actual TAP of some sort, then do | ||
// a child subtest for it as well. | ||
// | ||
// This will lead to some ambiguity in cases where there are multiple | ||
// levels of non-signaled subtests, but a Subtest comment in the | ||
// middle of them, which may or may not be considered "indented" | ||
// See the subtest-no-comment-mid-comment fixture for an example | ||
// of this. As it happens, the preference is towards an indented | ||
// Subtest comment as the interpretation, which is the only possible | ||
// way to resolve this, since otherwise there's no way to distinguish | ||
// between an anonymous subtest with a non-indented Subtest comment, | ||
// and an indented Subtest comment. | ||
var s = line.match(/( {4})+(.*\n)$/) | ||
if (s[2].charAt(0) !== ' ') { | ||
// integer number of indentations. | ||
var type = lineType(s[2]) | ||
if (type) { | ||
if (type[0] === 'comment') { | ||
this.emit('line', line) | ||
this.emitComment(line) | ||
} else { | ||
// it's relevant! start as an "unnamed" child subtest | ||
this.startChild(line) | ||
} | ||
return | ||
} | ||
} | ||
this.nonTap(line) | ||
} | ||
} | ||
// at this point, it's either a non-subtest comment, or garbage. | ||
class FinalResults { | ||
constructor (skipAll, self) { | ||
this.ok = self.ok | ||
this.count = self.count | ||
this.pass = self.pass | ||
this.fail = self.fail || 0 | ||
this.bailout = self.bailedOut || false | ||
this.todo = self.todo || 0 | ||
this.skip = skipAll ? self.count : self.skip || 0 | ||
this.plan = new FinalPlan(skipAll, self) | ||
this.failures = self.failures | ||
if (self.passes) | ||
this.passes = self.passes | ||
} | ||
} | ||
if (lineTypes.comment.test(line)) { | ||
this.emitComment(line) | ||
return | ||
class FinalPlan { | ||
constructor (skipAll, self) { | ||
this.start = self.planStart === -1 ? null : self.planStart | ||
this.end = self.planStart === -1 ? null : self.planEnd | ||
this.skipAll = skipAll | ||
this.skipReason = skipAll ? self.planComment : '' | ||
this.comment = self.planComment || '' | ||
} | ||
} | ||
this.nonTap(line) | ||
} | ||
module.exports = Parser |
{ | ||
"name": "tap-parser", | ||
"version": "5.4.0", | ||
"version": "6.0.0", | ||
"description": "parse the test anything protocol", | ||
@@ -11,3 +11,4 @@ "main": "index.js", | ||
"events-to-array": "^1.0.1", | ||
"js-yaml": "^3.2.7" | ||
"js-yaml": "^3.2.7", | ||
"minipass": "^2.2.0" | ||
}, | ||
@@ -53,5 +54,3 @@ "devDependencies": { | ||
"license": "MIT", | ||
"optionalDependencies": { | ||
"readable-stream": "^2" | ||
}, | ||
"optionalDependencies": {}, | ||
"files": [ | ||
@@ -58,0 +57,0 @@ "index.js", |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
42618
964
+ Addedminipass@^2.2.0
+ Addedminipass@2.9.0(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedyallist@3.1.1(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removedinherits@2.0.4(transitive)
- Removedisarray@1.0.0(transitive)
- Removedprocess-nextick-args@2.0.1(transitive)
- Removedreadable-stream@2.3.8(transitive)
- Removedsafe-buffer@5.1.2(transitive)
- Removedstring_decoder@1.1.1(transitive)
- Removedutil-deprecate@1.0.2(transitive)