UNPKG

4.47 kBJavaScriptView Raw
1'use strict'
2const Base = require('./base.js')
3
4const assert = require('assert')
5const util = require('util')
6const ownOr = require('own-or')
7const path = require('path')
8const cleanYamlObject = require('./clean-yaml-object.js')
9const cp = require('child_process')
10
11class Spawn extends Base {
12 constructor (options) {
13 // figure out the name before calling super()
14 options = options || {}
15 const cwd = ownOr(options, 'cwd', process.cwd())
16 const command = options.command
17 if (!command)
18 throw new TypeError('no command provided')
19 const args = ownOr(options, 'args', [])
20
21 options.name = options.name || Spawn.procName(cwd, command, args)
22
23 super(options)
24
25 this.command = options.command
26
27 this.args = options.args
28 // stdout must be a pipe
29 if (options.stdio) {
30 if (typeof options.stdio === 'string')
31 this.stdio = [ options.stdio, 'pipe', options.stdio ]
32 else
33 this.stdio = options.stdio.slice(0)
34 } else
35 this.stdio = [ 0, 'pipe', 2 ]
36
37 this.stdio[1] = 'pipe'
38 options.stdio = this.stdio
39
40 if (!options.env)
41 options.env = process.env
42 this.env = {
43 ...(options.env),
44 TAP_CHILD_ID: options.childId
45 || options.env.TAP_CHILD_ID
46 || /* istanbul ignore next */ 0,
47 TAP: '1',
48 TAP_BAIL: this.bail ? '1' : '0',
49 }
50 // prune off the extraneous bits so we're not logging the world
51 this.options.env = Object.keys(this.options.env).reduce((env, k) => {
52 if (process.env[k] !== this.options.env[k])
53 env[k] = this.options.env[k]
54 return env
55 }, {})
56
57 this.cwd = cwd
58 options.cwd = this.cwd
59
60 this.processDB = ownOr(options, 'processDB', null) || {
61 spawn: (name, ...rest) => cp.spawn(...rest)
62 }
63 delete options.processDB
64
65 this.proc = null
66 }
67
68 endAll () {
69 if (this.proc)
70 this.proc.kill('SIGKILL')
71 this.parser.abort('test unfinished')
72 this.callCb()
73 }
74
75 main (cb) {
76 this.cb = cb
77 this.setTimeout(this.options.timeout)
78 this.parser.on('comment', c => {
79 const tomatch = c.match(/# timeout=([0-9]+)\n$/)
80 if (tomatch)
81 this.setTimeout(+tomatch[1])
82 })
83 const options = {
84 ...(this.options),
85 cwd: this.cwd,
86 env: this.env,
87 stdio: this.stdio,
88 }
89 try {
90 this.emit('preprocess', options)
91 const proc = this.proc =
92 this.processDB.spawn(this.name, this.command, this.args, options)
93
94 proc.stdout.pipe(this.parser)
95 proc.on('close', (code, signal) => this.onprocclose(code, signal))
96 proc.on('error', er => this.threw(er))
97 this.emit('process', proc)
98 if (this.parent)
99 this.parent.emit('spawn', this)
100 } catch (er) {
101 er.tapCaught = 'spawn'
102 this.threw(er)
103 }
104 }
105
106 callCb () {
107 if (this.cb)
108 this.cb()
109 this.cb = null
110 }
111
112 threw (er, extra, proxy) {
113 extra = Base.prototype.threw.call(this, er, extra, proxy)
114 extra = cleanYamlObject(extra)
115 // unhook entirely
116 this.parser.abort(er.message, extra)
117 if (this.proc) {
118 this.proc.stdout.removeAllListeners('data')
119 this.proc.stdout.removeAllListeners('end')
120 this.proc.removeAllListeners('close')
121 this.proc.kill('SIGKILL')
122 }
123 this.callCb()
124 }
125
126 onprocclose (code, signal) {
127 this.debug('SPAWN close %j %s', code, signal)
128 this.options.exitCode = code
129 if (signal)
130 this.options.signal = signal
131
132 // spawn closing with no tests is treated as a skip.
133 if (this.results && this.results.plan && this.results.plan.skipAll && !code && !signal)
134 this.options.skip = this.results.plan.skipReason || true
135
136 if (code || signal) {
137 this.results.ok = false
138 this.parser.ok = false
139 }
140 return this.callCb()
141 }
142
143 timeout (extra) {
144 if (this.proc) {
145 this.proc.kill('SIGTERM')
146 const t = setTimeout(() => {
147 if (!this.options.signal && this.options.exitCode === undefined) {
148 Base.prototype.timeout.call(this, extra)
149 this.proc.kill('SIGKILL')
150 }
151 }, 1000)
152 /* istanbul ignore else */
153 if (t.unref)
154 t.unref()
155 }
156 }
157}
158
159Spawn.procName = (cwd, command, args) => (
160 (command === process.execPath)
161 ? path.basename(process.execPath) + ' ' + args.map(a =>
162 a.indexOf(cwd) === 0 ?
163 './' + a.substr(cwd.length + 1).replace(/\\/g, '/')
164 : a).join(' ').trim()
165 : command + ' ' + args.join(' ')
166).replace(/\\/g, '/')
167
168module.exports = Spawn