mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-26 00:50:41 -05:00
64f5bf0870
* WIP: catch child process killed with a signal * unit test getError * we don't need custom exit code * Update cli/lib/exec/spawn.js Co-Authored-By: Zach Bloomquist <github@chary.us> * Update cli/lib/errors.js Co-Authored-By: Zach Bloomquist <github@chary.us> * update snapshots with wording
519 lines
16 KiB
JavaScript
519 lines
16 KiB
JavaScript
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 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,
|
||
], {
|
||
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)
|
||
const expectedCliArgs = [
|
||
'--foo',
|
||
'--cwd',
|
||
cwd,
|
||
'--no-sandbox',
|
||
]
|
||
|
||
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,
|
||
], {
|
||
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(() => {
|
||
expect(cp.spawn.firstCall.args[2].env).to.deep.eq({
|
||
FORCE_COLOR: '1',
|
||
DEBUG_COLORS: '1',
|
||
MOCHA_COLORS: '1',
|
||
FORCE_STDERR_TTY: '1',
|
||
FORCE_STDIN_TTY: '1',
|
||
FORCE_STDOUT_TTY: '1',
|
||
})
|
||
})
|
||
})
|
||
|
||
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(() => {
|
||
expect(cp.spawn.firstCall.args[2].env).to.deep.eq({
|
||
FORCE_COLOR: '0',
|
||
DEBUG_COLORS: '0',
|
||
FORCE_STDERR_TTY: '0',
|
||
FORCE_STDIN_TTY: '0',
|
||
FORCE_STDOUT_TTY: '0',
|
||
})
|
||
})
|
||
})
|
||
|
||
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/)
|
||
})
|
||
})
|
||
})
|
||
})
|