Automatically retry verify and run commands on Linux if suspect DISPLAY problem (#4165)

* cli: debug explanation for XVFB

* linting

* add chai-as-promised to CLI dev

* show Linux specific error solution if cannot verify

* add todo

* chore: consolidate github issue url logic

* linting

* add npm script lint-changed to quickly eslint fix changes JS files

* retry verify with our XVFB

* update errors and tests

* update CLI tests

* add test for display error message

* fix unit test

* add successful test with retry

* finish verify retry test

* warn users if hit display problem on first verify

* try to detect display problem when running electron and retry with our xvfb

* add warning message to spawn when attempting xvfb re-run

* add test for display retry behavior on spawn

* more comments for clarity

* fix typo
This commit is contained in:
Gleb Bahmutov
2019-05-13 15:19:53 -04:00
committed by GitHub
parent 1b1c2f24bd
commit d25cfacc6f
19 changed files with 616 additions and 63 deletions

View File

@@ -266,6 +266,14 @@ DEBUG=cypress:launcher
We use [eslint](https://eslint.org/) to lint all JavaScript code and follow rules specified in
[eslint-plugin-cypress-dev](https://github.com/cypress-io/eslint-plugin-cypress-dev) plugin.
When you edit files, you can quickly fix all changed files before committing using
```bash
npm run lint-changed
```
When committing files, we run Git pre-commit hook to fix the staged JS files. See the `precommit-lint` script in [package.json](package.json). This might change JS files and you would need to commit the changes again.
### Tests
For most packages there are typically unit and some integration tests.

View File

@@ -1,3 +1,14 @@
exports['errors .errors.formErrorText calls solution if a function 1'] = `
description
a solution
----------
Platform: test platform (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['errors .errors.formErrorText returns fully formed text message 1'] = `
Your system is missing the dependency: XVFB
@@ -16,18 +27,54 @@ Cypress Version: 1.2.3
`
exports['errors individual has the following errors 1'] = [
"nonZeroExitCodeXvfb",
"missingXvfb",
"missingApp",
"notInstalledCI",
"missingDependency",
"versionMismatch",
"CYPRESS_RUN_BINARY",
"binaryNotExecutable",
"unexpected",
"failedDownload",
"failedUnzip",
"invalidCacheDirectory",
"invalidDisplayError",
"missingApp",
"missingDependency",
"missingXvfb",
"nonZeroExitCodeXvfb",
"notInstalledCI",
"removed",
"CYPRESS_RUN_BINARY",
"smokeTestFailure"
"smokeTestFailure",
"unexpected",
"versionMismatch"
]
exports['invalid display error'] = `
Cypress failed to start.
First, we have tried to start Cypress using your DISPLAY settings
but encountered the following problem:
----------
prev message
----------
Then we started our own XVFB and tried to start Cypress again, but
got the following error:
----------
current message
----------
This is usually caused by a missing library or dependency.
The error above should indicate which dependency is missing.
https://on.cypress.io/required-dependencies
If you are using Docker, we provide containers with all required dependencies installed.
----------
Platform: test platform (Foo-OsVersion)
Cypress Version: 1.2.3
`

View File

@@ -95,6 +95,9 @@ https://on.cypress.io/installing-cypress
exports['invalid cache directory 1'] = `
Error: Cypress cannot write to the cache directory due to file permissions
See discussion and possible solutions at
https://github.com/cypress-io/cypress/issues/1281
----------
Failed to access /invalid/cache/dir:

View File

@@ -159,7 +159,7 @@ Error: Cypress verification timed out.
This command failed with the following output:
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 --enable-logging
----------
@@ -181,7 +181,7 @@ Error: Cypress verification failed.
This command failed with the following output:
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 --enable-logging
----------
@@ -203,7 +203,7 @@ Error: Cypress verification failed.
This command failed with the following output:
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 --enable-logging
----------
@@ -384,3 +384,38 @@ Platform: darwin (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['tried to verify twice, on the first try got the DISPLAY error'] = `
Cypress failed to start.
First, we have tried to start Cypress using your DISPLAY settings
but encountered the following problem:
----------
[some noise here] Gtk: cannot open display: 987
----------
Then we started our own XVFB and tried to start Cypress again, but
got the following error:
----------
some other error
----------
This is usually caused by a missing library or dependency.
The error above should indicate which dependency is missing.
https://on.cypress.io/required-dependencies
If you are using Docker, we provide containers with all required dependencies installed.
----------
Platform: linux (Foo-OsVersion)
Cypress Version: 1.2.3
`

View File

@@ -2,14 +2,20 @@ const os = require('os')
const chalk = require('chalk')
const { stripIndent, stripIndents } = require('common-tags')
const { merge } = require('ramda')
const la = require('lazy-ass')
const is = require('check-more-types')
const util = require('./util')
const state = require('./tasks/state')
const issuesUrl = 'https://github.com/cypress-io/cypress/issues'
const docsUrl = 'https://on.cypress.io'
const requiredDependenciesUrl = `${docsUrl}/required-dependencies`
// TODO it would be nice if all error objects could be enforced via types
// to only have description + solution properties
const hr = '----------'
// common errors Cypress application can encounter
const failedDownload = {
description: 'The Cypress App could not be downloaded.',
@@ -21,7 +27,7 @@ const failedUnzip = {
solution: stripIndent`
Search for an existing issue or open a GitHub issue at
${chalk.blue(issuesUrl)}
${chalk.blue(util.issuesUrl)}
`,
}
@@ -102,6 +108,39 @@ const smokeTestFailure = (smokeTestCommand, timedOut) => {
}
}
const invalidDisplayError = {
description: 'Cypress failed to start.',
solution (msg, prevMessage) {
return stripIndent`
First, we have tried to start Cypress using your DISPLAY settings
but encountered the following problem:
${hr}
${prevMessage}
${hr}
Then we started our own XVFB and tried to start Cypress again, but
got the following error:
${hr}
${msg}
${hr}
This is usually caused by a missing library or dependency.
The error above should indicate which dependency is missing.
${chalk.blue(requiredDependenciesUrl)}
If you are using Docker, we provide containers with all required dependencies installed.
`
},
}
const missingDependency = {
description: 'Cypress failed to start.',
// this message is too Linux specific
@@ -118,7 +157,10 @@ const missingDependency = {
const invalidCacheDirectory = {
description: 'Cypress cannot write to the cache directory due to file permissions',
solution: '',
solution: stripIndent`
See discussion and possible solutions at
${chalk.blue(util.getGitHubIssueUrl(1281))}
`,
}
const versionMismatch = {
@@ -135,7 +177,7 @@ const unexpected = {
Check if there is a GitHub issue describing this crash:
${chalk.blue(issuesUrl)}
${chalk.blue(util.issuesUrl)}
Consider opening a new issue.
`,
@@ -191,10 +233,8 @@ function addPlatformInformation (info) {
/**
* Forms nice error message with error and platform information,
* and if possible a way to solve it. Resolves with a string.
*/
function formErrorText (info, msg) {
const hr = '----------'
*/
function formErrorText (info, msg, prevMessage) {
return addPlatformInformation(info)
.then((obj) => {
const formatted = []
@@ -205,20 +245,41 @@ function formErrorText (info, msg) {
)
}
add(`
${obj.description}
la(is.unemptyString(obj.description),
'expected error description to be text', obj.description)
${obj.solution}
// assuming that if there the solution is a function it will handle
// error message and (optional previous error message)
if (is.fn(obj.solution)) {
const text = obj.solution(msg, prevMessage)
`)
la(is.unemptyString(text), 'expected solution to be text', text)
if (msg) {
add(`
${hr}
${obj.description}
${msg}
${text}
`)
} else {
la(is.unemptyString(obj.solution),
'expected error solution to be text', obj.solution)
add(`
${obj.description}
${obj.solution}
`)
if (msg) {
add(`
${hr}
${msg}
`)
}
}
add(`
@@ -248,23 +309,24 @@ const raise = (text) => {
}
const throwFormErrorText = (info) => {
return (msg) => {
return formErrorText(info, msg)
return (msg, prevMessage) => {
return formErrorText(info, msg, prevMessage)
.then(raise)
}
}
module.exports = {
raise,
// formError,
formErrorText,
throwFormErrorText,
hr,
errors: {
nonZeroExitCodeXvfb,
missingXvfb,
missingApp,
notInstalledCI,
missingDependency,
invalidDisplayError,
versionMismatch,
binaryNotExecutable,
unexpected,

View File

@@ -4,10 +4,13 @@ const cp = require('child_process')
const path = require('path')
const Promise = require('bluebird')
const debug = require('debug')('cypress:cli')
const { stripIndent } = require('common-tags')
const util = require('../util')
const state = require('../tasks/state')
const xvfb = require('./xvfb')
const logger = require('../logger')
const logSymbols = require('log-symbols')
const { throwFormErrorText, errors } = require('../errors')
const isXlibOrLibudevRe = /^(?:Xlib|libudev)/
@@ -52,7 +55,7 @@ module.exports = {
executable = path.resolve(util.getEnv('CYPRESS_RUN_BINARY'))
}
debug('needs XVFB?', needsXvfb)
debug('needs to start own XVFB?', needsXvfb)
// always push cwd into the args
args = [].concat(args, '--cwd', process.cwd())
@@ -69,7 +72,9 @@ module.exports = {
// if we're in dev then reset
// the launch cmd to be 'npm run dev'
executable = 'node'
args.unshift(path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js'))
args.unshift(
path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js')
)
}
const overrides = util.getEnvOverrides()
@@ -91,6 +96,12 @@ module.exports = {
options = _.extend({}, options, { windowsHide: false })
}
if (os.platform() === 'linux' && process.env.DISPLAY) {
// make sure we use the latest DISPLAY variable if any
debug('passing DISPLAY', process.env.DISPLAY)
options.env.DISPLAY = process.env.DISPLAY
}
const child = cp.spawn(executable, args, options)
child.on('close', resolve)
@@ -101,17 +112,21 @@ module.exports = {
// if this is defined then we are manually piping for linux
// to filter out the garbage
child.stderr && child.stderr.on('data', (data) => {
const str = data.toString()
child.stderr &&
child.stderr.on('data', (data) => {
const str = data.toString()
// bail if this is warning line garbage
if (isXlibOrLibudevRe.test(str) || isHighSierraWarningRe.test(str)) {
return
}
// bail if this is warning line garbage
if (
isXlibOrLibudevRe.test(str) ||
isHighSierraWarningRe.test(str)
) {
return
}
// else pass it along!
process.stderr.write(data)
})
// else pass it along!
process.stderr.write(data)
})
// https://github.com/cypress-io/cypress/issues/1841
// In some versions of node, it will throw on windows
@@ -133,18 +148,61 @@ module.exports = {
})
}
const userFriendlySpawn = () => {
const spawnInXvfb = () => {
return xvfb
.start()
.then(() => {
// call userFriendlySpawn ourselves
// to prevent result of previous promise
// from becoming a parameter to userFriendlySpawn
debug('spawning Cypress after starting XVFB')
return userFriendlySpawn()
})
.finally(xvfb.stop)
}
const userFriendlySpawn = (shouldRetryOnDisplayProblem) => {
debug('spawning, should retry on display problem?', Boolean(shouldRetryOnDisplayProblem))
if (os.platform() === 'linux') {
debug('DISPLAY is %s', process.env.DISPLAY)
}
return spawn()
.then((code) => {
const POTENTIAL_DISPLAY_PROBLEM_EXIT_CODE = 234
if (shouldRetryOnDisplayProblem && code === POTENTIAL_DISPLAY_PROBLEM_EXIT_CODE) {
debug('Cypress thinks there is a potential display or OS problem')
debug('retrying the command with our XVFB')
// if we get this error, we are on Linux and DISPLAY is set
logger.warn(`${stripIndent`
${logSymbols.warning} Warning: Cypress process has finished very quickly with an error,
which might be related to a potential problem with how the DISPLAY is configured.
DISPLAY was set to "${process.env.DISPLAY}"
We will attempt to spin our XVFB server and run Cypress again.
`}\n`)
return spawnInXvfb()
}
return code
})
.catch(throwFormErrorText(errors.unexpected))
}
if (needsXvfb) {
return xvfb.start()
.then(userFriendlySpawn)
.finally(xvfb.stop)
return spawnInXvfb()
}
return userFriendlySpawn()
// if we have problems spawning Cypress, maybe user DISPLAY setting is incorrect
// in that case retry with our own XVFB
const shouldRetryOnDisplayProblem = os.platform() === 'linux'
return userFriendlySpawn(shouldRetryOnDisplayProblem)
},
}

View File

@@ -2,9 +2,11 @@ const os = require('os')
const Promise = require('bluebird')
const Xvfb = require('@cypress/xvfb')
const R = require('ramda')
const { stripIndent } = require('common-tags')
const debug = require('debug')('cypress:cli')
const debugXvfb = require('debug')('cypress:xvfb')
const { throwFormErrorText, errors } = require('../errors')
const util = require('../util')
const xvfb = Promise.promisifyAll(new Xvfb({
timeout: 5000, // milliseconds
@@ -41,7 +43,33 @@ module.exports = {
},
isNeeded () {
return os.platform() === 'linux' && !process.env.DISPLAY
if (os.platform() !== 'linux') {
return false
}
if (process.env.DISPLAY) {
const issueUrl = util.getGitHubIssueUrl(4034)
const message = stripIndent`
DISPLAY environment variable is set to ${process.env.DISPLAY} on Linux
Assuming this DISPLAY points at working X11 server,
Cypress will not spawn own XVFB
NOTE: if the X11 server is NOT working, Cypress will exit without explanation,
see ${issueUrl}
Solution: Unset the DISPLAY variable and try again:
DISPLAY= npx cypress run ...
`
debug(message)
return false
}
debug('undefined DISPLAY environment variable')
debug('Cypress will spawn its own XVFB')
return true
},
// async method, resolved with Boolean

View File

@@ -48,17 +48,54 @@ const runSmokeTest = (binaryDir, options) => {
return throwFormErrorText(errors.missingXvfb)(`Caught error trying to run XVFB: "${err.message}"`)
}
const onSmokeTestError = (smokeTestCommand) => {
const onSmokeTestError = (smokeTestCommand, runningWithOurXvfb, prevDisplayError) => {
return (err) => {
debug('Smoke test failed:', err)
let errMessage = err.stderr || err.message
debug('error message:', errMessage)
if (err.timedOut) {
debug('error timedOut is true')
return throwFormErrorText(errors.smokeTestFailure(smokeTestCommand, true))(errMessage)
}
if (!runningWithOurXvfb && !prevDisplayError && util.isDisplayError(errMessage)) {
// running without our XVFB
// for the very first time
// and we hit invalid display error
debug('Smoke test hit Linux display problem: %s', errMessage)
logger.warn(`${stripIndent`
${logSymbols.warning} Warning: we have caught a display problem:
${errMessage}
We will attempt to spin our XVFB server and verify again.
`}\n`)
const err = new Error(errMessage)
err.displayError = true
err.platform = 'linux'
throw err
}
if (prevDisplayError) {
debug('this was our 2nd attempt at verifying')
debug('first we tried with user-given DISPLAY')
debug('now we have tried spinning our own XVFB')
debug('and yet it still has failed with')
debug(errMessage)
return throwFormErrorText(errors.invalidDisplayError)(errMessage, prevDisplayError.message)
}
debug('throwing missing dependency error')
return throwFormErrorText(errors.missingDependency)(errMessage)
}
}
@@ -67,9 +104,13 @@ const runSmokeTest = (binaryDir, options) => {
debug('needs XVFB?', needsXvfb)
const spawn = () => {
/**
* Spawn Cypress running smoke test to check if all operating system
* dependencies are good.
*/
const spawn = (runningWithOurXvfb, prevDisplayError) => {
const random = _.random(0, 1000)
const args = ['--smoke-test', `--ping=${random}`]
const args = ['--smoke-test', `--ping=${random}`, '--enable-logging']
const smokeTestCommand = `${cypressExecPath} ${args.join(' ')}`
debug('smoke test command:', smokeTestCommand)
@@ -79,7 +120,7 @@ const runSmokeTest = (binaryDir, options) => {
args,
{ timeout: options.smokeTestTimeout }
))
.catch(onSmokeTestError(smokeTestCommand))
.catch(onSmokeTestError(smokeTestCommand, runningWithOurXvfb, prevDisplayError))
.then((result) => {
// TODO: when execa > 1.1 is released
@@ -89,25 +130,34 @@ const runSmokeTest = (binaryDir, options) => {
debug('smoke test stdout "%s"', smokeTestReturned)
if (!util.stdoutLineMatches(String(random), smokeTestReturned)) {
debug('Smoke test failed:', result)
debug('Smoke test failed because could not find %d in:', random, result)
return throwFormErrorText(errors.smokeTestFailure(smokeTestCommand, false))(result.stderr || result.stdout)
}
})
}
if (needsXvfb) {
const spinXvfbAndVerify = (prevDisplayError) => {
return xvfb.start()
.catch(onXvfbError)
.then(spawn)
.then(spawn.bind(null, true, prevDisplayError))
.finally(() => {
return xvfb.stop()
.catch(onXvfbError)
})
}
return spawn()
if (needsXvfb) {
return spinXvfbAndVerify()
}
return spawn()
.catch({ displayError: true, platform: 'linux' }, (e) => {
debug('there was a display error')
debug('will try spinning our own XVFB and verify Cypress')
return spinXvfbAndVerify(e)
})
}
function testBinary (version, binaryDir, options) {

View File

@@ -1,6 +1,8 @@
const _ = require('lodash')
const R = require('ramda')
const os = require('os')
const la = require('lazy-ass')
const is = require('check-more-types')
const tty = require('tty')
const path = require('path')
const isCi = require('is-ci')
@@ -16,6 +18,8 @@ const pkg = require(path.join(__dirname, '..', 'package.json'))
const logger = require('./logger')
const debug = require('debug')('cypress:cli')
const issuesUrl = 'https://github.com/cypress-io/cypress/issues'
const getosAsync = Promise.promisify(getos)
const stringify = (val) => {
@@ -26,6 +30,14 @@ function normalizeModuleOptions (options = {}) {
return _.mapValues(options, stringify)
}
/**
* Returns true if the platform is Linux. We do a lot of different
* stuff on Linux (like XVFB) and it helps to has readable code
*/
const isLinux = () => {
return os.platform() === 'linux'
}
function stdoutLineMatches (expectedLine, stdout) {
const lines = stdout.split('\n').map(R.trim)
const lineMatches = R.equals(expectedLine)
@@ -176,9 +188,11 @@ const util = {
return Promise.resolve(executable(filePath))
},
isLinux,
getOsVersionAsync () {
return Promise.try(() => {
if (os.platform() === 'linux') {
if (isLinux()) {
return getosAsync()
.then((osInfo) => {
return [osInfo.dist, osInfo.release].join(' - ')
@@ -243,6 +257,26 @@ const util = {
exec: execa,
stdoutLineMatches,
issuesUrl,
getGitHubIssueUrl (number) {
la(is.positive(number), 'github issue should be a positive number', number)
la(_.isInteger(number), 'github issue should be an integer', number)
return `${issuesUrl}/${number}`
},
/**
* If the DISPLAY variable is set incorrectly, when trying to spawn
* Cypress executable we get an error like this:
```
[1005:0509/184205.663837:WARNING:browser_main_loop.cc(258)] Gtk: cannot open display: 99
```
*/
isDisplayError (errorMessage) {
return isLinux() && errorMessage.includes('cannot open display:')
},
}
module.exports = util

View File

@@ -87,11 +87,13 @@
"babel-preset-es2015": "6.24.1",
"bin-up": "1.2.0",
"chai": "3.5.0",
"chai-as-promised": "7.1.1",
"chai-string": "1.4.0",
"dependency-check": "3.3.0",
"dtslint": "0.7.1",
"execa-wrap": "1.4.0",
"mock-fs": "4.9.0",
"mocked-env": "1.2.4",
"nock": "9.6.1",
"nyc": "13.3.0",
"proxyquire": "2.1.0",

View File

@@ -15,7 +15,7 @@ describe('errors', function () {
describe('individual', () => {
it('has the following errors', () => {
return snapshot(Object.keys(errors))
return snapshot(Object.keys(errors).sort())
})
})
@@ -29,5 +29,48 @@ describe('errors', function () {
snapshot(text)
})
})
it('calls solution if a function', () => {
const solution = sinon.stub().returns('a solution')
const error = {
description: 'description',
solution,
}
return formErrorText(error)
.then((text) => {
snapshot(text)
expect(solution).to.have.been.calledOnce
})
})
it('passes message and previous message', () => {
const solution = sinon.stub().returns('a solution')
const error = {
description: 'description',
solution,
}
return formErrorText(error, 'msg', 'prevMsg')
.then(() => {
expect(solution).to.have.been.calledWithExactly('msg', 'prevMsg')
})
})
it('expects solution to be a string', () => {
const error = {
description: 'description',
solution: 42,
}
return expect(formErrorText(error)).to.be.rejected
})
it('forms full text for invalid display error', () => {
return formErrorText(errors.invalidDisplayError, 'current message', 'prev message')
.then((text) => {
snapshot('invalid display error', text)
})
})
})
})

View File

@@ -5,6 +5,7 @@ const os = require('os')
const tty = require('tty')
const path = require('path')
const EE = require('events')
const mockedEnv = require('mocked-env')
const state = require(`${lib}/tasks/state`)
const xvfb = require(`${lib}/exec/xvfb`)
@@ -120,6 +121,38 @@ describe('lib/exec/spawn', function () {
})
})
describe('Linux display', () => {
let restore
beforeEach(() => {
restore = mockedEnv({
DISPLAY: 'test-display',
})
})
afterEach(() => {
restore()
})
it('retries with xvfb if fails with display exit code', function () {
const POTENTIAL_DISPLAY_PROBLEM_EXIT_CODE = 234
this.spawnedProcess.on.withArgs('close').onFirstCall().yieldsAsync(POTENTIAL_DISPLAY_PROBLEM_EXIT_CODE)
this.spawnedProcess.on.withArgs('close').onSecondCall().yieldsAsync(0)
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'

View File

@@ -56,7 +56,7 @@ context('lib/tasks/verify', () => {
sinon.stub(_, 'random').returns('222')
util.exec
.withArgs(executablePath, ['--smoke-test', '--ping=222'])
.withArgs(executablePath, ['--smoke-test', '--ping=222', '--enable-logging'])
.resolves(spawnedProcess)
})
@@ -284,6 +284,85 @@ context('lib/tasks/verify', () => {
})
})
describe('smoke test retries on bad display with our XVFB', () => {
beforeEach(() => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0777 }),
packageVersion,
})
util.exec.restore()
sinon.spy(logger, 'warn')
})
it('successfully retries with our XVFB on Linux', () => {
// initially we think the user has everything set
xvfb.isNeeded.returns(false)
sinon.stub(util, 'exec').callsFake(() => {
// using .callsFake to set platform to Linux
// to allow retry logic to work
os.platform.returns('linux')
const firstSpawnError = new Error('')
// this message contains typical Gtk error shown if X11 is incorrect
// like in the case of DISPLAY=987
firstSpawnError.stderr = '[some noise here] Gtk: cannot open display: 987'
firstSpawnError.stdout = ''
// the second time the binary returns expected ping
util.exec.withArgs(executablePath).resolves({
stdout: '222',
})
return Promise.reject(firstSpawnError)
})
return verify.start().then(() => {
expect(util.exec).to.have.been.calledTwice
// user should have been warned
expect(logger.warn).to.have.been.calledOnce
})
})
it('fails on both retries with our XVFB on Linux', () => {
// initially we think the user has everything set
xvfb.isNeeded.returns(false)
sinon.stub(util, 'exec').callsFake(() => {
expect(xvfb.start).to.not.have.been.called
os.platform.returns('linux')
const firstSpawnError = new Error('')
// this message contains typical Gtk error shown if X11 is incorrect
// like in the case of DISPLAY=987
firstSpawnError.stderr = '[some noise here] Gtk: cannot open display: 987'
firstSpawnError.stdout = ''
// the second time it runs, it fails for some other reason
util.exec.withArgs(executablePath).rejects(new Error('some other error'))
return Promise.reject(firstSpawnError)
})
return verify.start().then(() => {
throw new Error('Should have failed')
}, (e) => {
expect(util.exec).to.have.been.calledTwice
// second time around we should have called XVFB
expect(xvfb.start).to.have.been.calledOnce
expect(xvfb.stop).to.have.been.calledOnce
// user should have been warned
expect(logger.warn).to.have.been.calledOnce
snapshot('tried to verify twice, on the first try got the DISPLAY error', e.message)
})
})
})
it('logs an error if Cypress executable does not exist', () => {
createfs({
alreadyVerified: false,
@@ -500,7 +579,7 @@ context('lib/tasks/verify', () => {
customDir: '/real/custom',
})
util.exec
.withArgs(realEnvBinaryPath, ['--smoke-test', '--ping=222'])
.withArgs(realEnvBinaryPath, ['--smoke-test', '--ping=222', '--enable-logging'])
.resolves(spawnedProcess)
return verify.start().then(() => {

View File

@@ -15,6 +15,36 @@ describe('util', () => {
sinon.stub(logger, 'error')
})
context('.isDisplayError', () => {
it('detects only GTK message', () => {
os.platform.returns('linux')
const text = '[some noise here] Gtk: cannot open display: 99'
expect(util.isDisplayError(text)).to.be.true
// and not for the other messages
expect(util.isDisplayError('display was set incorrectly')).to.be.false
})
})
context('.getGitHubIssueUrl', () => {
it('returls url for issue number', () => {
const url = util.getGitHubIssueUrl(4034)
expect(url).to.equal('https://github.com/cypress-io/cypress/issues/4034')
})
it('throws for anything but a positive integer', () => {
expect(() => {
return util.getGitHubIssueUrl('4034')
}).to.throw
expect(() => {
return util.getGitHubIssueUrl(-5)
}).to.throw
expect(() => {
return util.getGitHubIssueUrl(5.19)
}).to.throw
})
})
context('.stdoutLineMatches', () => {
const { stdoutLineMatches } = util

View File

@@ -27,6 +27,7 @@ global.lib = path.join(__dirname, '..', 'lib')
require('chai')
.use(require('@cypress/sinon-chai'))
.use(require('chai-string'))
.use(require('chai-as-promised'))
sinon.usingPromise(Promise)

View File

@@ -33,6 +33,7 @@
"clean-deps": "npm run all clean-deps && rm -rf node_modules",
"docker": "./scripts/run-docker-local.sh",
"lint-js": "eslint --fix scripts/*.js packages/ts/*.js cli/*.js cli/**/*.js",
"lint-changed": "git diff --name-only | grep '\\.js$' | xargs npx eslint --fix",
"lint-coffee": "coffeelint scripts/**/*.coffee",
"lint": "npm run lint-js && npm run lint-coffee",
"pretest": "npm run lint && npm run all lint && npm run test-scripts",

View File

@@ -72,6 +72,10 @@ module.exports = {
debug("spawning %s with args", execPath, argv)
if debug.enabled
# let's see everything Electron spits back
argv.push("--enable-logging")
cp.spawn(execPath, argv, {stdio: "inherit"})
.on "close", (code) ->
debug("electron closing with code", code)

View File

@@ -11,6 +11,7 @@ require("./environment")
_ = require("lodash")
cp = require("child_process")
os = require("os")
path = require("path")
Promise = require("bluebird")
debug = require("debug")("cypress:server:cypress")
@@ -34,7 +35,13 @@ exitErr = (err) ->
require("./errors").log(err)
.then -> exit(1)
# signals back that Cypress thinks there the Electron could not
# start due to misconfigured DISPLAY on Linux
POTENTIAL_DISPLAY_PROBLEM_EXIT_CODE = 234
module.exports = {
POTENTIAL_DISPLAY_PROBLEM_EXIT_CODE,
isCurrentlyRunningElectron: ->
!!(process.versions and process.versions.electron)
@@ -54,12 +61,34 @@ module.exports = {
else
new Promise (resolve) ->
cypressElectron = require("@packages/electron")
electronStarted = Number(new Date())
fn = (code) ->
## juggle up the totalFailed since our outer
## promise is expecting this object structure
debug("electron finished with", code)
resolve({totalFailed: code})
cypressElectron.open(".", require("./util/args").toArray(options), fn)
electronFinished = Number(new Date())
elapsed = electronFinished - electronStarted
debug("electron open took %d ms", elapsed)
result = {totalFailed: code}
if code is 1 and elapsed < 1000
## very suspicious - the Electron process quickly exited
if os.platform() is "linux" && Boolean(process.env.DISPLAY)
## ok, maybe the DISPLAY is set incorrectly
## and Electron just failed to start with
## an error "Could not open display"
## or it could be some system library missing for Electron to start
debug("because Linux with DISPLAY set to %s", process.env.DISPLAY)
debug("and very short elapsed time, returning potentialDisplayProblem flag")
debug("could be DISPLAY configuration or OS dependency missing")
result.potentialDisplayProblem = true
resolve(result)
args = require("./util/args").toArray(options)
debug("electron open arguments %o", args)
cypressElectron.open(".", args, fn)
openProject: (options) ->
## this code actually starts a project
@@ -224,8 +253,13 @@ module.exports = {
## run headlessly and exit
## with num of totalFailed
@runElectron(mode, options)
.get("totalFailed")
.then(exit)
.then (result) ->
if result.potentialDisplayProblem
debug("exiting with potential display problem")
exit(POTENTIAL_DISPLAY_PROBLEM_EXIT_CODE)
# normal exit
exit(result.totalFailed)
.catch(exitErr)
when "interactive"

View File

@@ -268,6 +268,7 @@ module.exports = {
options = _.omit(options, configKeys)
options = normalizeBackslashes(options)
debug('options %o', options)
//# normalize project to projectRoot
if (p = options.project || options.runProject) {