UNPKG

tap-parser-yaml

Version:

parse the test anything protocol (with yaml diagnostic block parsing)

241 lines (216 loc) 6.82 kB
var Writable = require('readable-stream').Writable; var inherits = require('inherits'); var yaml = require('js-yaml'); 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+(.*)$/, diag_open: /^\s+---$/, diag_close: /^\s+\.\.\.$/ }; module.exports = Parser; inherits(Parser, Writable); 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: [], diags: [], 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('diag', this._ondiag); this.on('plan', this._onplan); this.on('parseError', function (err) { this.results.ok = false; err.line = this._lineNum; this.results.errors.push(err); }); } 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 ++; } this._line = parts[parts.length - 1]; next(); }; 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 (!res.number) { if (prev) res.number = prev.number + 1; else res.number = 1; } if (prev && prev.number + 1 !== res.number) { this.emit('parseError', { message: 'assert out of order' }); } }; Parser.prototype._ondiag = function (diag, text) { var results = this.results; results.diags.push(diag); var prevAssert = results.asserts[results.asserts.length - 1]; if (prevAssert) { prevAssert.diag = diag; } else { this.emit('parseError', { message: 'no assert to pair with diagnostic' }); } } Parser.prototype._onplan = function (plan, skip_reason) { var results = this.results; if (results.plan !== undefined) { this.emit('parseError', { message: 'unexpected additional plan' }); 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 } results.plan = plan; this._checkAssertionStart(); }; Parser.prototype._online = function (line) { var m; if (this._inDiag){ m = re.diag_close.exec(line); if (!m) { this._diagLines.push(line); } else { this._inDiag = false; try { var diagText = this._diagLines .join('\n') // NOTE: tools like substack/tape use object-inspect // to output actual/expected text, and the only // incompatibility between that and yaml.safeLoad I found // was that object-inspect inserts a \' for an actual ' // character inside a string, instead of '' which is what // yaml expects. .replace(/\\'/g, "''"); this.emit('diag', yaml.safeLoad(diagText), diagText); } catch (e) { this.emit('parseError', { message: 'failed to parse yaml in diagnostic block', reason: e }); } } } else if (m = re.version.exec(line)) { var ver = /^\d+(\.\d*)?$/.test(m[1]) ? Number(m[1]) : m[1]; this.emit('version', ver); } 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 if (m = re.diag_open.exec(line)) { this._inDiag = true; this._diagLines = []; } 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' }); }; Parser.prototype._finished = function () { var results = this.results; if (results.plan === undefined) { this.emit('parseError', { message: 'no plan found' }); } 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); };