catch child process killed with a signal (#5810)

* 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
This commit is contained in:
Gleb Bahmutov
2019-11-27 15:21:47 -05:00
committed by Zach Bloomquist
parent 0311c580c8
commit 64f5bf0870
6 changed files with 136 additions and 14 deletions
+25
View File
@@ -29,6 +29,7 @@ Cypress Version: 1.2.3
exports['errors individual has the following errors 1'] = [
"CYPRESS_RUN_BINARY",
"binaryNotExecutable",
"childProcessKilled",
"failedDownload",
"failedUnzip",
"invalidCacheDirectory",
@@ -71,3 +72,27 @@ If you are using Docker, we provide containers with all required dependencies in
Platform: test platform (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['child kill error object'] = {
"description": "The Test Runner unexpectedly exited via a exit event with signal SIGKILL",
"solution": "Please search Cypress documentation for possible solutions:\n\n https://on.cypress.io\n\nCheck if there is a GitHub issue describing this crash:\n\n https://github.com/cypress-io/cypress/issues\n\nConsider opening a new issue."
}
exports['Error message'] = `
The Test Runner unexpectedly exited via a exit event with signal SIGKILL
Please search Cypress documentation for possible solutions:
https://on.cypress.io
Check if there is a GitHub issue describing this crash:
https://github.com/cypress-io/cypress/issues
Consider opening a new issue.
----------
Platform: test platform (Foo-OsVersion)
Cypress Version: 1.2.3
`
+18
View File
@@ -0,0 +1,18 @@
exports['lib/exec/spawn .start detects kill signal exits with error on SIGKILL 1'] = `
The Test Runner unexpectedly exited via a exit event with signal SIGKILL
Please search Cypress documentation for possible solutions:
https://on.cypress.io
Check if there is a GitHub issue describing this crash:
https://github.com/cypress-io/cypress/issues
Consider opening a new issue.
----------
Platform: darwin (Foo-OsVersion)
Cypress Version: 0.0.0
`
+50 -11
View File
@@ -168,20 +168,21 @@ const versionMismatch = {
solution: 'Install Cypress and verify app again',
}
const solutionUnknown = stripIndent`
Please search Cypress documentation for possible solutions:
${chalk.blue(docsUrl)}
Check if there is a GitHub issue describing this crash:
${chalk.blue(util.issuesUrl)}
Consider opening a new issue.
`
const unexpected = {
description:
'An unexpected error occurred while verifying the Cypress executable.',
solution: stripIndent`
Please search Cypress documentation for possible solutions:
${chalk.blue(docsUrl)}
Check if there is a GitHub issue describing this crash:
${chalk.blue(util.issuesUrl)}
Consider opening a new issue.
`,
solution: solutionUnknown,
}
const invalidCypressEnv = {
@@ -191,6 +192,20 @@ const invalidCypressEnv = {
exitCode: 11,
}
/**
* This error happens when CLI detects that the child Test Runner process
* was killed with a signal, like SIGBUS
* @see https://github.com/cypress-io/cypress/issues/5808
* @param {'close'|'event'} eventName Child close event name
* @param {string} signal Signal that closed the child process, like "SIGBUS"
*/
const childProcessKilled = (eventName, signal) => {
return {
description: `The Test Runner unexpectedly exited via a ${chalk.cyan(eventName)} event with signal ${chalk.cyan(signal)}`,
solution: solutionUnknown,
}
}
const removed = {
CYPRESS_BINARY_VERSION: {
description: stripIndent`
@@ -240,6 +255,28 @@ function addPlatformInformation (info) {
})
}
/**
* Given an error object (see the errors above), forms error message text with details,
* then resolves with Error instance you can throw or reject with.
* @param {object} errorObject
* @returns {Promise<Error>} resolves with an Error
* @example
```js
// inside a Promise with "resolve" and "reject"
const errorObject = childProcessKilled('exit', 'SIGKILL')
return getError(errorObject).then(reject)
```
*/
function getError (errorObject) {
return formErrorText(errorObject).then((errorMessage) => {
const err = new Error(errorMessage)
err.known = true
return err
})
}
/**
* Forms nice error message with error and platform information,
* and if possible a way to solve it. Resolves with a string.
@@ -355,6 +392,7 @@ module.exports = {
// formError,
formErrorText,
throwFormErrorText,
getError,
hr,
errors: {
nonZeroExitCodeXvfb,
@@ -373,5 +411,6 @@ module.exports = {
removed,
CYPRESS_RUN_BINARY,
smokeTestFailure,
childProcessKilled,
},
}
+11 -2
View File
@@ -10,7 +10,7 @@ const util = require('../util')
const state = require('../tasks/state')
const xvfb = require('./xvfb')
const verify = require('../tasks/verify')
const { throwFormErrorText, errors } = require('../errors')
const errors = require('../errors')
const isXlibOrLibudevRe = /^(?:Xlib|libudev)/
const isHighSierraWarningRe = /\*\*\* WARNING/
@@ -140,6 +140,13 @@ module.exports = {
function resolveOn (event) {
return function (code, signal) {
debug('child event fired %o', { event, code, signal })
if (code === null) {
const errorObject = errors.errors.childProcessKilled(event, signal)
return errors.getError(errorObject).then(reject)
}
resolve(code)
}
}
@@ -251,7 +258,9 @@ module.exports = {
return code
})
.catch(throwFormErrorText(errors.unexpected))
// we can format and handle an error message from the code above
// prevent wrapping error again by using "known: undefined" filter
.catch({ known: undefined }, errors.throwFormErrorText(errors.errors.unexpected))
}
if (needsXvfb) {
+15 -1
View File
@@ -2,7 +2,7 @@ require('../spec_helper')
const os = require('os')
const snapshot = require('../support/snapshot')
const { errors, formErrorText } = require(`${lib}/errors`)
const { errors, getError, formErrorText } = require(`${lib}/errors`)
const util = require(`${lib}/util`)
describe('errors', function () {
@@ -19,6 +19,20 @@ describe('errors', function () {
})
})
context('getError', () => {
it('forms full message and creates Error object', () => {
const errObject = errors.childProcessKilled('exit', 'SIGKILL')
snapshot('child kill error object', errObject)
return getError(errObject).then((e) => {
expect(e).to.be.an('Error')
expect(e).to.have.property('known', true)
snapshot('Error message', e.message)
})
})
})
context('.errors.formErrorText', function () {
it('returns fully formed text message', () => {
expect(missingXvfb).to.be.an('object')
+17
View File
@@ -7,6 +7,7 @@ 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`)
@@ -14,6 +15,7 @@ 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()
@@ -171,6 +173,20 @@ describe('lib/exec/spawn', function () {
})
})
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)
@@ -246,6 +262,7 @@ describe('lib/exec/spawn', function () {
.then(() => {
throw new Error('should have hit error handler but did not')
}, (e) => {
debug('error message', e.message)
expect(e.message).to.include(msg)
})
})