Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
707b17a
Adding timeout option to writeFile
tbiethman Nov 18, 2021
0a4fc36
Adding timeout to readFile as well
tbiethman Nov 19, 2021
86917da
Adding unit tests for server
tbiethman Nov 19, 2021
2cc5b89
Updating some phrasing
tbiethman Nov 19, 2021
57b8c9f
Clearing the default timeout to prevent race condition
tbiethman Nov 22, 2021
0ca8f09
Increasing socket http buffer size. Using `responseTimeout` config va…
tbiethman Nov 23, 2021
8405842
Reverting buffer increase to better scope changes
tbiethman Nov 24, 2021
2a61915
Reverting inadvertent test changes.
tbiethman Nov 24, 2021
814ad09
Merge branch 'develop' of github.com:cypress-io/cypress into issue-33…
tbiethman Nov 29, 2021
65fed27
Merge branch 'develop' of github.com:cypress-io/cypress into issue-33…
tbiethman Nov 29, 2021
7a5e813
Adding proper timeout to readFile log options, adding test coverage
tbiethman Nov 29, 2021
7f210f7
Explicitly returning null from writeFile again
tbiethman Nov 30, 2021
51afbfc
Merge branch 'develop' of github.com:cypress-io/cypress into issue-33…
tbiethman Nov 30, 2021
2e4af33
Updating writeFile function definitions
tbiethman Nov 30, 2021
67f0f0c
Restructuring readFile error handling
tbiethman Dec 1, 2021
f053a9c
Updating default values to defaultCommandTimeout to account for passi…
tbiethman Dec 1, 2021
152a121
Detecting socket disconnections that occur during message send. Incre…
tbiethman Dec 2, 2021
e928959
Reverting previous changes to validate socket disconnect detection
tbiethman Dec 2, 2021
84e92f1
Linter got me
tbiethman Dec 2, 2021
58f2475
Utilizing server-side timeout for read/writeFile commands.
tbiethman Dec 2, 2021
ab7b52d
Reverting async/await usage for commands, as async/await introduces p…
tbiethman Dec 3, 2021
f6bc87f
Adding new error message for disconnections
tbiethman Dec 3, 2021
7137e5e
Stopped overriding AbortError. Updated clearTimeout reason.
tbiethman Dec 3, 2021
bdb0694
Using responseTimeout again. Updating tests.
tbiethman Dec 3, 2021
58d8b2b
Updating max buffer comment
tbiethman Dec 3, 2021
9d149ec
Adding global disconnect handler. Updating command_queue error report…
tbiethman Dec 3, 2021
c16be3a
Reverting work in progress
tbiethman Dec 3, 2021
845e1c5
Merge branch 'develop' of github.com:cypress-io/cypress into issue-33…
tbiethman Dec 3, 2021
bf1c901
Missed these timeout options
tbiethman Dec 3, 2021
fef9779
Merge branch 'develop' into issue-3350-writefile-timeout
tbiethman Dec 4, 2021
6d6045b
Merge branch 'develop' of github.com:cypress-io/cypress into issue-33…
tbiethman Dec 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2209,12 +2209,9 @@ declare namespace Cypress {
* @see https://on.cypress.io/writefile
```
cy.writeFile('path/to/message.txt', 'Hello World')
.then((text) => {
expect(text).to.equal('Hello World') // true
})
```
*/
writeFile<C extends FileContents>(filePath: string, contents: C, encoding: Encodings): Chainable<C>
writeFile(filePath: string, contents: FileContents, encoding: Encodings): Chainable<null>
/**
* Write to a file with the specified encoding and contents.
*
Expand All @@ -2223,12 +2220,10 @@ declare namespace Cypress {
cy.writeFile('path/to/ascii.txt', 'Hello World', {
flag: 'a+',
encoding: 'ascii'
}).then((text) => {
expect(text).to.equal('Hello World') // true
})
```
*/
writeFile<C extends FileContents>(filePath: string, contents: C, options?: Partial<WriteFileOptions>): Chainable<C>
writeFile(filePath: string, contents: FileContents, options?: Partial<WriteFileOptions & Timeoutable>): Chainable<null>
/**
* Write to a file with the specified encoding and contents.
*
Expand All @@ -2238,12 +2233,10 @@ declare namespace Cypress {
```
cy.writeFile('path/to/ascii.txt', 'Hello World', 'utf8', {
flag: 'a+',
}).then((text) => {
expect(text).to.equal('Hello World') // true
})
```
*/
writeFile<C extends FileContents>(filePath: string, contents: C, encoding: Encodings, options?: Partial<WriteFileOptions>): Chainable<C>
writeFile(filePath: string, contents: FileContents, encoding: Encodings, options?: Partial<WriteFileOptions & Timeoutable>): Chainable<null>

/**
* jQuery library bound to the AUT
Expand Down
100 changes: 99 additions & 1 deletion packages/driver/cypress/integration/commands/files_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ describe('src/cy/commands/files', () => {
if (attrs.name === 'readFile') {
expect(log.get('state')).to.eq('pending')
expect(log.get('message')).to.eq('foo.json')
expect(log.get('timeout')).to.eq(Cypress.config('defaultCommandTimeout'))
}
})

Expand Down Expand Up @@ -321,6 +322,54 @@ describe('src/cy/commands/files', () => {

cy.readFile('foo.json').should('equal', 'contents')
})

it('throws when the read timeout expires', function (done) {
Cypress.backend.callsFake(() => {
return new Cypress.Promise(() => { /* Broken promise for timeout */ })
})

cy.on('fail', (err) => {
const { lastLog } = this

assertLogLength(this.logs, 1)
expect(lastLog.get('error')).to.eq(err)
expect(lastLog.get('state')).to.eq('failed')
expect(err.message).to.eq(stripIndent`\
\`cy.readFile("foo")\` timed out after waiting \`10ms\`.
`)

expect(err.docsUrl).to.eq('https://on.cypress.io/readfile')

done()
})

cy.readFile('foo', { timeout: 10 })
})

it('uses defaultCommandTimeout config value if option not provided', {
defaultCommandTimeout: 42,
}, function (done) {
Cypress.backend.callsFake(() => {
return new Cypress.Promise(() => { /* Broken promise for timeout */ })
})

cy.on('fail', (err) => {
const { lastLog } = this

assertLogLength(this.logs, 1)
expect(lastLog.get('error')).to.eq(err)
expect(lastLog.get('state')).to.eq('failed')
expect(err.message).to.eq(stripIndent`\
\`cy.readFile("foo")\` timed out after waiting \`42ms\`.
`)

expect(err.docsUrl).to.eq('https://on.cypress.io/readfile')

done()
})

cy.readFile('foo')
})
})
})

Expand Down Expand Up @@ -394,7 +443,7 @@ describe('src/cy/commands/files', () => {
Cypress.backend.resolves(okResponse)

cy.writeFile('foo.txt', 'contents').then((subject) => {
expect(subject).to.not.exist
expect(subject).to.eq(null)
})
})

Expand Down Expand Up @@ -481,6 +530,7 @@ describe('src/cy/commands/files', () => {
if (attrs.name === 'writeFile') {
expect(log.get('state')).to.eq('pending')
expect(log.get('message')).to.eq('foo.txt', 'contents')
expect(log.get('timeout')).to.eq(Cypress.config('defaultCommandTimeout'))
}
})

Expand Down Expand Up @@ -601,6 +651,54 @@ describe('src/cy/commands/files', () => {

cy.writeFile('foo.txt', 'contents')
})

it('throws when the write timeout expires', function (done) {
Cypress.backend.callsFake(() => {
return new Cypress.Promise(() => {})
})

cy.on('fail', (err) => {
const { lastLog } = this

assertLogLength(this.logs, 1)
expect(lastLog.get('error')).to.eq(err)
expect(lastLog.get('state')).to.eq('failed')
expect(err.message).to.eq(stripIndent`
\`cy.writeFile("foo.txt")\` timed out after waiting \`10ms\`.
`)

expect(err.docsUrl).to.eq('https://on.cypress.io/writefile')

done()
})

cy.writeFile('foo.txt', 'contents', { timeout: 10 })
})

it('uses defaultCommandTimeout config value if option not provided', {
defaultCommandTimeout: 42,
}, function (done) {
Cypress.backend.callsFake(() => {
return new Cypress.Promise(() => { /* Broken promise for timeout */ })
})

cy.on('fail', (err) => {
const { lastLog } = this

assertLogLength(this.logs, 1)
expect(lastLog.get('error')).to.eq(err)
expect(lastLog.get('state')).to.eq('failed')
expect(err.message).to.eq(stripIndent`
\`cy.writeFile("foo.txt")\` timed out after waiting \`42ms\`.
`)

expect(err.docsUrl).to.eq('https://on.cypress.io/writefile')

done()
})

cy.writeFile('foo.txt', 'contents')
})
})
})
})
59 changes: 39 additions & 20 deletions packages/driver/src/cy/commands/files.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// @ts-nocheck
import _ from 'lodash'
import Promise from 'bluebird'

import $errUtils from '../../cypress/error_utils'

Expand All @@ -22,6 +21,7 @@ export default (Commands, Cypress, cy) => {
// to restore the default node behavior.
encoding: encoding === undefined ? 'utf8' : encoding,
log: true,
timeout: Cypress.config('defaultCommandTimeout'),
})

const consoleProps = {}
Expand All @@ -43,21 +43,33 @@ export default (Commands, Cypress, cy) => {
})
}

// We clear the default timeout so we can handle
// the timeout ourselves
cy.clearTimeout()

const verifyAssertions = () => {
return Cypress.backend('read:file', file, _.pick(options, 'encoding'))
return Cypress.backend('read:file', file, _.pick(options, 'encoding')).timeout(options.timeout)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So no server-side timeout?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not with this PR, no. I'm going to log a separate issue to track and evaluate how we want to handle command/server timeouts and increase the determinism of those workflows.

.catch((err) => {
if (err.code === 'ENOENT') {
return {
contents: null,
filePath: err.filePath,
}
if (err.name === 'TimeoutError') {
return $errUtils.throwErrByPath('files.timed_out', {
onFail: options._log,
args: { cmd: 'readFile', file, timeout: options.timeout },
})
}

return $errUtils.throwErrByPath('files.unexpected_error', {
onFail: options._log,
args: { cmd: 'readFile', action: 'read', file, filePath: err.filePath, error: err.message },
})
}).then(({ contents, filePath }) => {
// Non-ENOENT errors are not retried
if (err.code !== 'ENOENT') {
return $errUtils.throwErrByPath('files.unexpected_error', {
onFail: options._log,
args: { cmd: 'readFile', action: 'read', file, filePath: err.filePath, error: err.message },
})
}

return {
contents: null,
filePath: err.filePath,
}
}).then(({ filePath, contents }) => {
// https://github.com/cypress-io/cypress/issues/1558
// We invoke Buffer.from() in order to transform this from an ArrayBuffer -
// which socket.io uses to transfer the file over the websocket - into a
Expand Down Expand Up @@ -110,14 +122,15 @@ export default (Commands, Cypress, cy) => {
encoding: encoding === undefined ? 'utf8' : encoding,
flag: userOptions.flag ? userOptions.flag : 'w',
log: true,
timeout: Cypress.config('defaultCommandTimeout'),
})

const consoleProps = {}

if (options.log) {
options._log = Cypress.log({
message: fileName,
timeout: 0,
timeout: options.timeout,
consoleProps () {
return consoleProps
},
Expand All @@ -142,19 +155,25 @@ export default (Commands, Cypress, cy) => {
contents = JSON.stringify(contents, null, 2)
}

return Cypress.backend('write:file', fileName, contents, _.pick(options, ['encoding', 'flag']))
.then(({ contents, filePath }) => {
// We clear the default timeout so we can handle
// the timeout ourselves
cy.clearTimeout()

return Cypress.backend('write:file', fileName, contents, _.pick(options, 'encoding', 'flag')).timeout(options.timeout)
.then(({ filePath, contents }) => {
consoleProps['File Path'] = filePath
consoleProps['Contents'] = contents

return null
}).catch(Promise.TimeoutError, () => {
return $errUtils.throwErrByPath('files.timed_out', {
onFail: options._log,
args: { cmd: 'writeFile', file: fileName, timeout: options.timeout },
})
})
.catch((err) => {
if (err.name === 'TimeoutError') {
return $errUtils.throwErrByPath('files.timed_out', {
onFail: options._log,
args: { cmd: 'writeFile', file: fileName, timeout: options.timeout },
})
}

return $errUtils.throwErrByPath('files.unexpected_error', {
onFail: options._log,
args: { cmd: 'writeFile', action: 'write', file: fileName, filePath: err.filePath, error: err.message },
Expand Down
16 changes: 8 additions & 8 deletions packages/socket/lib/socket.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import fs from 'fs'
import buffer from 'buffer'
import type http from 'http'
import server, { Server as SocketIOBaseServer, ServerOptions } from 'socket.io'
import { client } from './browser'

const HUNDRED_MEGABYTES = 1e8 // 100000000

const { version } = require('socket.io-client/package.json')
const clientSource = require.resolve('socket.io-client/dist/socket.io.js')

Expand All @@ -15,13 +14,14 @@ type PatchedServerOptions = ServerOptions & { cookie: { name: string | boolean }

class SocketIOServer extends SocketIOBaseServer {
constructor (srv: http.Server, opts?: Partial<PatchedServerOptions>) {
// in socket.io v3, they reduced down the max buffer size
// from 100mb to 1mb, so we reset it back to the previous value
//
// previous commit for reference:
// https://github.com/socketio/engine.io/blame/61b949259ed966ef6fc8bfd61f14d1a2ef06d319/lib/server.js#L29
opts = opts ?? {}
opts.maxHttpBufferSize = opts.maxHttpBufferSize ?? HUNDRED_MEGABYTES

// the maxHttpBufferSize is used to limit the message size sent over
// the socket. Small values can be used to mitigate exposure to
// denial of service attacks; the default as of v3.0 is 1MB.
// because our server is local, we do not need to arbitrarily limit
// the message size and can use the theoretical maximum value.
opts.maxHttpBufferSize = opts.maxHttpBufferSize ?? buffer.constants.MAX_LENGTH

super(srv, opts)
}
Expand Down