UNPKG

8.56 kBJavaScriptView Raw
1'use strict'
2
3const MiniPass = require('minipass')
4
5const extraFromError = require('./extra-from-error.js')
6const assert = require('assert')
7
8const Domain = require('async-hook-domain')
9const {AsyncResource} = require('async_hooks')
10const util = require('util')
11
12class TapWrap extends AsyncResource {
13 constructor (test) {
14 super('tap.' + test.constructor.name)
15 this.test = test
16
17 // Polyfill for Node.js 8 and before
18 /* istanbul ignore next */
19 if (!this.runInAsyncScope)
20 this.runInAsyncScope = (fn, thisArg, ...args) => {
21 this.emitBefore()
22 try {
23 return Reflect.apply(fn, thisArg, args)
24 } finally {
25 this.emitAfter()
26 }
27 }
28 }
29}
30
31/* istanbul ignore next */
32const INSPECT = util.inspect.custom || 'inspect'
33
34const Parser = require('tap-parser')
35
36const ownOr = require('own-or')
37const ownOrEnv = require('own-or-env')
38const hrtime = require('browser-process-hrtime')
39
40class Base extends MiniPass {
41 constructor (options) {
42 options = options || {}
43 super(options)
44
45 this.started = false
46
47 // establish the wrapper resource to limit the domain to this one object
48 this.hook = new TapWrap(this)
49 this.hook.runInAsyncScope(() =>
50 this.hookDomain = new Domain((er, type) => {
51 if (!er || typeof er !== 'object')
52 er = { error: er }
53 er.tapCaught = type
54 this.threw(er)
55 }))
56
57 this.start = 0
58 this.hrtime = null
59 this.time = null
60 this.timer = null
61 this.readyToProcess = false
62 this.options = options
63 this.grep = ownOr(options, 'grep', [])
64 this.grepInvert = ownOr(options, 'grepInvert', false)
65 this.parent = ownOr(options, 'parent', null)
66 this.bail = ownOrEnv(options, 'bail', 'TAP_BAIL', true)
67 const name = (ownOr(options, 'name', '') || '').replace(/[\n\r\s\t]/g, ' ')
68 Object.defineProperty(this, 'name', {
69 value: name,
70 writable: false,
71 enumerable: true,
72 configurable: false,
73 })
74 this.indent = ownOr(options, 'indent', '')
75 this.silent = !!options.silent
76 this.buffered = !!options.buffered || !!options.silent
77 this.finished = false
78 this.strict = ownOrEnv(options, 'strict', 'TAP_STRICT', true)
79 this.omitVersion = !!options.omitVersion
80 this.preserveWhitespace = ownOr(options, 'preserveWhitespace', true)
81 this.jobs = +ownOrEnv(options, 'jobs', 'TAP_JOBS') || 0
82 this.runOnly = ownOrEnv(options, 'runOnly', 'TAP_ONLY', true)
83 this.setupParser(options)
84 this.finished = false
85 this.output = ''
86 this.results = null
87 this.bailedOut = false
88 this.childId = +ownOrEnv(options, 'childId', 'TAP_CHILD_ID')
89 || /* istanbul ignore next */ 0
90 const skip = ownOr(options, 'skip', false)
91 const todo = ownOr(options, 'todo', false)
92 if (skip || todo)
93 this.main = Base.prototype.main
94
95 this.counts = {
96 total: 0,
97 pass: 0,
98 fail: 0,
99 skip: 0,
100 todo: 0,
101 }
102
103 const ctx = ownOr(options, 'context', null)
104 delete options.context
105 this.context = typeof ctx === 'object' || ctx instanceof Object
106 ? Object.create(ctx) : ctx
107
108 this.lists = {
109 fail: [],
110 todo: [],
111 skip: [],
112 }
113
114 const doDebug = typeof options.debug === 'boolean' ? options.debug
115 : /\btap\b/i.test(process.env.NODE_DEBUG || '')
116
117 if (doDebug)
118 this.debug = debug(this.name)
119 }
120
121 passing () {
122 return this.parser.ok
123 }
124
125 setTimeout (n, quiet) {
126 if (!this.hrtime)
127 this.hrtime = hrtime()
128
129 if (!this.start)
130 this.start = Date.now()
131
132 if (!n) {
133 clearTimeout(this.timer)
134 this.timer = null
135 } else {
136 if (this.timer)
137 clearTimeout(this.timer)
138
139 this.timer = setTimeout(() => this.timeout(), n)
140 /* istanbul ignore else */
141 if (this.timer.unref) {
142 this.timer.duration = n
143 this.timer.unref()
144 }
145 }
146 }
147
148 threw (er, extra, proxy) {
149 this.hook.emitDestroy()
150 this.hookDomain.destroy()
151 if (!er || typeof er !== 'object')
152 er = { error: er }
153 if (this.name && !proxy)
154 er.test = this.name
155
156 const message = er.message
157
158 if (!extra)
159 extra = extraFromError(er, extra, this.options)
160
161 if (this.results) {
162 this.results.ok = false
163 if (this.parent)
164 this.parent.threw(er, extra, true)
165 else if (!er.stack)
166 console.error(er)
167 else {
168 if (message)
169 er.message = message
170 delete extra.stack
171 delete extra.at
172 console.error('%s: %s', er.name || 'Error', message)
173 console.error(er.stack.split(/\n/).slice(1).join('\n'))
174 console.error(extra)
175 }
176 } else
177 this.parser.ok = false
178
179 return extra
180 }
181
182 timeout (options) {
183 this.setTimeout(false)
184 const er = new Error('timeout!')
185 options = options || {}
186 options.expired = options.expired || this.name
187 this.emit('timeout', this.threw(new Error('timeout!'), options))
188 }
189
190 runMain (cb) {
191 this.started = true
192 this.hook.runInAsyncScope(this.main, this, cb)
193 }
194
195 main (cb) {
196 cb()
197 }
198
199 online (line) {
200 this.debug('LINE %j', line)
201 return this.write(this.indent + line)
202 }
203
204 write (c, e) {
205 assert.equal(typeof c, 'string')
206 assert.equal(c.substr(-1), '\n')
207
208 if (this.buffered) {
209 this.output += c
210 return true
211 }
212
213 return super.write(c, e)
214 }
215
216 onbail (reason) {
217 this.bailedOut = reason || true
218 this.emit('bailout', reason)
219 }
220
221 oncomplete (results) {
222 if (this.hrtime) {
223 this.hrtime = hrtime(this.hrtime)
224 this.time = Math.round(this.hrtime[0] * 1e6 + this.hrtime[1] / 1e3) / 1e3
225 }
226
227 this.debug('ONCOMPLETE %j %j', this.name, results)
228
229 if (this.results)
230 Object.keys(this.results)
231 .forEach(k => results[k] = this.results[k])
232
233 this.results = results
234 this.emit('complete', results)
235 const failures = results.failures
236 .filter(f => f.tapError)
237 .map(f => {
238 delete f.diag
239 delete f.ok
240 return f
241 })
242
243 if (failures.length)
244 this.options.failures = failures
245
246 this.onbeforeend()
247 // if we're piping, and buffered, then it means we need to hold off
248 // on emitting 'end' and calling ondone() until the pipes clear out.
249 if (this.pipes.length && this.buffer.length)
250 super.end()
251 else
252 this.emit('end')
253 }
254
255 onbeforeend () {}
256 ondone () {}
257
258 emit (ev, data) {
259 if (ev === 'end') {
260 const ret = super.emit(ev, data)
261 this.ondone()
262 this.hook.emitDestroy()
263 this.hookDomain.destroy()
264 return ret
265 } else
266 return super.emit(ev, data)
267 }
268
269 setupParser (options) {
270 this.parser = options.parser || new Parser({
271 bail: this.bail,
272 strict: this.strict,
273 omitVersion: this.omitVersion,
274 preserveWhitespace: this.preserveWhitespace,
275 name: this.name,
276 })
277 this.parser.on('line', l => this.online(l))
278 this.parser.once('bailout', reason => this.onbail(reason))
279 this.parser.on('complete', result => this.oncomplete(result))
280
281 this.parser.on('result', () => this.counts.total++)
282 this.parser.on('pass', res => this.counts.pass++)
283 this.parser.on('todo', res => {
284 this.counts.todo++
285 this.lists.todo.push(res)
286 })
287 this.parser.on('skip', res => {
288 this.counts.skip++
289 this.lists.skip.push(res)
290 })
291 this.parser.on('fail', res => {
292 this.counts.fail++
293 this.lists.fail.push(res)
294 })
295 }
296
297 [INSPECT] () {
298 return this.constructor.name + ' ' + util.inspect({
299 name: this.name,
300 jobs: this.jobs,
301 buffered: this.buffered,
302 occupied: this.occupied,
303 pool: this.pool,
304 queue: this.queue,
305 subtests: this.subtests,
306 output: this.output,
307 skip: ownOr(this.options, 'skip', false),
308 todo: ownOr(this.options, 'todo', false),
309 only: ownOr(this.options, 'only', false),
310 results: this.results,
311 options: [
312 'autoend',
313 'command',
314 'args',
315 'stdio',
316 'env',
317 'cwd',
318 'exitCode',
319 'signal',
320 'expired',
321 'timeout',
322 'at',
323 'skip',
324 'todo',
325 'only',
326 'runOnly'
327 ].filter(k => this.options[k] !== undefined)
328 .reduce((s, k) => (s[k] = this.options[k], s), {})
329 })
330 }
331
332 debug () {}
333
334}
335
336const debug = name => (...args) => {
337 const prefix = `TAP ${process.pid} ${name}: `
338 const msg = util.format(...args).trim()
339 console.error(prefix + msg.split('\n').join(`\n${prefix}`))
340}
341
342module.exports = Base