mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-25 10:30:39 -06:00
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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
[34mhttps://on.cypress.io/required-dependencies[39m
|
||||
|
||||
If you are using Docker, we provide containers with all required dependencies installed.
|
||||
|
||||
----------
|
||||
|
||||
Platform: test platform (Foo-OsVersion)
|
||||
Cypress Version: 1.2.3
|
||||
`
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
[34mhttps://on.cypress.io/required-dependencies[39m
|
||||
|
||||
If you are using Docker, we provide containers with all required dependencies installed.
|
||||
|
||||
----------
|
||||
|
||||
Platform: linux (Foo-OsVersion)
|
||||
Cypress Version: 1.2.3
|
||||
`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user