Skip to content

Commit

Permalink
fix: Handle more argument types in privileged commands (#27166)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbreiding committed Jun 29, 2023
1 parent 6787693 commit e0d814c
Show file tree
Hide file tree
Showing 17 changed files with 95 additions and 85 deletions.
2 changes: 1 addition & 1 deletion cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ _Released 07/05/2023 (PENDING)_

**Bugfixes:**

- Fixed issues where commands would fail with the error `must only be invoked from the spec file or support file`. Fixes [#27149](https://github.com/cypress-io/cypress/issues/27149).
- Fixed issues where commands would fail with the error `must only be invoked from the spec file or support file`. Fixes [#27149](https://github.com/cypress-io/cypress/issues/27149) and [#27163](https://github.com/cypress-io/cypress/issues/27163).
- Fixed an issue where chrome was not recovering from browser crashes properly. Fixes [#24650](https://github.com/cypress-io/cypress/issues/24650).
- Fixed a race condition that was causing a GraphQL error to appear on the [Debug page](https://docs.cypress.io/guides/cloud/runs#Debug) when viewing a running Cypress Cloud build. Fixed in [#27134](https://github.com/cypress-io/cypress/pull/27134).

Expand Down
13 changes: 12 additions & 1 deletion cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6364,7 +6364,18 @@ declare namespace Cypress {
stderr: string
}

type FileReference = string | BufferType | FileReferenceObject
type TypedArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array

type FileReference = string | BufferType | FileReferenceObject | TypedArray
interface FileReferenceObject {
/*
* Buffers will be used as-is, while strings will be interpreted as an alias or a file path.
Expand Down
4 changes: 2 additions & 2 deletions packages/driver/cypress/e2e/commands/exec.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('src/cy/commands/exec', () => {
cy.exec('ls').then(() => {
expect(Cypress.backend).to.be.calledWith('run:privileged', {
commandName: 'exec',
userArgs: ['ls'],
userArgs: ['8374177128052794'],
options: {
cmd: 'ls',
timeout: 2500,
Expand All @@ -34,7 +34,7 @@ describe('src/cy/commands/exec', () => {
cy.exec('ls', { env: { FOO: 'foo' } }).then(() => {
expect(Cypress.backend).to.be.calledWith('run:privileged', {
commandName: 'exec',
userArgs: ['ls', { env: { FOO: 'foo' } }],
userArgs: ['8374177128052794', '6419589148408857'],
options: {
cmd: 'ls',
timeout: 2500,
Expand Down
16 changes: 8 additions & 8 deletions packages/driver/cypress/e2e/commands/files.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('src/cy/commands/files', () => {
'run:privileged',
{
commandName: 'readFile',
userArgs: ['foo.json'],
userArgs: ['6998637248317671'],
options: {
file: 'foo.json',
encoding: 'utf8',
Expand All @@ -40,7 +40,7 @@ describe('src/cy/commands/files', () => {
'run:privileged',
{
commandName: 'readFile',
userArgs: ['foo.json', 'ascii'],
userArgs: ['6998637248317671', '2573904513237804'],
options: {
file: 'foo.json',
encoding: 'ascii',
Expand All @@ -62,7 +62,7 @@ describe('src/cy/commands/files', () => {
'run:privileged',
{
commandName: 'readFile',
userArgs: ['foo.json', null],
userArgs: ['6998637248317671', '6158203196586298'],
options: {
file: 'foo.json',
encoding: null,
Expand Down Expand Up @@ -452,7 +452,7 @@ describe('src/cy/commands/files', () => {
'run:privileged',
{
commandName: 'writeFile',
userArgs: ['foo.txt', 'contents'],
userArgs: ['2916834115813688', '4891975990226114'],
options: {
fileName: 'foo.txt',
contents: 'contents',
Expand All @@ -472,7 +472,7 @@ describe('src/cy/commands/files', () => {
'run:privileged',
{
commandName: 'writeFile',
userArgs: ['foo.txt', 'contents', 'ascii'],
userArgs: ['2916834115813688', '4891975990226114', '2573904513237804'],
options: {
fileName: 'foo.txt',
contents: 'contents',
Expand All @@ -495,7 +495,7 @@ describe('src/cy/commands/files', () => {
'run:privileged',
{
commandName: 'writeFile',
userArgs: ['foo.txt', buffer, null],
userArgs: ['2916834115813688', '6309890104324788', '6158203196586298'],
options: {
fileName: 'foo.txt',
contents: buffer,
Expand All @@ -515,7 +515,7 @@ describe('src/cy/commands/files', () => {
'run:privileged',
{
commandName: 'writeFile',
userArgs: ['foo.txt', 'contents', { encoding: 'ascii' }],
userArgs: ['2916834115813688', '4891975990226114', '4694939291947123'],
options: {
fileName: 'foo.txt',
contents: 'contents',
Expand Down Expand Up @@ -570,7 +570,7 @@ describe('src/cy/commands/files', () => {
'run:privileged',
{
commandName: 'writeFile',
userArgs: ['foo.txt', 'contents', { flag: 'a+' }],
userArgs: ['2916834115813688', '4891975990226114', '2343101193011749'],
options: {
fileName: 'foo.txt',
contents: 'contents',
Expand Down
4 changes: 2 additions & 2 deletions packages/driver/cypress/e2e/commands/task.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('src/cy/commands/task', () => {
cy.task('foo').then(() => {
expect(Cypress.backend).to.be.calledWith('run:privileged', {
commandName: 'task',
userArgs: ['foo'],
userArgs: ['338657716278786'],
options: {
task: 'foo',
timeout: 2500,
Expand All @@ -31,7 +31,7 @@ describe('src/cy/commands/task', () => {
cy.task('foo', { foo: 'foo' }).then(() => {
expect(Cypress.backend).to.be.calledWith('run:privileged', {
commandName: 'task',
userArgs: ['foo', { foo: 'foo' }],
userArgs: ['338657716278786', '4940328425038888'],
options: {
task: 'foo',
timeout: 2500,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ context('cy.origin files', { browser: '!webkit' }, () => {
'run:privileged',
{
commandName: 'writeFile',
userArgs: ['foo.json', contents],
userArgs: ['6998637248317671', '4581875909943693'],
options: {
fileName: 'foo.json',
contents,
Expand Down
13 changes: 13 additions & 0 deletions packages/driver/cypress/e2e/e2e/privileged_commands.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ describe('privileged commands', () => {
}
})

it('handles ArrayBuffer arguments', () => {
cy.task('return:arg', new ArrayBuffer(10))
})

it('handles Buffer arguments', () => {
cy.task('return:arg', Cypress.Buffer.from('contents'))
cy.writeFile('cypress/_test-output/written.json', Cypress.Buffer.from('contents'))
})

it('handles TypedArray arguments', () => {
cy.get('#basic').selectFile(Uint8Array.from([98, 97, 122]))
})

it('passes in test body .then() callback', () => {
cy.then(() => {
cy.exec('echo "hello"')
Expand Down
4 changes: 2 additions & 2 deletions packages/driver/src/cy/commands/actions/selectFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import $dom from '../../../dom'
import $errUtils from '../../../cypress/error_utils'
import $actionability from '../../actionability'
import { addEventCoords, dispatch } from './trigger'
import { runPrivilegedCommand, trimUserArgs } from '../../../util/privileged_channel'
import { runPrivilegedCommand } from '../../../util/privileged_channel'

/* dropzone.js relies on an experimental, nonstandard API, webkitGetAsEntry().
* https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
Expand Down Expand Up @@ -287,7 +287,7 @@ export default (Commands, Cypress, cy, state, config) => {
// privileged commands need to send any and all args, even if not part
// of their API, so they can be compared to the args collected when the
// command is invoked
const userArgs = trimUserArgs([files, _.isObject(options) ? { ...options } : undefined, ...extras])
const userArgs = [files, _.isObject(options) ? { ...options } : undefined, ...extras]

options = _.defaults({}, options, {
action: 'select',
Expand Down
4 changes: 2 additions & 2 deletions packages/driver/src/cy/commands/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Promise from 'bluebird'

import $errUtils from '../../cypress/error_utils'
import type { Log } from '../../cypress/log'
import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel'
import { runPrivilegedCommand } from '../../util/privileged_channel'

interface InternalExecOptions extends Partial<Cypress.ExecOptions> {
_log?: Log
Expand All @@ -17,7 +17,7 @@ export default (Commands, Cypress, cy) => {
// privileged commands need to send any and all args, even if not part
// of their API, so they can be compared to the args collected when the
// command is invoked
const userArgs = trimUserArgs([cmd, userOptions, ...extras])
const userArgs = [cmd, userOptions, ...extras]

userOptions = userOptions || {}

Expand Down
6 changes: 3 additions & 3 deletions packages/driver/src/cy/commands/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { basename } from 'path'

import $errUtils from '../../cypress/error_utils'
import type { Log } from '../../cypress/log'
import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel'
import { runPrivilegedCommand } from '../../util/privileged_channel'

interface InternalReadFileOptions extends Partial<Cypress.Loggable & Cypress.Timeoutable> {
_log?: Log
Expand All @@ -25,7 +25,7 @@ export default (Commands, Cypress, cy, state) => {
// privileged commands need to send any and all args, even if not part
// of their API, so they can be compared to the args collected when the
// command is invoked
const userArgs = trimUserArgs([file, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined, ...extras])
const userArgs = [file, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined, ...extras]

if (_.isObject(encoding)) {
userOptions = encoding
Expand Down Expand Up @@ -149,7 +149,7 @@ export default (Commands, Cypress, cy, state) => {
// privileged commands need to send any and all args, even if not part
// of their API, so they can be compared to the args collected when the
// command is invoked
const userArgs = trimUserArgs([fileName, contents, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined, ...extras])
const userArgs = [fileName, contents, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined, ...extras]

if (_.isObject(encoding)) {
userOptions = encoding
Expand Down
14 changes: 5 additions & 9 deletions packages/driver/src/cy/commands/origin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { $Location } from '../../../cypress/location'
import { LogUtils } from '../../../cypress/log'
import logGroup from '../../logGroup'
import type { StateFunc } from '../../../cypress/state'
import { runPrivilegedCommand, trimUserArgs } from '../../../util/privileged_channel'
import { runPrivilegedCommand } from '../../../util/privileged_channel'

const reHttp = /^https?:\/\//

Expand All @@ -27,17 +27,13 @@ const normalizeOrigin = (urlOrDomain) => {
type OptionsOrFn<T> = { args: T } | (() => {})
type Fn<T> = (args?: T) => {}

function stringifyFn (fn?: any) {
return _.isFunction(fn) ? fn.toString() : undefined
}

function getUserArgs<T> (urlOrDomain: string, optionsOrFn: OptionsOrFn<T>, extras: never[], fn?: Fn<T>) {
return trimUserArgs([
return [
urlOrDomain,
fn && _.isObject(optionsOrFn) ? { ...optionsOrFn } : stringifyFn(optionsOrFn),
fn ? stringifyFn(fn) : undefined,
fn && _.isObject(optionsOrFn) ? { ...optionsOrFn } : optionsOrFn,
fn ? fn : undefined,
...extras,
])
]
}

export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: StateFunc, config: Cypress.InternalConfig) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/driver/src/cy/commands/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import $utils from '../../cypress/utils'
import $errUtils from '../../cypress/error_utils'
import $stackUtils from '../../cypress/stack_utils'
import type { Log } from '../../cypress/log'
import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel'
import { runPrivilegedCommand } from '../../util/privileged_channel'

interface InternalTaskOptions extends Partial<Cypress.Loggable & Cypress.Timeoutable> {
_log?: Log
Expand All @@ -18,7 +18,7 @@ export default (Commands, Cypress, cy) => {
// privileged commands need to send any and all args, even if not part
// of their API, so they can be compared to the args collected when the
// command is invoked
const userArgs = trimUserArgs([task, arg, _.isObject(userOptions) ? { ...userOptions } : undefined, ...extras])
const userArgs = [task, arg, _.isObject(userOptions) ? { ...userOptions } : undefined, ...extras]

userOptions = userOptions || {}

Expand Down
43 changes: 37 additions & 6 deletions packages/driver/src/util/privileged_channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,51 @@ interface RunPrivilegedCommandOptions {
userArgs: any[]
}

// hashes a string in the same manner as is in the privileged channel.
// unfortunately this can't be shared because we want to reduce the surface
// area in the privileged channel, which uses closured references to
// globally-accessible functions
// source: https://github.com/bryc/code/blob/d0dac1c607a005679799024ff66166e13601d397/jshash/experimental/cyrb53.js
function hash (str) {
const seed = 0
let h1 = 0xdeadbeef ^ seed
let h2 = 0x41c6ce57 ^ seed

for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i)
h1 = Math.imul(h1 ^ ch, 2654435761)
h2 = Math.imul(h2 ^ ch, 1597334677)
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)

return `${4294967296 * (2097151 & h2) + (h1 >>> 0)}`
}

export function runPrivilegedCommand ({ commandName, cy, Cypress, options, userArgs }: RunPrivilegedCommandOptions): Bluebird<any> {
const hashedArgs = _.dropRightWhile(userArgs || [], _.isUndefined)
.map((arg) => {
if (arg === undefined) {
arg = null
}

if (typeof arg === 'function') {
arg = arg.toString()
}

return hash(JSON.stringify(arg))
})

return Bluebird.try(() => {
return cy.state('current').get('verificationPromise')[0]
})
.then(() => {
return Cypress.backend('run:privileged', {
commandName,
options,
userArgs,
userArgs: hashedArgs,
})
})
}

// removes trailing undefined args
export function trimUserArgs (args: any[]) {
return _.dropRightWhile(args, _.isUndefined)
}
11 changes: 0 additions & 11 deletions packages/launchpad/cypress/e2e/scaffold-project.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +178,6 @@ describe('scaffolding new projects', { defaultCommandTimeout: 7000 }, () => {
cy.task('uninstallDependenciesInScaffoldedProject', { currentProject })
})

/**
* The task `uninstallDependenciesInScaffoldedProject` removed the node_modules directory from the scaffolded project. This task is async
* so it may not have completed before the tests continues. This test has been flaky due to a race condition caused by:
* * the app would create the cypress config file when the node_modules is still present which causes it to include a call to`defineConfig`
* * then the task above would remove the node_modules folder
* * then the app would try to launch the project and get an error that "Cannot fnid module 'cypress'"
*
* Adding a wait below to let the task above complete before continuing.
*/
cy.wait(1000)

cy.visitLaunchpad()
cy.skipWelcome()
cy.contains('button', cy.i18n.testingType.e2e.name).click()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import fs from 'fs'
import fs from 'fs/promises'
import path from 'path'

export async function uninstallDependenciesInScaffoldedProject ({ currentProject }) {
// @ts-ignore
fs.rmdirSync(path.resolve(currentProject, '../node_modules'), { recursive: true, force: true })
await fs.rm(path.resolve(currentProject, '../node_modules'), { recursive: true, force: true })

return null
}
Loading

4 comments on commit e0d814c

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on e0d814c Jun 29, 2023

Choose a reason for hiding this comment

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

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.17.0/linux-x64/develop-e0d814c24757762adab428aae45f9e47e8d022d6/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on e0d814c Jun 29, 2023

Choose a reason for hiding this comment

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

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.17.0/linux-arm64/develop-e0d814c24757762adab428aae45f9e47e8d022d6/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on e0d814c Jun 29, 2023

Choose a reason for hiding this comment

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

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.17.0/darwin-x64/develop-e0d814c24757762adab428aae45f9e47e8d022d6/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on e0d814c Jun 29, 2023

Choose a reason for hiding this comment

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

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.17.0/darwin-arm64/develop-e0d814c24757762adab428aae45f9e47e8d022d6/cypress.tgz

Please sign in to comment.