1 module.exports = TapConsumer
3 // pipe a stream into this that's emitting tap-formatted data,
4 // and it'll emit "data" events with test objects or comment strings
5 // and an "end" event with the final results.
7 var yamlish = require("yamlish")
8 , Results = require("./tap-results")
9 , inherits = require("inherits")
11 TapConsumer.decode = TapConsumer.parse = function (str) {
12 var tc = new TapConsumer
14 tc.on("data", function (res) {
18 tc.results.list = list
22 inherits(TapConsumer, require("stream").Stream)
23 function TapConsumer () {
24 if (!(this instanceof TapConsumer)) {
25 return new TapConsumer
28 TapConsumer.super.call(this)
29 this.results = new Results
30 this.readable = this.writable = true
32 this.on("data", function (res) {
33 if (typeof res === "object") this.results.add(res)
43 //console.error("TapConsumer ctor done")
46 TapConsumer.prototype.bailedOut = false
48 TapConsumer.prototype.write = function (chunk) {
49 if (!this.writable) this.emit("error", new Error("not writable"))
50 if (this.bailedOut) return true
52 this._buffer = this._buffer + chunk
53 // split it up into lines.
54 var lines = this._buffer.split(/\r?\n/)
55 // ignore the last line, since it might be incomplete.
56 this._buffer = lines.pop()
58 for (var i = 0, l = lines.length; i < l; i ++) {
59 //console.error([i, lines[i]])
60 // see if it's indented.
62 , spaces = (this._indent.length && !line.trim())
64 // at this level, only interested in fully undented stuff.
67 while (c < l && (!lines[c].trim() || lines[c].match(/^\s/))) {
68 this._indent.push(lines[c++])
70 //console.error(c-i, "indented", this._indent, this._current)
74 // some kind of line. summary, ok, notok, comment, or garbage.
75 // this also finishes parsing any of the indented lines from before
81 TapConsumer.prototype.end = function () {
82 // finish up any hanging indented sections or final buffer
83 if (this._buffer.match(/^\s/)) this._indent.push(this.buffer)
84 else this._parseLine(this._buffer)
86 if (!this.bailedOut &&
87 this._plan !== null &&
88 this.results.testsTotal !== this._plan) {
89 while (this._actualCount < this._plan) {
90 this.emit("data", {ok: false, name:"MISSING TEST",
91 id:this._actualCount ++ })
98 this.emit("end", null, this._actualCount, this._passed)
101 TapConsumer.prototype._parseLine = function (line) {
102 if (this.bailedOut) return
103 //console.error("_parseLine", [line])
104 // if there are any indented lines, and there is a
105 // current object already, then they belong to it.
106 // if there is not a current object, then they're garbage.
107 if (this._current && this._indent.length) {
108 this._parseIndented()
110 this._indent.length = 0
112 if (this._current.ok) this._passed.push(this._current.id)
113 else this._failed.push(this._current.id)
114 this.emit("data", this._current)
119 // try to see what kind of line this is.
122 if (bo = line.match(/^bail out!\s*(.*)$/i)) {
123 this.bailedOut = true
124 // this.emit("error", new Error(line))
125 this.emit("bailout", bo[1])
129 if (line.match(/^#/)) { // just a comment
130 line = line.replace(/^#+/, "").trim()
131 // console.error("outputting comment", [line])
132 if (line) this.emit("data", line)
136 var plan = line.match(/^([0-9]+)\.\.([0-9]+)(?:\s+#(.*))?$/)
138 var start = +(plan[1])
142 // TODO: maybe do something else with this?
143 // it might be something like: "1..0 #Skip because of reasons"
145 this.emit("plan", end, comment)
146 // plan must come before or after all tests.
147 if (this._actualCount !== 0) {
153 if (line.match(/^(not )?ok(?:\s+([0-9]+))?/)) {
154 this._parseResultLine(line)
158 // garbage. emit as a comment.
159 //console.error("emitting", [line.trim()])
160 if (line.trim()) this.emit("data", line.trim())
163 TapConsumer.prototype._parseDirective = function (line) {
165 if (line.match(/^TODO\b/i)) {
166 return { todo:true, explanation: line.replace(/^TODO\s*/i, "") }
167 } else if (line.match(/^SKIP\b/i)) {
168 return { skip:true, explanation: line.replace(/^SKIP\s*/i, "") }
172 TapConsumer.prototype._parseResultLine = function (line) {
175 this.emit("data", {ok: false, name:"plan in the middle of tests"
176 ,id:this._actualCount ++})
178 var parsed = line.match(/^(not )?ok(?: ([0-9]+))?(?:(?: - )?(.*))?$/)
180 , id = +(parsed[2] || this._actualCount)
181 , rest = parsed[3] || ""
183 , res = { id:id, ok:ok }
185 // split on un-escaped # characters
187 //console.log("# "+JSON.stringify([name, rest]))
188 rest = rest.replace(/([^\\])((?:\\\\)*)#/g, "$1\n$2").split("\n")
190 rest = rest.filter(function (r) { return r.trim() }).join("#")
191 //console.log("# "+JSON.stringify([name, rest]))
193 // now, let's see if there's a directive in there.
194 var dir = this._parseDirective(rest.trim())
195 if (!dir) name += rest ? "#" + rest : ""
198 if (dir.skip) res.skip = true
199 else if (dir.todo) res.todo = true
200 if (dir.explanation) res.explanation = dir.explanation
204 //console.error(line, [ok, id, name])
208 TapConsumer.prototype._parseIndented = function () {
209 // pull yamlish block out
210 var ind = this._indent
215 //console.error(ind, this._indent)
216 for (var i = 0, l = ind.length; i < l; i ++) {
220 ys = line.match(/^(\s*)---(.*)$/)
224 //console.error([line,ys, diag])
226 } else if (lt) this.emit("data", lt)
227 } else if (ys && !ye) {
228 if (line === yind + "...") ye = true
230 diag.push(line.substr(yind.length))
232 } else if (ys && ye && lt) this.emit("data", lt)
235 //console.error('about to parse', diag)
236 diag = yamlish.decode(diag.join("\n"))
237 //console.error('parsed', diag)
238 Object.keys(diag).forEach(function (k) {
239 //console.error(this._current, k)
240 if (!this._current.hasOwnProperty(k)) this._current[k] = diag[k]