UNPKG

6.12 kBJavaScriptView Raw
1'use strict'
2const Test = require('./test.js')
3const Stdin = require('./stdin.js')
4const Spawn = require('./spawn.js')
5const util = require('util')
6const objToYaml = require('./obj-to-yaml.js')
7const _didPipe = Symbol('_didPipe')
8const _unmagicPipe = Symbol('_unmagicPipe')
9const Domain = require('async-hook-domain')
10
11let processPatched = false
12const rootDomain = new Domain((er, type) => {
13 if (!processPatched)
14 throw er
15 else {
16 if (!er || typeof er !== 'object')
17 er = { error: er }
18 er.tapCaught = type
19 tap.threw(er)
20 }
21})
22
23// We test that file separately, and removing stdout
24// causes some weird behavior in the Node test runner.
25/* istanbul ignore next */
26if (!process.stdout) {
27 require('./stdio-polyfill')()
28}
29
30const monkeypatchEpipe = () => {
31 const emit = process.stdout.emit
32 process.stdout.emit = function (ev, er) {
33 if (ev !== 'error' || er.code !== 'EPIPE')
34 return emit.apply(process.stdout, arguments)
35 }
36}
37
38const monkeypatchExit = () => {
39 const exit = process.exit
40 const reallyExit = process.reallyExit
41
42 // ensure that we always get run, even if a user does
43 // process.on('exit', process.exit)
44 process.reallyExit = code => reallyExit.call(process, onExitEvent(code))
45 process.exit = code => exit.call(process, onExitEvent(code))
46 process.on('beforeExit', onExitEvent)
47 process.on('exit', onExitEvent)
48}
49
50class TAP extends Test {
51 constructor (options) {
52 super(options)
53 this.runOnly = process.env.TAP_ONLY === '1'
54 this.childId = +process.env.TAP_CHILD_ID
55 || /* istanbul ignore next */ 0
56 this.start = Date.now()
57 this[_didPipe] = false
58 }
59
60 resume () {
61 this[_unmagicPipe]()
62 const ret = this.resume.apply(this, arguments)
63 this.process()
64 return ret
65 }
66
67 [_unmagicPipe] () {
68 this[_didPipe] = true
69 this.setTimeout(this.options.timeout)
70 this.pipe = Test.prototype.pipe
71 this.write = Test.prototype.write
72 this.resume = Test.prototype.resume
73 }
74
75 setTimeout (n, quiet) {
76 if (n && typeof n === 'number' && !quiet)
77 this.write(`# timeout=${n}\n`)
78 return super.setTimeout(n)
79 }
80
81 pipe () {
82 this[_unmagicPipe]()
83 const ret = this.pipe.apply(this, arguments)
84 this.process()
85 return ret
86 }
87
88 write (c, e) {
89 // this resets write and pipe to standard values
90 this.patchProcess()
91 this.pipe(process.stdout)
92 return super.write(c, e)
93 }
94
95 patchProcess () {
96 if (processPatched)
97 return
98 processPatched = true
99 monkeypatchEpipe()
100 monkeypatchExit()
101 }
102
103 onbeforeend () {
104 if (this[_didPipe] && this.time && !this.bailedOut)
105 this.emit('data', '# time=' + this.time + 'ms\n')
106 }
107
108 ondone () {
109 return this.emitSubTeardown(this)
110 }
111
112 // Root test runner doesn't have the 'teardown' event, because it
113 // isn't hooked into any parent Test as a harness.
114 teardown (fn) {
115 if (this.options.autoend !== false)
116 this.autoend(true)
117 return Test.prototype.teardown.apply(this, arguments)
118 }
119 tearDown (fn) {
120 return this.teardown(fn)
121 }
122}
123
124let didOnExitEvent = false
125const onExitEvent = code => {
126 if (didOnExitEvent)
127 return process.exitCode
128
129 didOnExitEvent = true
130
131 if (!tap.results)
132 tap.endAll()
133
134 if (tap.results && !tap.results.ok && code === 0)
135 process.exitCode = 1
136
137 return process.exitCode || code || 0
138}
139
140
141const opt = { name: 'TAP' }
142if (process.env.TAP_DEBUG === '1' ||
143 /\btap\b/.test(process.env.NODE_DEBUG || ''))
144 opt.debug = true
145
146if (process.env.TAP_GREP) {
147 opt.grep = process.env.TAP_GREP.split('\n').map(g => {
148 const p = g.match(/^\/(.*)\/([a-z]*)$/)
149 g = p ? p[1] : g
150 const flags = p ? p[2] : ''
151 return new RegExp(g, flags)
152 })
153}
154
155if (process.env.TAP_GREP_INVERT === '1')
156 opt.grepInvert = true
157
158if (process.env.TAP_ONLY === '1')
159 opt.only = true
160
161const tap = new TAP(opt)
162
163module.exports = tap.default = tap.t = tap
164tap.mocha = require('./mocha.js')
165tap.mochaGlobals = tap.mocha.global
166
167tap.Test = Test
168tap.Spawn = Spawn
169tap.Stdin = Stdin
170tap.synonyms = require('./synonyms.js')
171
172// SIGTERM means being forcibly killed, almost always by timeout
173const onExit = require('signal-exit')
174let didTimeoutKill = false
175onExit((code, signal) => {
176 if (signal !== 'SIGTERM' || !tap[_didPipe] || didTimeoutKill)
177 return
178
179 const handles = process._getActiveHandles().filter(h =>
180 h !== process.stdout &&
181 h !== process.stdin &&
182 h !== process.stderr
183 )
184 const requests = process._getActiveRequests()
185
186 // Ignore this because it's really hard to test cover in a way
187 // that isn't inconsistent and unpredictable.
188 /* istanbul ignore next */
189 const extra = {
190 at: null,
191 signal: signal
192 }
193 if (requests.length) {
194 extra.requests = requests.map(r => {
195 const ret = {}
196 ret.type = r.constructor.name
197
198 // most everything in node has a context these days
199 /* istanbul ignore else */
200 if (r.context)
201 ret.context = r.context
202
203 return ret
204 })
205 }
206
207 // Newer node versions don't have this as reliably.
208 /* istanbul ignore next */
209 if (handles.length) {
210 extra.handles = handles.map(h => {
211 const ret = {}
212 ret.type = h.constructor.name
213
214 // all of this is very internal-ish
215 /* istanbul ignore next */
216 if (h.msecs)
217 ret.msecs = h.msecs
218
219 /* istanbul ignore next */
220 if (h._events)
221 ret.events = Object.keys(h._events)
222
223 /* istanbul ignore next */
224 if (h._sockname)
225 ret.sockname = h._sockname
226
227 /* istanbul ignore next */
228 if (h._connectionKey)
229 ret.connectionKey = h._connectionKey
230
231 return ret
232 })
233 }
234
235 // this is impossible to cover, because it happens after nyc has
236 // already done its stuff.
237 /* istanbul ignore else */
238 if (!tap.results && tap.timeout)
239 tap.timeout(extra)
240 else {
241 console.error('possible timeout: SIGTERM received after tap end')
242 if (extra.handles || extra.requests) {
243 delete extra.signal
244 if (!extra.at) {
245 delete extra.at
246 }
247 console.error(objToYaml(extra))
248 }
249 didTimeoutKill = true
250 process.kill(process.pid, 'SIGTERM')
251 }
252})