tap-parser
Advanced tools
Comparing version 0.7.0 to 1.0.0
#!/usr/bin/env node | ||
var minimist = require('minimist'); | ||
var parser = require('../'); | ||
var fs = require('fs'); | ||
var Parser = require('../') | ||
var etoa = require('events-to-array') | ||
var util = require('util') | ||
var argv = minimist(process.argv.slice(2), { | ||
alias: { | ||
h: 'help', v: 'version', | ||
o: 'outfile', i: 'infile', r: 'results' | ||
}, | ||
default: { outfile: '-', infile: '-' }, | ||
boolean: [ 'results' ] | ||
}); | ||
if (argv.help) { | ||
return fs.createReadStream(__dirname + '/usage.txt') | ||
.pipe(process.stdout) | ||
; | ||
var args = process.argv.slice(2) | ||
var json = null | ||
args.forEach(function (arg, i) { | ||
if (arg === '-j') { | ||
json = args[i + 1] || 2 | ||
} else { | ||
var m = arg.match(/^--json(?:=([0-9]+))$/) | ||
if (m) | ||
json = +m[1] || args[i + 1] || 2 | ||
} | ||
if (arg === '-h' || arg === '--help') | ||
usage() | ||
}) | ||
function usage () { | ||
console.log(function () {/* | ||
Usage: | ||
tap-parser [-j [<indent>] | --json[=indent]] | ||
Parses TAP data from stdin, and outputs an object representing | ||
the data found in the TAP stream to stdout. | ||
If there are any failures in the TAP stream, then exits with a | ||
non-zero status code. | ||
Data is output by default using node's `util.format()` method, but | ||
JSON can be specified using the `-j` or `--json` flag with a number | ||
of spaces to use as the indent (default=2). | ||
*/}.toString().split('\n').slice(1, -1).join('\n')) | ||
if (!process.stdin.isTTY) | ||
process.stdin.resume() | ||
process.exit() | ||
} | ||
if (argv.version) { | ||
console.log(require('../package.json').version); | ||
return; | ||
function format (msg) { | ||
if (json !== null) | ||
return JSON.stringify(msg, null, +json) | ||
else | ||
return util.inspect(events, null, Infinity) | ||
} | ||
var input = argv.infile === '-' | ||
? process.stdin | ||
: fs.createReadStream(argv.infile) | ||
; | ||
var output = argv.outfile === '-' | ||
? process.stdout | ||
: fs.createWriteStream(argv.outfile) | ||
; | ||
var parser = new Parser() | ||
var events = etoa(parser, [ 'pipe', 'unpipe', 'prefinish', 'finish' ]) | ||
input.pipe(parser(function (results) { | ||
process.on('exit', function (code) { | ||
if (code === 0) process.exit(results.ok ? 0 : 1); | ||
}); | ||
if (argv.results) { | ||
output.write(JSON.stringify(results, null, 2) + '\n'); | ||
if (output !== process.stdout) output.end(); | ||
} | ||
})); | ||
if (!argv.results) input.pipe(output); | ||
process.stdin.pipe(parser) | ||
process.on('exit', function () { | ||
console.log(format(events)) | ||
if (!parser.ok) | ||
process.exit(1) | ||
}) |
592
index.js
@@ -1,190 +0,440 @@ | ||
var Writable = require('readable-stream').Writable; | ||
var inherits = require('inherits'); | ||
// Transforms a stream of TAP into a stream of result objects | ||
// and string comments. Emits "results" event with summary. | ||
var Writable = require('stream').Writable | ||
var yaml = require('js-yaml') | ||
var util = require('util') | ||
var assert = require('assert') | ||
var re = { | ||
ok: new RegExp([ | ||
'^(not )?ok\\b(?:', | ||
'(?:\\s+(\\d+))?(?:\\s+(?:(?:\\s*-\\s*)?(.*)))?', | ||
')?' | ||
].join('')), | ||
plan: /^(\d+)\.\.(\d+)\b(?:\s+#\s+SKIP\s+(.*)$)?/, | ||
comment: /^#\s*(.+)/, | ||
version: /^TAP\s+version\s+(\d+)/i, | ||
label_todo: /^(.*?)\s*#\s*TODO\s+(.*)$/ | ||
}; | ||
util.inherits(Parser, Writable) | ||
module.exports = Parser; | ||
inherits(Parser, Writable); | ||
module.exports = Parser | ||
function Parser (cb) { | ||
if (!(this instanceof Parser)) return new Parser(cb); | ||
Writable.call(this, { encoding: 'string' }); | ||
if (cb) this.on('results', cb); | ||
this.results = { | ||
ok: undefined, | ||
asserts: [], | ||
pass: [], | ||
fail: [], | ||
todo: [], | ||
errors: [] | ||
}; | ||
this._lineNum = 1; | ||
this._line = ''; | ||
this._planMismatch = false; | ||
this.on('finish', function () { | ||
if (this._line.length) this._online(this._line); | ||
this._finished(); | ||
}); | ||
this.on('assert', this._onassert); | ||
this.on('plan', this._onplan); | ||
this.on('parseError', function (err) { | ||
this.results.ok = false; | ||
err.line = this._lineNum; | ||
this.results.errors.push(err); | ||
}); | ||
var testPointRE = /^(not )?ok(?: ([0-9]+))?(?:(?: - )?(.*))?\n$/ | ||
function createResult (line, count) { | ||
if (!testPointRE.test(line)) | ||
return null | ||
return new Result(line, count) | ||
} | ||
Parser.prototype._write = function (chunk, enc, next) { | ||
var parts = (this._line + chunk).split('\n'); | ||
for (var i = 0; i < parts.length - 1; i++) { | ||
this._online(parts[i]); | ||
this._lineNum ++; | ||
function parseDirective (line) { | ||
line = line.trim() | ||
var re = /^(todo|skip)\b/i | ||
var type = line.match(re) | ||
if (!type) | ||
return false | ||
return [ type[0].toLowerCase(), line.replace(re, '').trim() || true ] | ||
} | ||
function Result (line, count) { | ||
var parsed = line.match(testPointRE) | ||
assert(parsed, 'invalid line to Result') | ||
var ok = !parsed[1] | ||
var id = +(parsed[2] || count + 1) | ||
this.ok = ok | ||
this.id = id | ||
var src = line | ||
Object.defineProperty(this, 'src', { | ||
value: line, | ||
writable: true, | ||
enumerable: false, | ||
configurable: false | ||
}) | ||
this.src = line | ||
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('#') | ||
// now, let's see if there's a directive in there. | ||
var dir = parseDirective(rest.trim()) | ||
if (!dir) | ||
name += rest ? '#' + rest : '' | ||
else | ||
this[dir[0]] = dir[1] | ||
if (name) | ||
this.name = name.trim() | ||
return this | ||
} | ||
Object.defineProperty(Result.prototype, 'toString', { | ||
value: function () { | ||
return this.src | ||
}, | ||
enumerable: false, | ||
writable: true, | ||
configurable: true | ||
}) | ||
function Parser (options, onComplete) { | ||
if (typeof options === 'function') { | ||
onComplete = options | ||
options = {} | ||
} | ||
if (!(this instanceof Parser)) | ||
return new Parser(options, onComplete) | ||
options = options || {} | ||
if (onComplete) | ||
this.on('complete', onComplete) | ||
this.indent = options.indent || '' | ||
this.level = options.level || 0 | ||
Writable.call(this) | ||
this.buffer = '' | ||
this.bailedOut = false | ||
this.planStart = -1 | ||
this.planEnd = -1 | ||
this.planComment = '' | ||
this.yamlish = '' | ||
this.yind = '' | ||
this.child = null | ||
this.current = null | ||
this.count = 0 | ||
this.pass = 0 | ||
this.fail = 0 | ||
this.todo = 0 | ||
this.skip = 0 | ||
this.ok = true | ||
this.postPlan = false | ||
} | ||
Parser.prototype.processYamlish = function () { | ||
var yamlish = this.yamlish | ||
this.yamlish = '' | ||
this.yind = '' | ||
if (!this.current) { | ||
this.emit('extra', yamlish) | ||
return | ||
} | ||
try { | ||
var diags = yaml.safeLoad(yamlish) | ||
} catch (er) { | ||
this.emit('extra', yamlish) | ||
return | ||
} | ||
this.current.src += yamlish | ||
this.current.diag = diags | ||
this.emitResult() | ||
} | ||
Parser.prototype.write = function (chunk, encoding, cb) { | ||
if (typeof encoding === 'string' && encoding !== 'utf8') | ||
chunk = new Buffer(chunk, encoding) | ||
if (Buffer.isBuffer(chunk)) | ||
chunk += '' | ||
if (typeof encoding === 'function') { | ||
cb = encoding | ||
encoding = null | ||
} | ||
if (this.bailedOut) { | ||
if (cb) | ||
process.nextTick(cb) | ||
return true | ||
} | ||
this.buffer += chunk | ||
do { | ||
var match = this.buffer.match(/^.*\r?\n/) | ||
if (!match || this.bailedOut) | ||
break | ||
this.buffer = this.buffer.substr(match[0].length) | ||
this._parse(match[0]) | ||
} while (this.buffer.length) | ||
if (cb) | ||
process.nextTick(cb) | ||
return true | ||
} | ||
Parser.prototype.end = function (chunk, encoding, cb) { | ||
if (chunk) { | ||
if (typeof encoding === 'function') { | ||
cb = encoding | ||
encoding = null | ||
} | ||
this._line = parts[parts.length - 1]; | ||
next(); | ||
}; | ||
this.write(chunk, encoding) | ||
} | ||
Parser.prototype._onassert = function (res) { | ||
var results = this.results; | ||
results.asserts.push(res); | ||
if (!res.ok && !res.todo) results.ok = false; | ||
var dest = (res.ok ? results.pass : results.fail); | ||
if (res.todo) dest = results.todo; | ||
dest.push(res); | ||
var prev = results.asserts[results.asserts.length - 2]; | ||
if (this.buffer) | ||
this.write('\n') | ||
if (!res.number) { | ||
if (prev) res.number = prev.number + 1; | ||
else res.number = 1; | ||
// if we have yamlish, means we didn't finish with a ... | ||
if (this.yamlish) | ||
this.emit('extra', this.yamlish) | ||
this.emitResult() | ||
var skipAll | ||
if (this.planEnd === 0 && this.planStart === 1) { | ||
this.ok = true | ||
skipAll = true | ||
} else if (this.count !== (this.planEnd - this.planStart + 1)) | ||
this.ok = false | ||
if (this.ok && !skipAll && this.first !== this.planStart) | ||
this.ok = false | ||
if (this.ok && !skipAll && this.last !== this.planEnd) | ||
this.ok = false | ||
var final = { | ||
ok: this.ok, | ||
count: this.count, | ||
pass: this.pass | ||
} | ||
if (this.fail) | ||
final.fail = this.fail | ||
if (this.bailedOut) | ||
final.bailout = this.bailedOut | ||
if (this.todo) | ||
final.todo = this.todo | ||
if (this.skip) | ||
final.skip = this.skip | ||
if (this.planStart !== -1) { | ||
final.plan = { start: this.planStart, end: this.planEnd } | ||
if (skipAll) { | ||
final.plan.skipAll = true | ||
if (this.planComment) | ||
final.plan.skipReason = this.planComment | ||
} | ||
} | ||
if (prev && prev.number + 1 !== res.number) { | ||
this.emit('parseError', { | ||
message: 'assert out of order' | ||
}); | ||
this.emit('complete', final) | ||
Writable.prototype.end.call(this, null, null, cb) | ||
} | ||
Parser.prototype.bailout = function (reason) { | ||
this.bailedOut = reason || true | ||
this.ok = false | ||
this.emit('bailout', reason) | ||
} | ||
Parser.prototype.emitResult = function () { | ||
if (this.child) { | ||
this.child.end() | ||
this.child = null | ||
} | ||
this.yamlish = '' | ||
this.yind = '' | ||
if (!this.current) | ||
return | ||
var res = this.current | ||
this.current = null | ||
this.count++ | ||
if (res.ok) { | ||
this.pass++ | ||
} else { | ||
this.fail++ | ||
if (!res.todo) | ||
this.ok = false | ||
} | ||
if (res.skip) | ||
this.skip++ | ||
if (res.todo) | ||
this.todo++ | ||
this.emit('assert', res) | ||
} | ||
Parser.prototype.startChild = function (indent, line) { | ||
this.emitResult() | ||
this.child = new Parser({ | ||
indent: indent, | ||
parent: this, | ||
level: this.level + 1 | ||
}) | ||
this.emit('child', this.child) | ||
this.child.on('bailout', this.bailout.bind(this)) | ||
var self = this | ||
this.child.on('complete', function (results) { | ||
if (!results.ok) | ||
self.ok = false | ||
}) | ||
this.child.write(line.substr(indent.length)) | ||
} | ||
Parser.prototype._parse = function (line) { | ||
// normalize line endings | ||
line = line.replace(/\r\n$/, '\n') | ||
// ignore empty lines | ||
if (line === '\n') | ||
return | ||
// After a bailout, everything is ignored | ||
if (this.bailedOut) | ||
return | ||
// comment | ||
if (line.match(/^\s*#/)) { | ||
this.emit('comment', line) | ||
return | ||
} | ||
var bailout = line.match(/^bail out!(.*)\n$/i) | ||
if (bailout) { | ||
var reason = bailout[1].trim() | ||
this.bailout(reason) | ||
return | ||
} | ||
// If version is specified, must be at the very beginning. | ||
var version = line.match(/^TAP Version ([0-9]+)\n$/i) | ||
if (version) { | ||
if (this.planStart === -1 && this.count === 0) | ||
this.emit('version', version[1]) | ||
else | ||
this.emit('extra', line) | ||
return | ||
} | ||
// if we got a plan at the end, or a 1..0 plan, then we can't | ||
// have any more results, yamlish, or child sets. | ||
if (this.postPlan) { | ||
this.emit('extra', line) | ||
return | ||
} | ||
// still belongs to the child. | ||
if (this.child && line.indexOf(this.child.indent) === 0) { | ||
line = line.substr(this.child.indent.length) | ||
this.child.write(line) | ||
return | ||
} | ||
var indent = line.match(/^[ \t]+/) | ||
if (indent) { | ||
indent = indent[0] | ||
// if we don't have a current res, then it can't be yamlish, | ||
// must be a child result set | ||
if (!this.current) { | ||
this.startChild(indent, line) | ||
return | ||
} | ||
}; | ||
Parser.prototype._onplan = function (plan, skip_reason) { | ||
var results = this.results; | ||
if (results.plan !== undefined) { | ||
this.emit('parseError', { | ||
message: 'unexpected additional plan' | ||
}); | ||
return; | ||
// if we are not currently processing yamlish, then it has to | ||
// be either the start of a child, or the start of yamlish. | ||
if (!this.yind) { | ||
// either this starts yamlish, or it is a child. | ||
if (line === indent + '---\n') | ||
this.yind = indent | ||
else | ||
this.startChild(indent, line) | ||
return | ||
} | ||
if (plan.start === 1 && plan.end === 0) { | ||
plan.skip_all = true; | ||
plan.skip_reason = skip_reason; // could be undefined | ||
} else if (skip_reason) { | ||
this.emit('parseError', { | ||
message: 'plan is not empty, but has a SKIP reason', | ||
skip_reason: skip_reason | ||
}); | ||
plan.skip_all = false; | ||
plan.skip_reason = skip_reason; | ||
// continue to use the plan | ||
// now we know it is yamlish | ||
// if it's not as indented, then it's broken. | ||
// The whole yamlish chunk is garbage. | ||
if (indent.indexOf(this.yind) !== 0) { | ||
// oops! was not actually yamlish, I guess. | ||
// treat as garbage | ||
this.emit('extra', this.yamlish + line) | ||
this.emitResult() | ||
return | ||
} | ||
results.plan = plan; | ||
this._checkAssertionStart(); | ||
}; | ||
Parser.prototype._online = function (line) { | ||
var m; | ||
if (m = re.version.exec(line)) { | ||
var ver = /^\d+(\.\d*)?$/.test(m[1]) ? Number(m[1]) : m[1]; | ||
this.emit('version', ver); | ||
// yamlish ends with "...\n" | ||
if (line === this.yind + '...\n') { | ||
this.processYamlish() | ||
return | ||
} | ||
else if (m = re.comment.exec(line)) { | ||
this.emit('comment', m[1]); | ||
} | ||
else if (m = re.ok.exec(line)) { | ||
var ok = !m[1]; | ||
var num = m[2] && Number(m[2]); | ||
var name = m[3]; | ||
var asrt = { | ||
ok: ok, | ||
number: num, | ||
name: name | ||
}; | ||
if (m = re.label_todo.exec(name)) { | ||
asrt.name = m[1]; | ||
asrt.todo = m[2]; | ||
} | ||
this.emit('assert', asrt); | ||
} | ||
else if (m = re.plan.exec(line)) { | ||
this.emit('plan', { | ||
start: Number(m[1]), | ||
end: Number(m[2]) | ||
}, | ||
m[3]); // reason, if SKIP | ||
} | ||
else this.emit('extra', line) | ||
}; | ||
Parser.prototype._checkAssertionStart = function () { | ||
var results = this.results; | ||
if (this._planMismatch) return; | ||
if (!results.asserts[0]) return; | ||
if (!results.plan) return; | ||
if (results.asserts[0].number === results.plan.start) return; | ||
this._planMismatch = true; | ||
this.emit('parseError', { | ||
message: 'plan range mismatch' | ||
}); | ||
}; | ||
// ok! it is valid yamlish indentation, and not the ... | ||
// save it to parse later. | ||
this.yamlish += line | ||
return | ||
} | ||
Parser.prototype._finished = function () { | ||
var results = this.results; | ||
if (results.plan === undefined) { | ||
this.emit('parseError', { | ||
message: 'no plan found' | ||
}); | ||
// not indented. if we were doing yamlish, then it didn't go good | ||
if (this.yind) { | ||
this.emit('extra', this.yamlish) | ||
this.yamlish = '' | ||
this.yind = '' | ||
} | ||
this.emitResult() | ||
var plan = line.match(/^([0-9]+)\.\.([0-9]+)(?:\s+(?:#\s*(.*)))?\n$/) | ||
if (plan) { | ||
if (this.planStart !== -1) { | ||
// this is not valid tap, just garbage | ||
this.emit('extra', line) | ||
return | ||
} | ||
if (results.ok === undefined) results.ok = true; | ||
var skip_all = (results.plan && results.plan.skip_all); | ||
if (results.asserts.length === 0 && ! skip_all) { | ||
this.emit('parseError', { | ||
message: 'no assertions found' | ||
}); | ||
} else if (skip_all && results.asserts.length !== 0) { | ||
this.emit('parseError', { | ||
message: 'assertion found after skip_all plan' | ||
}); | ||
} | ||
var last = results.asserts.length | ||
&& results.asserts[results.asserts.length - 1].number | ||
; | ||
if (results.ok && last < results.plan.end) { | ||
this.emit('parseError', { | ||
message: 'not enough asserts' | ||
}); | ||
} | ||
else if (results.ok && last > results.plan.end) { | ||
this.emit('parseError', { | ||
message: 'too many asserts' | ||
}); | ||
} | ||
this.emit('results', results); | ||
}; | ||
var start = +(plan[1]) | ||
var end = +(plan[2]) | ||
var comment = plan[3] | ||
this.planStart = start | ||
this.planEnd = end | ||
var p = { start: start, end: end } | ||
if (comment) | ||
this.planComment = p.comment = comment | ||
this.emit('plan', p) | ||
// 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 | ||
return | ||
} | ||
var res = createResult(line, this.count) | ||
if (!res) { | ||
this.emit('extra', line) | ||
return | ||
} | ||
if (res.id) { | ||
if (!this.first || res.id < this.first) | ||
this.first = res.id | ||
else if (!this.last || res.id > this.last) | ||
this.last = res.id | ||
} | ||
// hold onto it, because we might get yamlish diagnostics | ||
this.current = res | ||
} |
{ | ||
"name": "tap-parser", | ||
"version": "0.7.0", | ||
"version": "1.0.0", | ||
"description": "parse the test anything protocol", | ||
@@ -10,9 +10,10 @@ "main": "index.js", | ||
"dependencies": { | ||
"events-to-array": "^1.0.1", | ||
"inherits": "~2.0.1", | ||
"minimist": "^0.2.0", | ||
"readable-stream": "~1.1.11" | ||
"js-yaml": "^3.2.7" | ||
}, | ||
"devDependencies": { | ||
"tape": "~2.3.2", | ||
"tap": "~0.4.6" | ||
"glob": "^5.0.2", | ||
"tap": "~0.4.6", | ||
"tape": "^3.5.0" | ||
}, | ||
@@ -19,0 +20,0 @@ "scripts": { |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
151735
68
2987
0
176
3
3
3
+ Addedevents-to-array@^1.0.1
+ Addedjs-yaml@^3.2.7
+ Addedargparse@1.0.10(transitive)
+ Addedesprima@4.0.1(transitive)
+ Addedevents-to-array@1.1.2(transitive)
+ Addedjs-yaml@3.14.1(transitive)
+ Addedsprintf-js@1.0.3(transitive)
- Removedminimist@^0.2.0
- Removedreadable-stream@~1.1.11
- Removedcore-util-is@1.0.3(transitive)
- Removedisarray@0.0.1(transitive)
- Removedminimist@0.2.4(transitive)
- Removedreadable-stream@1.1.14(transitive)
- Removedstring_decoder@0.10.31(transitive)