-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Expand file tree
/
Copy pathcypress.ts
More file actions
321 lines (260 loc) · 9.27 KB
/
cypress.ts
File metadata and controls
321 lines (260 loc) · 9.27 KB
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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
// we are not requiring everything up front
// to optimize how quickly electron boots while
// in dev or linux production. the reasoning is
// that we likely may need to spawn a new child process
// and its a huge waste of time (about 1.5secs) of
// synchronous requires the first go around just to
// essentially do it all again when we boot the correct
// mode.
import Debug from 'debug'
import { getPublicConfigKeys } from '@packages/config'
import argsUtils from './util/args'
import { telemetry } from '@packages/telemetry'
import { getCtx, hasCtx } from '@packages/data-context'
import { warning as errorsWarning } from './errors'
import { getCwd } from './cwd'
import type { CypressError } from '@packages/errors'
import { toNumber } from 'lodash'
const debug = Debug('cypress:server:cypress')
type Mode = 'exit' | 'info' | 'interactive' | 'pkg' | 'record' | 'results' | 'run' | 'smokeTest' | 'version' | 'returnPkg' | 'exitWithCode'
/**
* Waits for a writable stream to drain its buffer.
* This is necessary because when stdout/stderr is piped (e.g., in CI environments),
* writes are buffered and process.exit() would terminate before the buffer is flushed.
*/
const waitForStreamDrain = (stream: NodeJS.WriteStream): Promise<void> => {
return new Promise((resolve) => {
if (!stream.isTTY && stream.writableLength > 0) {
debug('waiting for stream to drain, writableLength: %d', stream.writableLength)
stream.once('drain', resolve)
// Safety timeout to prevent hanging indefinitely
setTimeout(resolve, 5000)
} else {
setImmediate(resolve)
}
})
}
const exit = async (code = 0) => {
// TODO: we shouldn't have to do this
// but cannot figure out how null is
// being passed into exit
debug('about to exit with code', code)
if (hasCtx()) {
await getCtx().lifecycleManager.mainProcessWillDisconnect().catch((err: any) => {
debug('mainProcessWillDisconnect errored with: ', err)
})
}
const span = telemetry.getSpan('cypress')
span?.setAttribute('exitCode', code)
span?.end()
await telemetry.shutdown().catch((err: any) => {
debug('telemetry shutdown errored with: ', err)
})
// Wait for stdout/stderr to drain before exiting to prevent truncated output in CI
debug('waiting for stdout/stderr to drain before exit')
await Promise.all([
waitForStreamDrain(process.stdout),
waitForStreamDrain(process.stderr),
])
debug('process.exit', code)
return process.exit(code)
}
const showWarningForInvalidConfig = (options: any) => {
const publicConfigKeys = getPublicConfigKeys()
const invalidConfigOptions = require('lodash').keys(options.config).reduce((invalid, option) => {
if (!publicConfigKeys.find((configKey) => configKey === option)) {
invalid.push(option)
}
return invalid
}, [])
if (invalidConfigOptions.length && options.invokedFromCli) {
return errorsWarning('INVALID_CONFIG_OPTION', invalidConfigOptions)
}
return undefined
}
const exit0 = () => {
return exit(0)
}
function isCypressError (err: unknown): err is CypressError {
return (err as CypressError).isCypressErr
}
async function exitErr (err: unknown, posixExitCodes?: boolean) {
// log errors to the console
// and potentially raygun
// and exit with 1
debug('exiting with err', err)
await require('./errors').logException(err)
if (isCypressError(err)) {
if (
posixExitCodes && (
err.type === 'CLOUD_CANNOT_PROCEED_IN_PARALLEL_NETWORK' ||
err.type === 'CLOUD_CANNOT_PROCEED_IN_SERIAL_NETWORK'
)) {
return exit(112)
}
}
return exit(1)
}
export = {
isCurrentlyRunningElectron () {
return require('./util/electron-app').isRunning()
},
runElectron (mode: Mode, options: any) {
// wrap all of this in a promise to force the
// promise interface - even if it doesn't matter
// in dev mode due to cp.spawn
return Promise.resolve().then(() => {
// if we have the electron property on versions
// that means we're already running in electron
// like in production and we shouldn't spawn a new
// process
if (this.isCurrentlyRunningElectron()) {
// if we weren't invoked from the CLI
// then display a warning to the user
if (!options.invokedFromCli) {
errorsWarning('INVOKED_BINARY_OUTSIDE_NPM_MODULE')
}
debug('running Electron currently')
return require('./modes')(mode, options)
}
return new Promise((resolve) => {
debug('starting Electron')
const cypressElectron = require('@packages/electron')
const fn = (code: number) => {
// juggle up the totalFailed since our outer
// promise is expecting this object structure
debug('electron finished with', code)
if (mode === 'smokeTest') {
return resolve(code)
}
return resolve({ totalFailed: code })
}
const args = require('./util/args').toArray(options)
debug('electron open arguments %o', args)
// const mainEntryFile = require.main.filename
const serverMain = getCwd()
return cypressElectron.open(serverMain, args, fn)
})
})
},
start (argv: any = []) {
debug('starting cypress with argv %o', argv)
// if the CLI passed "--" somewhere, we need to remove it
// for https://github.com/cypress-io/cypress/issues/5466
argv = argv.filter((val) => val !== '--')
let options
try {
options = argsUtils.toObject(argv)
showWarningForInvalidConfig(options)
} catch (argumentsError: any) {
debug('could not parse CLI arguments: %o', argv)
// note - this is promise-returned call
return exitErr(argumentsError, Boolean(options?.posixExitCodes))
}
debug('from argv %o got options %o', argv, options)
// @ts-expect-error TODO: Fix type that says attachRecordKey is not a function
telemetry.exporter()?.attachRecordKey(options.key)
if (options.headless) {
// --headless is same as --headed false
if (options.headed) {
throw new Error('Impossible options: both headless and headed are true')
}
options.headed = false
}
if (options.runProject && !options.headed) {
debug('scaling electron app in headless mode')
// scale the electron browser window
// to force retina screens to not
// upsample their images when offscreen
// rendering
require('./util/electron-app').scale()
}
// make sure we have the appData folder
return Promise.all([
require('./util/app_data').ensure(),
require('./util/electron-app').setRemoteDebuggingPort(),
])
.then(() => {
// else determine the mode by
// the passed in arguments / options
// and normalize this mode
let mode = options.mode || 'interactive'
if (options.version) {
mode = 'version'
} else if (options.smokeTest) {
mode = 'smokeTest'
} else if (options.returnPkg) {
mode = 'returnPkg'
} else if (!(options.exitWithCode == null)) {
mode = 'exitWithCode'
} else if (options.runProject) {
// go into headless mode when running
// until completion + exit
mode = 'run'
}
return this.startInMode(mode, options)
})
},
async startInMode (mode: Mode, options: any) {
debug('starting in mode %s with options %o', mode, options)
if (mode === 'interactive') {
return this.runElectron(mode, options)
}
try {
switch (mode) {
case 'version': {
const version = await require('./modes/pkg')(options).get('version')
// eslint-disable-next-line no-console
console.log(version)
break
}
case 'info': {
await require('./modes/info')(options)
break
}
case 'smokeTest': {
const pong = await this.runElectron(mode, options)
if (!this.isCurrentlyRunningElectron()) {
return exit(pong)
} else if (pong !== options.ping) {
return exit(1)
}
break
}
case 'returnPkg': {
const pkg = await require('./modes/pkg')(options)
// eslint-disable-next-line no-console
console.log(JSON.stringify(pkg))
break
}
case 'exitWithCode': {
return exit(toNumber(options.exitWithCode))
break
}
case 'run': {
const results = await this.runElectron(mode, options)
if (results.runs) {
const isCanceled = results.runs.filter((run) => run.skippedSpec).length
if (isCanceled) {
// eslint-disable-next-line no-console
console.log(require('chalk').magenta('\n Exiting with non-zero exit code because the run was canceled.'))
return exit(1)
}
}
debug('results.totalFailed, posix?', results.totalFailed, options.posixExitCodes)
if (options.posixExitCodes) {
return exit(results.totalFailed ? 1 : 0)
}
return exit(results.totalFailed ?? 0)
}
default: {
throw new Error(`Cannot start. Invalid mode: '${mode}'`)
}
}
} catch (err) {
return exitErr(err, options.posixExitCodes)
}
debug('end of startInMode, exit 0')
return exit(0)
},
}