From 7154fc8dcd7abbef7feb0c0fdacaef31174bc672 Mon Sep 17 00:00:00 2001 From: Alejandro Estrada Date: Mon, 12 Dec 2022 12:47:57 -0500 Subject: [PATCH 1/3] feat: Improve CLOUD_PARALLEL_GROUP_PARAMS_MISMATCH error message (#24799) Co-authored-by: Emily Rohrbough --- ...OUP_PARAMS_MISMATCH - differentParams.html | 67 +++++++++++++++++++ packages/errors/src/errors.ts | 34 +++++++++- .../test/unit/visualSnapshotErrors_spec.ts | 34 ++++++++++ packages/server/__snapshots__/cypress_spec.js | 13 ++-- packages/server/lib/modes/record.js | 1 + .../server/test/integration/cypress_spec.js | 13 ++++ 6 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 packages/errors/__snapshot-html__/CLOUD_PARALLEL_GROUP_PARAMS_MISMATCH - differentParams.html diff --git a/packages/errors/__snapshot-html__/CLOUD_PARALLEL_GROUP_PARAMS_MISMATCH - differentParams.html b/packages/errors/__snapshot-html__/CLOUD_PARALLEL_GROUP_PARAMS_MISMATCH - differentParams.html new file mode 100644 index 000000000000..26e6dbf56c4f --- /dev/null +++ b/packages/errors/__snapshot-html__/CLOUD_PARALLEL_GROUP_PARAMS_MISMATCH - differentParams.html @@ -0,0 +1,67 @@ + + + + + + + + + + + +
You passed the --parallel flag, but we do not parallelize tests across different environments.
+
+This machine is sending different environment parameters than the first machine that started this parallel run.
+
+The existing run is: https://cloud.cypress.io/project/abcd/runs/1
+
+In order to run in parallel mode each machine must send identical environment parameters such as:
+
+ - specs
+ - osName
+ - osVersion
+ - browserName
+ - browserVersion (major)
+
+This machine sent the following parameters:
+
+{
+  "osName": "darwin",
+  "osVersion": "v1",
+  "browserName": "Electron.... (Expected: Electron)",
+  "browserVersion": "59.1.2.3.... (Expected: 64)",
+  "differentSpecs": {
+    "added": [
+      "cypress/integration/foo_spec.js"
+    ],
+    "missing": []
+  }
+}
+
+https://on.cypress.io/parallel-group-params-mismatch
+
\ No newline at end of file diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index 74be0d423f19..9d8d1c3c0f5b 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -279,7 +279,37 @@ export const AllCypressErrors = { https://on.cypress.io/parallel-disallowed` }, - CLOUD_PARALLEL_GROUP_PARAMS_MISMATCH: (arg1: {runUrl: string, parameters: any}) => { + CLOUD_PARALLEL_GROUP_PARAMS_MISMATCH: (arg1: {runUrl: string, parameters: any, payload: any }) => { + let params: any = arg1.parameters + + if (arg1.payload?.differentParams) { + params = {} + + _.map(arg1.parameters, (value, key) => { + if (key === 'specs' && arg1.payload.differentSpecs?.length) { + const addedSpecs: string[] = [] + const missingSpecs: string[] = [] + + _.forEach(arg1.payload.differentSpecs, (s) => { + if (value.includes(s)) { + addedSpecs.push(s) + } else { + missingSpecs.push(s) + } + }) + + params['differentSpecs'] = { + added: addedSpecs, + missing: missingSpecs, + } + } else if (arg1.payload.differentParams[key]?.expected) { + params[key] = `${value}.... (Expected: ${(arg1.payload.differentParams[key].expected)})` + } else { + params[key] = value + } + }) + } + return errTemplate`\ You passed the ${fmt.flag(`--parallel`)} flag, but we do not parallelize tests across different environments. @@ -299,7 +329,7 @@ export const AllCypressErrors = { This machine sent the following parameters: - ${fmt.meta(arg1.parameters)} + ${fmt.meta(params)} https://on.cypress.io/parallel-group-params-mismatch` }, diff --git a/packages/errors/test/unit/visualSnapshotErrors_spec.ts b/packages/errors/test/unit/visualSnapshotErrors_spec.ts index a570cd13aded..cbdfa67e1c1a 100644 --- a/packages/errors/test/unit/visualSnapshotErrors_spec.ts +++ b/packages/errors/test/unit/visualSnapshotErrors_spec.ts @@ -487,6 +487,40 @@ describe('visual error templates', () => { 'cypress/integration/app_spec.js', ], }, + payload: {}, + }, + ], + differentParams: [ + { + group: 'foo', + runUrl: 'https://cloud.cypress.io/project/abcd/runs/1', + ciBuildId: 'test-ciBuildId-123', + parameters: { + osName: 'darwin', + osVersion: 'v1', + browserName: 'Electron', + browserVersion: '59.1.2.3', + specs: [ + 'cypress/integration/app_spec.js', + 'cypress/integration/foo_spec.js', + 'cypress/integration/bar_spec.js', + ], + }, + payload: { + differentParams: { + browserName: { + detected: 'Chrome', + expected: 'Electron', + }, + browserVersion: { + detected: '65', + expected: '64', + }, + }, + differentSpecs: [ + 'cypress/integration/foo_spec.js', + ], + }, }, ], } diff --git a/packages/server/__snapshots__/cypress_spec.js b/packages/server/__snapshots__/cypress_spec.js index db7ef02924f8..609fd1d818a2 100644 --- a/packages/server/__snapshots__/cypress_spec.js +++ b/packages/server/__snapshots__/cypress_spec.js @@ -203,11 +203,14 @@ This machine sent the following parameters: { "osName": "darwin", "osVersion": "v1", - "browserName": "Electron", - "browserVersion": "59.1.2.3", - "specs": [ - "cypress/e2e/app.cy.js" - ] + "browserName": "Electron.... (Expected: Electron)", + "browserVersion": "59.1.2.3.... (Expected: 64)", + "differentSpecs": { + "added": [], + "missing": [ + "cypress/integration/foo_spec.js" + ] + } } https://on.cypress.io/parallel-group-params-mismatch diff --git a/packages/server/lib/modes/record.js b/packages/server/lib/modes/record.js index e1eba002ffd6..2caab1693782 100644 --- a/packages/server/lib/modes/record.js +++ b/packages/server/lib/modes/record.js @@ -466,6 +466,7 @@ const createRun = Promise.method((options = {}) => { browserVersion, specs, }, + payload, }) } case 'PARALLEL_DISALLOWED': diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js index 129832968d66..1c6803fe6410 100644 --- a/packages/server/test/integration/cypress_spec.js +++ b/packages/server/test/integration/cypress_spec.js @@ -1491,6 +1491,19 @@ describe('lib/cypress', () => { code: 'PARALLEL_GROUP_PARAMS_MISMATCH', payload: { runUrl: 'https://cloud.cypress.io/runs/12345', + differentParams: { + browserName: { + detected: 'Chrome', + expected: 'Electron', + }, + browserVersion: { + detected: '65', + expected: '64', + }, + }, + differentSpecs: [ + 'cypress/integration/foo_spec.js', + ], }, } From 2062670f5c51011de09f679352eb34c7bf1406a9 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Mon, 12 Dec 2022 13:07:03 -0600 Subject: [PATCH 2/3] fix: recollect session data after validation is successful (#25112) --- .../e2e/commands/sessions/sessions.cy.js | 56 ++++++++++++++----- .../driver/cypress/fixtures/auth/index.html | 18 ++++-- .../driver/src/cy/commands/sessions/index.ts | 16 ++++++ 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index dda1626167d4..120b76b9224d 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -256,7 +256,8 @@ describe('cy.session', { retries: 0 }, () => { const sessionStorageData = consoleProps.groups[0].groups[0] expect(sessionStorageData.name).to.contain('Session Storage - (1)') - expect(sessionStorageData.items).to.deep.eq({ cypressAuthToken: '{"body":{"username":"tester"}}' }) + expect(sessionStorageData.items).to.have.property('cypressAuthToken') + expect(sessionStorageData.items.cypressAuthToken).to.contains('"username":"tester"') }) }) @@ -839,29 +840,39 @@ describe('cy.session', { retries: 0 }, () => { let clearPageCount = 0 let sessionGroupId let setup + let slowSetup let validate - const handleSetup = () => { + const handleSetup = (slowLogin = false) => { // create session clears page before running cy.contains('Default blank page').should('not.exist') cy.visit('/fixtures/auth/index.html') cy.contains('You are not logged in') - cy.window().then((win) => { - win.sessionStorage.setItem('cypressAuthToken', JSON.stringify({ body: { username: 'tester' } })) - }) + cy.get('[data-cy=login-same-origin]').click() + cy.get('input').type('tester') + if (slowLogin) { + cy.get('[data-cy=slow-login]').click() + } else { + cy.get('[data-cy=login]').click() + } } + const handleSlowSetup = () => handleSetup(true) + const handleValidate = () => { - // both create & restore session clears page after running + // both create & restore session clears page after running cy.contains('Default blank page').should('not.exist') - cy.visit('/fixtures/auth/index.html') - cy.contains('Welcome tester') + cy.window() + .its('sessionStorage') + .its('cypressAuthToken', { timeout: 5000 }) + .should('contain', '"username":"tester"') } before(() => { setup = cy.stub().callsFake(handleSetup).as('setupSession') + slowSetup = cy.stub().callsFake(handleSlowSetup).as('setupSlowSession') validate = cy.stub().callsFake(handleValidate).as('validateSession') }) @@ -879,7 +890,7 @@ describe('cy.session', { retries: 0 }, () => { resetMocks() clearAllSavedSessions() cy.on('log:added', (attrs, log) => { - if (attrs.name === 'session' || attrs.name === 'sessions_manager' || attrs.name === 'page load' || attrs.alias?.includes('setupSession') || attrs.alias?.includes('validateSession')) { + if (attrs.name === 'session' || attrs.name === 'sessions_manager' || attrs.alias?.includes('setupSession') || attrs.alias?.includes('setupSlowSession') || attrs.alias?.includes('validateSession')) { logs.push(log) if (!sessionGroupId) { sessionGroupId = attrs.id @@ -974,7 +985,8 @@ describe('cy.session', { retries: 0 }, () => { const sessionStorageData = consoleProps.groups[0].groups[0] expect(sessionStorageData.name).to.contain('Session Storage - (1)') - expect(sessionStorageData.items).to.deep.eq({ cypressAuthToken: '{"body":{"username":"tester"}}' }) + expect(sessionStorageData.items).to.have.property('cypressAuthToken') + expect(sessionStorageData.items.cypressAuthToken).to.contains('"username":"tester"') }) }) @@ -985,7 +997,7 @@ describe('cy.session', { retries: 0 }, () => { setupTestContext() cy.log('Creating new session with validation to test against') sessionId = `session-${Cypress.state('test').id}` - cy.session(sessionId, setup, { validate }) + cy.session(sessionId, slowSetup, { validate }) }) it('does not clear the page after command', () => { @@ -993,7 +1005,7 @@ describe('cy.session', { retries: 0 }, () => { }) it('successfully creates new session and validates it', () => { - expect(setup).to.be.calledOnce + expect(slowSetup).to.be.calledOnce expect(validate).to.be.calledOnce expect(clearPageCount, 'total times session cleared the page').to.eq(0) }) @@ -1024,7 +1036,7 @@ describe('cy.session', { retries: 0 }, () => { }) expect(logs[3].get()).to.deep.contain({ - alias: ['setupSession'], + alias: ['setupSlowSession'], group: createNewSessionGroup.id, }) @@ -1040,6 +1052,24 @@ describe('cy.session', { retries: 0 }, () => { group: validateSessionGroup.id, }) }) + + it('has session details in the consoleProps', () => { + const consoleProps = logs[0].get('consoleProps')() + + expect(consoleProps.Command).to.eq('session') + expect(consoleProps.id).to.eq(sessionId) + expect(consoleProps.Domains).to.eq('This session captured data from localhost.') + + expect(consoleProps.groups).to.have.length(1) + expect(consoleProps.groups[0].name).to.eq('localhost data:') + expect(consoleProps.groups[0].groups).to.have.length(1) + + const sessionStorageData = consoleProps.groups[0].groups[0] + + expect(sessionStorageData.name).to.contain('Session Storage - (1)') + expect(sessionStorageData.items).to.have.property('cypressAuthToken') + expect(sessionStorageData.items.cypressAuthToken).to.contains('"username":"tester"') + }) }) describe('create session with failed validation flow', () => { diff --git a/packages/driver/cypress/fixtures/auth/index.html b/packages/driver/cypress/fixtures/auth/index.html index c2406435849a..02f6c680821a 100644 --- a/packages/driver/cypress/fixtures/auth/index.html +++ b/packages/driver/cypress/fixtures/auth/index.html @@ -13,7 +13,7 @@ if (newToken) { - sessionStorage.setItem('cypressAuthToken', decodeURIComponent(newToken)); + sessionStorage.setItem('cypressAuthToken', decodeURIComponent(newToken)) urlParams.delete('token') const newSearchParams = urlParams.toString() @@ -43,13 +43,23 @@ // Add Login button that redirects to the idp const loginBtn = document.createElement("button"); - loginBtn.innerHTML = "Login IDP" - loginBtn.dataset.cy = "login-idp" + loginBtn.innerHTML = "Login Same Origin" + loginBtn.dataset.cy = "login-same-origin" loginBtn.onclick = function () { - window.location.href = `http://www.idp.com:3500/fixtures/auth/idp.html?redirect=${encodeURIComponent(window.location.href)}` + window.location.href = `http://localhost:3500/fixtures/auth/idp.html?redirect=${encodeURIComponent(window.location.href)}` }; document.body.appendChild(loginBtn); + + // Add Login button that redirects to the idp + const loginIDPBtn = document.createElement("button"); + loginIDPBtn.innerHTML = "Login IDP" + loginIDPBtn.dataset.cy = "login-idp" + loginIDPBtn.onclick = function () { + window.location.href = `http://www.idp.com:3500/fixtures/auth/idp.html?redirect=${encodeURIComponent(window.location.href)}` + }; + document.body.appendChild(loginIDPBtn); + // Add Login button that redirects to the idp const loginFoobarBtn = document.createElement("button"); loginFoobarBtn.innerHTML = "Login Foobar" diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 32c1fa680f9a..3d168ccd2a54 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -185,6 +185,8 @@ export default function (Commands, Cypress, cy) { consoleProps: () => { return { Step: statusMap.stepName(step), + Message: 'The following is the collected session data after the session was successfully setup:', + ...getConsoleProps(existingSession), } }, @@ -396,6 +398,20 @@ export default function (Commands, Cypress, cy) { } } + // collect all session data again that may have been updated during the validation check + const data = await sessions.getCurrentSessionData() + + _.extend(existingSession, data) + validateLog.set({ + consoleProps: () => { + return { + Step: 'Validate Session', + Message: 'The following is the collected session data after the session was successfully validated:', + ...getConsoleProps(existingSession), + } + }, + }) + return isValidSession }) From 8888cd9e211e608f9bbac81478667b2877dab76e Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 12 Dec 2022 14:42:46 -0600 Subject: [PATCH 3/3] fix: add column, line, and method check to integrity check (#25094) --- .circleci/cache-version.txt | 2 +- .circleci/workflows.yml | 7 +- patches/bytenode+1.3.7.dev.patch | 124 ++++++++++++++++++ scripts/binary/binary-cleanup.js | 3 +- scripts/binary/binary-entry-point-source.js | 8 +- .../binary/binary-integrity-check-source.js | 44 ++++++- scripts/binary/binary-sources.js | 2 + scripts/binary/smoke.js | 18 +++ 8 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 patches/bytenode+1.3.7.dev.patch diff --git a/.circleci/cache-version.txt b/.circleci/cache-version.txt index ac501d1253cf..125dd7e97cdf 100644 --- a/.circleci/cache-version.txt +++ b/.circleci/cache-version.txt @@ -1,3 +1,3 @@ # Bump this version to force CI to re-create the cache from scratch. -12-05-22 +12-12-22 diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 1f72027568cf..a2de66d3c6ef 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -28,6 +28,7 @@ mainBuildFilters: &mainBuildFilters only: - develop - /^release\/\d+\.\d+\.\d+$/ + - 'ryanm/fix/column-line' # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -36,6 +37,7 @@ macWorkflowFilters: &darwin-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] + - equal: [ 'ryanm/fix/column-line', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -43,6 +45,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] + - equal: [ 'ryanm/fix/column-line', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -60,7 +63,7 @@ windowsWorkflowFilters: &windows-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'mschile/fix_windows_flake', << pipeline.git.branch >> ] + - equal: [ 'ryanm/fix/column-line', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -126,7 +129,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "ryanm/fix/issue-with-integrity-check" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "ryanm/fix/column-line" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi diff --git a/patches/bytenode+1.3.7.dev.patch b/patches/bytenode+1.3.7.dev.patch new file mode 100644 index 000000000000..ef1f3800ce8f --- /dev/null +++ b/patches/bytenode+1.3.7.dev.patch @@ -0,0 +1,124 @@ +diff --git a/node_modules/bytenode/lib/cli.js b/node_modules/bytenode/lib/cli.js +index 9f57493..3ba173e 100755 +--- a/node_modules/bytenode/lib/cli.js ++++ b/node_modules/bytenode/lib/cli.js +@@ -2,11 +2,15 @@ + + 'use strict'; + +-const fs = require('fs'); +-const path = require('path'); +-const wrap = require('module').wrap; +-const spawnSync = require('child_process').spawnSync; +-const bytenode = require('./index.js'); ++import fs from 'fs'; ++import path from 'path'; ++import * as mod from 'module'; ++import { spawnSync } from 'child_process'; ++import * as bytenode from './index.js'; ++import * as url from 'url'; ++ ++const __filename = url.fileURLToPath(import.meta.url); ++const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + + const args = process.argv.slice(2); + +@@ -100,7 +104,7 @@ if (program.flags.includes('--compile')) { + if (program.flags.includes('--no-module')) { + process.stdout.write(bytenode.compileCode(script)); + } else { +- process.stdout.write(bytenode.compileCode(wrap(script))); ++ process.stdout.write(bytenode.compileCode(mod.wrap(script))); + } + } catch (error) { + console.error(error); +@@ -136,13 +140,6 @@ if (program.flags.includes('--compile')) { + $ echo 'console.log("Hello");' | bytenode --compile - > hello.jsc + compile from stdin and save to \`hello.jsc\` + `); +-} else if (program.flags.includes('--version') && program.flags.length === 1 && program.files.length === 0) { +- const pkg = require('../package.json'); +- console.log(pkg.name, pkg.version); +- console.log('Node', process.versions.node); +- if (process.versions.electron) { +- console.log('Electron', process.versions.electron); +- } + } else { + try { + spawnSync(program.nodeBin, [ +diff --git a/node_modules/bytenode/lib/index.js b/node_modules/bytenode/lib/index.js +index cdd98cc..635bc27 100644 +--- a/node_modules/bytenode/lib/index.js ++++ b/node_modules/bytenode/lib/index.js +@@ -1,11 +1,13 @@ + 'use strict'; + +-const fs = require('fs'); +-const vm = require('vm'); +-const v8 = require('v8'); +-const path = require('path'); +-const Module = require('module'); +-const fork = require('child_process').fork; ++import fs from 'fs'; ++import vm from 'vm'; ++import v8 from 'v8'; ++import path from 'path'; ++import Module from 'module'; ++import { fork } from 'child_process'; ++import { createRequire } from 'module'; ++import * as url from 'url'; + + v8.setFlagsFromString('--no-lazy'); + +@@ -46,10 +48,12 @@ const compileElectronCode = function (javascriptCode) { + return new Promise((resolve, reject) => { + let data = Buffer.from([]); + ++ const require = createRequire(import.meta.url) + const electronPath = path.join(path.dirname(require.resolve('electron')), 'cli.js'); + if (!fs.existsSync(electronPath)) { + throw new Error('Electron not installed'); + } ++ const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + const bytenodePath = path.join(__dirname, 'cli.js'); + + // create a subprocess in which we run Electron as our Node and V8 engine +@@ -249,7 +253,7 @@ const runBytecodeFile = function (filename) { + return runBytecode(bytecodeBuffer); + }; + +-Module._extensions[COMPILED_EXTNAME] = function (fileModule, filename) { ++const runBytecodeAsModule = function (fileModule, filename) { + const bytecodeBuffer = fs.readFileSync(filename); + + fixBytecode(bytecodeBuffer); +@@ -333,14 +337,13 @@ const loaderCode = function (targetPath) { + `; + }; + +-global.bytenode = { ++export { + compileCode, + compileFile, + compileElectronCode, + runBytecode, + runBytecodeFile, + addLoaderFile, +- loaderCode ++ loaderCode, ++ runBytecodeAsModule, + }; +- +-module.exports = global.bytenode; +diff --git a/node_modules/bytenode/package.json b/node_modules/bytenode/package.json +index 6caff7c..72e2c46 100644 +--- a/node_modules/bytenode/package.json ++++ b/node_modules/bytenode/package.json +@@ -3,6 +3,7 @@ + "version": "1.3.7", + "description": "A minimalist bytecode compiler for Node.js", + "main": "lib/index.js", ++ "type": "module", + "bin": "lib/cli.js", + "types": "lib/index.d.ts", + "files": [ diff --git a/scripts/binary/binary-cleanup.js b/scripts/binary/binary-cleanup.js index 0b985fe1de9e..7df26766fdce 100644 --- a/scripts/binary/binary-cleanup.js +++ b/scripts/binary/binary-cleanup.js @@ -6,7 +6,6 @@ const esbuild = require('esbuild') const snapshotMetadata = require('@tooling/v8-snapshot/cache/prod-darwin/snapshot-meta.cache.json') const tempDir = require('temp-dir') const workingDir = path.join(tempDir, 'binary-cleanup-workdir') -const bytenode = require('bytenode') fs.ensureDirSync(workingDir) @@ -137,6 +136,8 @@ const createServerEntryPointBundle = async (buildAppDir) => { console.log(`compiling server entry point bundle to ${path.join(buildAppDir, 'packages', 'server', 'index.jsc')}`) // Use bytenode to compile the entry point bundle. This will save time on the v8 compile step and ensure the integrity of the entry point + const bytenode = await import('bytenode') + await bytenode.compileFile({ filename: path.join(buildAppDir, 'packages', 'server', 'index.js'), output: path.join(buildAppDir, 'packages', 'server', 'index.jsc'), diff --git a/scripts/binary/binary-entry-point-source.js b/scripts/binary/binary-entry-point-source.js index fcab4c31418c..4bccda2d553b 100644 --- a/scripts/binary/binary-entry-point-source.js +++ b/scripts/binary/binary-entry-point-source.js @@ -1,13 +1,13 @@ -const Module = require('module') -const path = require('path') +import Module from 'module' +import path from 'path' +import { runBytecodeAsModule } from 'bytenode' process.env.CYPRESS_INTERNAL_ENV = process.env.CYPRESS_INTERNAL_ENV || 'production' try { - require('bytenode') const filename = path.join(__dirname, 'packages', 'server', 'index.jsc') const dirname = path.dirname(filename) - Module._extensions['.jsc']({ + runBytecodeAsModule({ require: module.require, id: filename, filename, diff --git a/scripts/binary/binary-integrity-check-source.js b/scripts/binary/binary-integrity-check-source.js index 59f4a89cf98b..50a800a37570 100644 --- a/scripts/binary/binary-integrity-check-source.js +++ b/scripts/binary/binary-integrity-check-source.js @@ -2,6 +2,8 @@ const OrigError = Error const captureStackTrace = Error.captureStackTrace const toString = Function.prototype.toString const callFn = Function.call +const filter = Array.prototype.filter +const startsWith = String.prototype.startsWith const integrityErrorMessage = ` We detected an issue with the integrity of the Cypress binary. It may have been modified and cannot run. We recommend re-installing the Cypress binary with: @@ -22,7 +24,7 @@ const stackIntegrityCheck = function stackIntegrityCheck (options) { const tempError = new OrigError captureStackTrace(tempError, arguments.callee) - const stack = tempError.stack.filter((frame) => !frame.getFileName().startsWith('node:internal') && !frame.getFileName().startsWith('node:electron')) + const stack = filter.call(tempError.stack, (frame) => !startsWith.call(frame.getFileName(), 'node:internal') && !startsWith.call(frame.getFileName(), 'node:electron')) OrigError.prepareStackTrace = originalPrepareStackTrace OrigError.stackTraceLimit = originalStackTraceLimit @@ -33,9 +35,11 @@ const stackIntegrityCheck = function stackIntegrityCheck (options) { } for (let index = 0; index < options.stackToMatch.length; index++) { - const { functionName: expectedFunctionName, fileName: expectedFileName } = options.stackToMatch[index] + const { functionName: expectedFunctionName, fileName: expectedFileName, line: expectedLineNumber, column: expectedColumnNumber } = options.stackToMatch[index] const actualFunctionName = stack[index].getFunctionName() const actualFileName = stack[index].getFileName() + const actualColumnNumber = stack[index].getColumnNumber() + const actualLineNumber = stack[index].getLineNumber() if (expectedFunctionName && actualFunctionName !== expectedFunctionName) { console.error(`Integrity check failed with expected function name ${expectedFunctionName} but got ${actualFunctionName}`) @@ -46,13 +50,37 @@ const stackIntegrityCheck = function stackIntegrityCheck (options) { console.error(`Integrity check failed with expected file name ${expectedFileName} but got ${actualFileName}`) throw new Error(integrityErrorMessage) } + + if (expectedLineNumber && actualLineNumber !== expectedLineNumber) { + console.error(`Integrity check failed with expected line number ${expectedLineNumber} but got ${actualLineNumber}`) + throw new Error(integrityErrorMessage) + } + + if (expectedColumnNumber && actualColumnNumber !== expectedColumnNumber) { + console.error(`Integrity check failed with expected column number ${expectedColumnNumber} but got ${actualColumnNumber}`) + throw new Error(integrityErrorMessage) + } + } +} + +function validateStartsWith () { + if (startsWith.call !== callFn) { + console.error(`Integrity check failed for startsWith.call`) + throw new Error(integrityErrorMessage) + } +} + +function validateFilter () { + if (filter.call !== callFn) { + console.error(`Integrity check failed for filter.call`) + throw new Error(integrityErrorMessage) } } function validateToString () { if (toString.call !== callFn) { console.error(`Integrity check failed for toString.call`) - throw new Error('Integrity check failed for toString.call') + throw new Error(integrityErrorMessage) } } @@ -60,7 +88,7 @@ function validateElectron (electron) { // Hard coded function as this is electron code and there's not an easy way to get the function string at package time. If this fails on an updated version of electron, we'll need to update this. if (toString.call(electron.app.getAppPath) !== 'function getAppPath() { [native code] }') { console.error(`Integrity check failed for toString.call(electron.app.getAppPath)`) - throw new Error(`Integrity check failed for toString.call(electron.app.getAppPath)`) + throw new Error(integrityErrorMessage) } } @@ -106,6 +134,8 @@ function integrityCheck (options) { const crypto = require('crypto') // 1. Validate that the native functions we are using haven't been tampered with + validateStartsWith() + validateFilter() validateToString() validateElectron(electron) validateFs(fs) @@ -143,13 +173,17 @@ function integrityCheck (options) { fileName: 'evalmachine.', }, { - functionName: 'Module2._extensions.', + functionName: 'v', // eslint-disable-next-line no-undef fileName: [appPath, 'index.js'].join(PATH_SEP), + line: 1, + column: 2573, }, { // eslint-disable-next-line no-undef fileName: [appPath, 'index.js'].join(PATH_SEP), + line: 1, + column: 2764, }, ], }) diff --git a/scripts/binary/binary-sources.js b/scripts/binary/binary-sources.js index 2e9d1ade817a..e754ca050922 100644 --- a/scripts/binary/binary-sources.js +++ b/scripts/binary/binary-sources.js @@ -17,6 +17,8 @@ const getBinaryEntryPointSource = async () => { bundle: true, platform: 'node', write: false, + minify: true, + treeShaking: true, }) return esbuildResult.outputFiles[0].text diff --git a/scripts/binary/smoke.js b/scripts/binary/smoke.js index 063ad600ea24..879c5e68e5b9 100644 --- a/scripts/binary/smoke.js +++ b/scripts/binary/smoke.js @@ -302,6 +302,24 @@ const runIntegrityTest = async function (buildAppExecutable, buildAppDir, e2e) { const allowList = ['regeneratorRuntime', '__core-js_shared__', 'getSnapshotResult', 'supportTypeScript'] await testAlteringEntryPoint(`(${compareGlobals.toString()})()`, `extra keys in electron process: ${allowList}\nIntegrity check failed with expected stack length 9 but got 10`) + + const testTemporarilyRewritingEntryPoint = async () => { + const file = path.join(buildAppDir, 'index.js') + const backupFile = path.join(buildAppDir, 'index.js.bak') + const contents = await fs.readFile(file) + + // Backup state + await fs.move(file, backupFile) + + // Modify app + await fs.writeFile(file, `console.log("rewritten code");const fs=require('fs');const { join } = require('path');fs.writeFileSync(join(__dirname,'index.js'),fs.readFileSync(join(__dirname,'index.js.bak')));${contents}`) + await runErroringProjectTest(buildAppExecutable, e2e, 'temporarily rewriting index.js', 'Integrity check failed with expected column number 2573 but got') + + // Restore original state + await fs.move(backupFile, file, { overwrite: true }) + } + + await testTemporarilyRewritingEntryPoint() } const test = async function (buildAppExecutable, buildAppDir) {