/
tap-consumer.js
243 lines (215 loc) · 6.77 KB
/
tap-consumer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
module.exports = TapConsumer
// pipe a stream into this that's emitting tap-formatted data,
// and it'll emit "data" events with test objects or comment strings
// and an "end" event with the final results.
var yamlish = require("yamlish")
, Results = require("./tap-results")
, inherits = require("inherits")
TapConsumer.decode = TapConsumer.parse = function (str) {
var tc = new TapConsumer
, list = []
tc.on("data", function (res) {
list.push(res)
})
tc.end(str)
tc.results.list = list
return tc.results
}
inherits(TapConsumer, require("stream").Stream)
function TapConsumer () {
if (!(this instanceof TapConsumer)) {
return new TapConsumer
}
TapConsumer.super.call(this)
this.results = new Results
this.readable = this.writable = true
this.on("data", function (res) {
if (typeof res === "object") this.results.add(res)
})
this._plan = null
this._buffer = ""
this._indent = []
this._current = null
this._actualCount = 0
this._passed = []
this._failed = []
//console.error("TapConsumer ctor done")
}
TapConsumer.prototype.bailedOut = false
TapConsumer.prototype.write = function (chunk) {
if (!this.writable) this.emit("error", new Error("not writable"))
if (this.bailedOut) return true
this._buffer = this._buffer + chunk
// split it up into lines.
var lines = this._buffer.split(/\r?\n/)
// ignore the last line, since it might be incomplete.
this._buffer = lines.pop()
for (var i = 0, l = lines.length; i < l; i ++) {
//console.error([i, lines[i]])
// see if it's indented.
var line = lines[i]
, spaces = (this._indent.length && !line.trim())
|| line.match(/^\s/)
// at this level, only interested in fully undented stuff.
if (spaces) {
var c = i
while (c < l && (!lines[c].trim() || lines[c].match(/^\s/))) {
this._indent.push(lines[c++])
}
//console.error(c-i, "indented", this._indent, this._current)
i = c - 1
continue
}
// some kind of line. summary, ok, notok, comment, or garbage.
// this also finishes parsing any of the indented lines from before
this._parseLine(line)
}
return true
}
TapConsumer.prototype.end = function () {
// finish up any hanging indented sections or final buffer
if (this._buffer.match(/^\s/)) this._indent.push(this.buffer)
else this._parseLine(this._buffer)
if (!this.bailedOut &&
this._plan !== null &&
this.results.testsTotal !== this._plan) {
while (this._actualCount < this._plan) {
this.emit("data", {ok: false, name:"MISSING TEST",
id:this._actualCount ++ })
}
}
this._parseLine("")
this._buffer = ""
this.writable = false
this.emit("end", null, this._actualCount, this._passed)
}
TapConsumer.prototype._parseLine = function (line) {
if (this.bailedOut) return
//console.error("_parseLine", [line])
// if there are any indented lines, and there is a
// current object already, then they belong to it.
// if there is not a current object, then they're garbage.
if (this._current && this._indent.length) {
this._parseIndented()
}
this._indent.length = 0
if (this._current) {
if (this._current.ok) this._passed.push(this._current.id)
else this._failed.push(this._current.id)
this.emit("data", this._current)
}
this._current = null
line = line.trim()
if (!line) return
// try to see what kind of line this is.
var bo
if (bo = line.match(/^bail out!\s*(.*)$/i)) {
this.bailedOut = true
// this.emit("error", new Error(line))
this.emit("bailout", bo[1])
return
}
if (line.match(/^#/)) { // just a comment
line = line.replace(/^#+/, "").trim()
// console.error("outputting comment", [line])
if (line) this.emit("data", line)
return
}
var plan = line.match(/^([0-9]+)\.\.([0-9]+)(?:\s+#(.*))?$/)
if (plan) {
var start = +(plan[1])
, end = +(plan[2])
, comment = plan[3]
// TODO: maybe do something else with this?
// it might be something like: "1..0 #Skip because of reasons"
this._plan = end
this.emit("plan", end, comment)
// plan must come before or after all tests.
if (this._actualCount !== 0) {
this._sawPlan = true
}
return
}
if (line.match(/^(not )?ok(?:\s+([0-9]+))?/)) {
this._parseResultLine(line)
return
}
// garbage. emit as a comment.
//console.error("emitting", [line.trim()])
if (line.trim()) this.emit("data", line.trim())
}
TapConsumer.prototype._parseDirective = function (line) {
line = line.trim()
if (line.match(/^TODO\b/i)) {
return { todo:true, explanation: line.replace(/^TODO\s*/i, "") }
} else if (line.match(/^SKIP\b/i)) {
return { skip:true, explanation: line.replace(/^SKIP\s*/i, "") }
}
}
TapConsumer.prototype._parseResultLine = function (line) {
this._actualCount ++
if (this._sawPlan) {
this.emit("data", {ok: false, name:"plan in the middle of tests"
,id:this._actualCount ++})
}
var parsed = line.match(/^(not )?ok(?: ([0-9]+))?(?:(?: - )?(.*))?$/)
, ok = !parsed[1]
, id = +(parsed[2] || this._actualCount)
, rest = parsed[3] || ""
, name
, res = { id:id, ok:ok }
// split on un-escaped # characters
//console.log("# "+JSON.stringify([name, rest]))
rest = rest.replace(/([^\\])((?:\\\\)*)#/g, "$1\n$2").split("\n")
name = rest.shift()
rest = rest.filter(function (r) { return r.trim() }).join("#")
//console.log("# "+JSON.stringify([name, rest]))
// now, let's see if there's a directive in there.
var dir = this._parseDirective(rest.trim())
if (!dir) name += rest ? "#" + rest : ""
else {
res.ok = true
if (dir.skip) res.skip = true
else if (dir.todo) res.todo = true
if (dir.explanation) res.explanation = dir.explanation
}
res.name = name
//console.error(line, [ok, id, name])
this._current = res
}
TapConsumer.prototype._parseIndented = function () {
// pull yamlish block out
var ind = this._indent
, ys
, ye
, yind
, diag
//console.error(ind, this._indent)
for (var i = 0, l = ind.length; i < l; i ++) {
var line = ind[i]
, lt = line.trim()
if (!ys) {
ys = line.match(/^(\s*)---(.*)$/)
if (ys) {
yind = ys[1]
diag = [ys[2]]
//console.error([line,ys, diag])
continue
} else if (lt) this.emit("data", lt)
} else if (ys && !ye) {
if (line === yind + "...") ye = true
else {
diag.push(line.substr(yind.length))
}
} else if (ys && ye && lt) this.emit("data", lt)
}
if (diag) {
//console.error('about to parse', diag)
diag = yamlish.decode(diag.join("\n"))
//console.error('parsed', diag)
Object.keys(diag).forEach(function (k) {
//console.error(this._current, k)
if (!this._current.hasOwnProperty(k)) this._current[k] = diag[k]
}, this)
}
}