Files
cypress/cli/test/lib/exec/spawn_spec.js
T
Matt Henkes 82429c0027 feat: Defaulting nodeVersion to system (#18732)
* Defaulting nodeVersion to system

* try to fix system test

* Rename arg parameters, fix system test in a much better way.

* remove invalid comment

* Add deprecation warning for the nodeVersion config.

* Remove default value to avoid warning regardless of the presence of `nodeVersion`

* More tests fixes 😅

* Updates to deprecation message

* update node version in deprecation notice.

* flex config file name that we tell consumers to update

* simplify validateNoBreakingConfig options
2021-11-04 11:45:25 -07:00

549 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
require('../../spec_helper')
const _ = require('lodash')
const cp = require('child_process')
const os = require('os')
const tty = require('tty')
const path = require('path')
const EE = require('events')
const mockedEnv = require('mocked-env')
const debug = require('debug')('test')
const state = require(`${lib}/tasks/state`)
const xvfb = require(`${lib}/exec/xvfb`)
const spawn = require(`${lib}/exec/spawn`)
const verify = require(`${lib}/tasks/verify`)
const util = require(`${lib}/util.js`)
const expect = require('chai').expect
const snapshot = require('../../support/snapshot')
const cwd = process.cwd()
const execPath = process.execPath
const nodeVersion = process.versions.node
const defaultBinaryDir = '/default/binary/dir'
describe('lib/exec/spawn', function () {
beforeEach(function () {
os.platform.returns('darwin')
sinon.stub(process, 'exit')
this.spawnedProcess = {
on: sinon.stub().returns(undefined),
unref: sinon.stub().returns(undefined),
stdin: {
on: sinon.stub().returns(undefined),
pipe: sinon.stub().returns(undefined),
},
stdout: {
on: sinon.stub().returns(undefined),
pipe: sinon.stub().returns(undefined),
},
stderr: {
pipe: sinon.stub().returns(undefined),
on: sinon.stub().returns(undefined),
},
}
// process.stdin is both an event emitter and a readable stream
this.processStdin = new EE()
this.processStdin.pipe = sinon.stub().returns(undefined)
sinon.stub(process, 'stdin').value(this.processStdin)
sinon.stub(cp, 'spawn').returns(this.spawnedProcess)
sinon.stub(xvfb, 'start').resolves()
sinon.stub(xvfb, 'stop').resolves()
sinon.stub(xvfb, 'isNeeded').returns(false)
sinon.stub(state, 'getBinaryDir').returns(defaultBinaryDir)
sinon.stub(state, 'getPathToExecutable').withArgs(defaultBinaryDir).returns('/path/to/cypress')
})
context('.isGarbageLineWarning', () => {
it('returns true', () => {
const str = `
[46454:0702/140217.292422:ERROR:gles2_cmd_decoder.cc(4439)] [.RenderWorker-0x7f8bc5815a00.GpuRasterization]GL ERROR :GL_INVALID_FRAMEBUFFER_OPERATION : glDrawElements: framebuffer incomplete
[46454:0702/140217.292466:ERROR:gles2_cmd_decoder.cc(17788)] [.RenderWorker-0x7f8bc5815a00.GpuRasterization]GL ERROR :GL_INVALID_OPERATION : glCreateAndConsumeTextureCHROMIUM: invalid mailbox name
[46454:0702/140217.292526:ERROR:gles2_cmd_decoder.cc(4439)] [.RenderWorker-0x7f8bc5815a00.GpuRasterization]GL ERROR :GL_INVALID_FRAMEBUFFER_OPERATION : glClear: framebuffer incomplete
[46454:0702/140217.292555:ERROR:gles2_cmd_decoder.cc(4439)] [.RenderWorker-0x7f8bc5815a00.GpuRasterization]GL ERROR :GL_INVALID_FRAMEBUFFER_OPERATION : glDrawElements: framebuffer incomplete
[46454:0702/140217.292584:ERROR:gles2_cmd_decoder.cc(4439)] [.RenderWorker-0x7f8bc5815a00.GpuRasterization]GL ERROR :GL_INVALID_FRAMEBUFFER_OPERATION : glClear: framebuffer incomplete
[46454:0702/140217.292612:ERROR:gles2_cmd_decoder.cc(4439)] [.RenderWorker-0x7f8bc5815a00.GpuRasterization]GL ERROR :GL_INVALID_FRAMEBUFFER_OPERATION : glDrawElements: framebuffer incomplete'
`
const lines = _
.chain(str)
.split('\n')
.invokeMap('trim')
.compact()
.value()
_.each(lines, (line) => {
expect(spawn.isGarbageLineWarning(line), `expected line to be garbage: ${line}`).to.be.true
})
})
})
context('.start', function () {
// ️⚠️ NOTE ⚠️
// when asserting the calls made to spawn the child Cypress process
// we have to be _very_ careful. Spawn uses process.env object, if an assertion
// fails, it will print the entire process.env object to the logs, which
// might contain sensitive environment variables. Think about what the
// failed assertion might print to the public CI logs and limit
// the environment variables when running tests on CI.
it('passes args + options to spawn', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(verify, 'needsSandbox').returns(false)
return spawn.start('--foo', { foo: 'bar' })
.then(() => {
expect(cp.spawn).to.be.calledWithMatch('/path/to/cypress', [
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
], {
detached: false,
stdio: ['inherit', 'inherit', 'pipe'],
})
})
})
it('uses --no-sandbox when needed', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(verify, 'needsSandbox').returns(true)
return spawn.start('--foo', { foo: 'bar' })
.then(() => {
// skip the options argument: we do not need anything about it
// and also less risk that a failed assertion would dump the
// entire ENV object with possible sensitive variables
const args = cp.spawn.firstCall.args.slice(0, 2)
// it is important for "--no-sandbox" to appear before "--" separator
const expectedCliArgs = [
'--no-sandbox',
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
]
expect(args).to.deep.equal(['/path/to/cypress', expectedCliArgs])
})
})
it('uses npm command when running in dev mode', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(verify, 'needsSandbox').returns(false)
const p = path.resolve('..', 'scripts', 'start.js')
return spawn.start('--foo', { dev: true, foo: 'bar' })
.then(() => {
expect(cp.spawn).to.be.calledWithMatch('node', [
p,
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
], {
detached: false,
stdio: ['inherit', 'inherit', 'pipe'],
})
})
})
it('does not pass --no-sandbox when running in dev mode', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(verify, 'needsSandbox').returns(true)
const p = path.resolve('..', 'scripts', 'start.js')
return spawn.start('--foo', { dev: true, foo: 'bar' })
.then(() => {
expect(cp.spawn).to.be.calledWithMatch('node', [
p,
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
], {
detached: false,
stdio: ['inherit', 'inherit', 'pipe'],
})
})
})
it('starts xvfb when needed', function () {
xvfb.isNeeded.returns(true)
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start('--foo')
.then(() => {
expect(xvfb.start).to.be.calledOnce
})
})
context('closes', function () {
['close', 'exit'].forEach((event) => {
it(`if '${event}' event fired`, function () {
this.spawnedProcess.on.withArgs(event).yieldsAsync(0)
return spawn.start('--foo')
})
})
it('if exit event fired and close event fired', function () {
this.spawnedProcess.on.withArgs('exit').yieldsAsync(0)
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start('--foo')
})
})
context('detects kill signal', function () {
it('exits with error on SIGKILL', function () {
this.spawnedProcess.on.withArgs('exit').yieldsAsync(null, 'SIGKILL')
return spawn.start('--foo')
.then(() => {
throw new Error('should have hit error handler but did not')
}, (e) => {
debug('error message', e.message)
snapshot(e.message)
})
})
})
it('does not start xvfb when its not needed', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start('--foo')
.then(() => {
expect(xvfb.start).not.to.be.called
})
})
it('stops xvfb when spawn closes', function () {
xvfb.isNeeded.returns(true)
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
this.spawnedProcess.on.withArgs('close').yields()
return spawn.start('--foo')
.then(() => {
expect(xvfb.stop).to.be.calledOnce
})
})
it('resolves with spawned close code in the message', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(10)
return spawn.start('--foo')
.then((code) => {
expect(code).to.equal(10)
})
})
describe('Linux display', () => {
let restore
beforeEach(() => {
restore = mockedEnv({
DISPLAY: 'test-display',
})
})
afterEach(() => {
restore()
})
it('retries with xvfb if fails with display exit code', function () {
this.spawnedProcess.on.withArgs('close').onFirstCall().yieldsAsync(1)
this.spawnedProcess.on.withArgs('close').onSecondCall().yieldsAsync(0)
const buf1 = '[some noise here] Gtk: cannot open display: 987'
this.spawnedProcess.stderr.on
.withArgs('data')
.yields(buf1)
os.platform.returns('linux')
return spawn.start('--foo')
.then((code) => {
expect(xvfb.start).to.have.been.calledOnce
expect(xvfb.stop).to.have.been.calledOnce
expect(cp.spawn).to.have.been.calledTwice
// second code should be 0 after successfully running with Xvfb
expect(code).to.equal(0)
})
})
})
it('rejects with error from spawn', function () {
const msg = 'the error message'
this.spawnedProcess.on.withArgs('error').yieldsAsync(new Error(msg))
return spawn.start('--foo')
.then(() => {
throw new Error('should have hit error handler but did not')
}, (e) => {
debug('error message', e.message)
expect(e.message).to.include(msg)
})
})
it('unrefs if options.detached is true', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start(null, { detached: true })
.then(() => {
expect(this.spawnedProcess.unref).to.be.calledOnce
})
})
it('does not unref by default', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start()
.then(() => {
expect(this.spawnedProcess.unref).not.to.be.called
})
})
it('sets process.env to options.env', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
process.env.FOO = 'bar'
return spawn.start()
.then(() => {
expect(cp.spawn.firstCall.args[2].env.FOO).to.eq('bar')
})
})
it('forces colors and streams when supported', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(util, 'supportsColor').returns(true)
sinon.stub(tty, 'isatty').returns(true)
return spawn.start([], { env: {} })
.then(() => {
snapshot(cp.spawn.firstCall.args[2].env)
})
})
it('sets windowsHide:false property in windows', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
os.platform.returns('win32')
return spawn.start([], { env: {} })
.then(() => {
expect(cp.spawn.firstCall.args[2].windowsHide).to.be.false
})
})
it('does not set windowsHide property when in darwin', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start([], { env: {} })
.then(() => {
expect(cp.spawn.firstCall.args[2].windowsHide).to.be.undefined
})
})
it('does not force colors and streams when not supported', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(util, 'supportsColor').returns(false)
sinon.stub(tty, 'isatty').returns(false)
return spawn.start([], { env: {} })
.then(() => {
snapshot(cp.spawn.firstCall.args[2].env)
})
})
it('pipes when on win32', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
os.platform.returns('win32')
xvfb.isNeeded.returns(false)
return spawn.start()
.then(() => {
expect(cp.spawn.firstCall.args[2].stdio).to.deep.eq('pipe')
// parent process STDIN was piped to child process STDIN
expect(this.processStdin.pipe, 'process.stdin').to.have.been.calledOnce
.and.to.have.been.calledWith(this.spawnedProcess.stdin)
})
})
it('inherits when on linux and xvfb isnt needed', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
os.platform.returns('linux')
xvfb.isNeeded.returns(false)
return spawn.start()
.then(() => {
expect(cp.spawn.firstCall.args[2].stdio).to.deep.eq('inherit')
})
})
it('uses [inherit, inherit, pipe] when linux and xvfb is needed', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
xvfb.isNeeded.returns(true)
os.platform.returns('linux')
return spawn.start()
.then(() => {
expect(cp.spawn.firstCall.args[2].stdio).to.deep.eq([
'inherit', 'inherit', 'pipe',
])
})
})
it('uses [inherit, inherit, pipe] on darwin', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
xvfb.isNeeded.returns(false)
os.platform.returns('darwin')
return spawn.start()
.then(() => {
expect(cp.spawn.firstCall.args[2].stdio).to.deep.eq([
'inherit', 'inherit', 'pipe',
])
})
})
it('writes everything on win32', function () {
const buf1 = Buffer.from('asdf')
this.spawnedProcess.stdin.pipe.withArgs(process.stdin)
this.spawnedProcess.stdout.pipe.withArgs(process.stdout)
this.spawnedProcess.stderr.on
.withArgs('data')
.yields(buf1)
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(process.stderr, 'write').withArgs(buf1)
os.platform.returns('win32')
return spawn.start()
})
it('does not write to process.stderr when from xlib or libudev', function () {
const buf1 = Buffer.from('Xlib: something foo')
const buf2 = Buffer.from('libudev something bar')
const buf3 = Buffer.from('asdf')
this.spawnedProcess.stderr.on
.withArgs('data')
.onFirstCall()
.yields(buf1)
.onSecondCall()
.yields(buf2)
.onThirdCall()
.yields(buf3)
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(process.stderr, 'write').withArgs(buf3)
os.platform.returns('linux')
xvfb.isNeeded.returns(true)
return spawn.start()
.then(() => {
expect(process.stderr.write).not.to.be.calledWith(buf1)
expect(process.stderr.write).not.to.be.calledWith(buf2)
})
})
it('does not write to process.stderr when from high sierra warnings', function () {
const buf1 = Buffer.from('2018-05-19 15:30:30.287 Cypress[7850:32145] *** WARNING: Textured Window')
const buf2 = Buffer.from('asdf')
this.spawnedProcess.stderr.on
.withArgs('data')
.onFirstCall()
.yields(buf1)
.onSecondCall(buf2)
.yields(buf2)
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(process.stderr, 'write').withArgs(buf2)
os.platform.returns('darwin')
return spawn.start()
.then(() => {
expect(process.stderr.write).not.to.be.calledWith(buf1)
})
})
// https://github.com/cypress-io/cypress/issues/1841
// https://github.com/cypress-io/cypress/issues/5241
;['EPIPE', 'ENOTCONN'].forEach((errCode) => {
it(`catches process.stdin errors and returns when code=${errCode}`, function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start()
.then(() => {
let called = false
const fn = () => {
called = true
const err = new Error()
err.code = errCode
return process.stdin.emit('error', err)
}
expect(fn).not.to.throw()
expect(called).to.be.true
})
})
})
it('throws process.stdin errors code!=EPIPE', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start()
.then(() => {
const fn = () => {
const err = new Error('wattttt')
err.code = 'FAILWHALE'
return process.stdin.emit('error', err)
}
expect(fn).to.throw(/wattttt/)
})
})
})
})