1 | 'use strict'
|
2 |
|
3 | const MiniPass = require('minipass')
|
4 |
|
5 | const extraFromError = require('./extra-from-error.js')
|
6 | const assert = require('assert')
|
7 |
|
8 | const Domain = require('async-hook-domain')
|
9 | const {AsyncResource} = require('async_hooks')
|
10 | const util = require('util')
|
11 |
|
12 | class TapWrap extends AsyncResource {
|
13 | constructor (test) {
|
14 | super('tap.' + test.constructor.name)
|
15 | this.test = test
|
16 |
|
17 |
|
18 |
|
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 |
|
32 | const INSPECT = util.inspect.custom || 'inspect'
|
33 |
|
34 | const Parser = require('tap-parser')
|
35 |
|
36 | const ownOr = require('own-or')
|
37 | const ownOrEnv = require('own-or-env')
|
38 | const hrtime = require('browser-process-hrtime')
|
39 |
|
40 | class Base extends MiniPass {
|
41 | constructor (options) {
|
42 | options = options || {}
|
43 | super(options)
|
44 |
|
45 | this.started = false
|
46 |
|
47 |
|
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 | || 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 |
|
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 |
|
248 |
|
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 |
|
336 | const 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 |
|
342 | module.exports = Base
|