diff --git a/.circleci/src/workflows/@workflows.yml b/.circleci/src/workflows/@workflows.yml index 281a3d88a6..2b864651a1 100644 --- a/.circleci/src/workflows/@workflows.yml +++ b/.circleci/src/workflows/@workflows.yml @@ -116,7 +116,7 @@ commands: name: Set environment variable to determine whether or not to persist artifacts command: | echo "Setting SHOULD_PERSIST_ARTIFACTS variable" - echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "chore/refactor_cli_to_ts" ]]; then + echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "fix/dbus-messages" ]]; then export SHOULD_PERSIST_ARTIFACTS=true fi' >> "$BASH_ENV" # You must run `setup_should_persist_artifacts` command and be using bash before running this command diff --git a/.circleci/src/workflows/workflows/@main.yml b/.circleci/src/workflows/workflows/@main.yml index 87a17d9318..688659e866 100644 --- a/.circleci/src/workflows/workflows/@main.yml +++ b/.circleci/src/workflows/workflows/@main.yml @@ -1,11 +1,11 @@ - linux-x64: when: &full-workflow-filters or: - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] + - equal: [ 'fix/dbus-messages', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 1d71d40a7b..09216ff78f 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -868,7 +868,7 @@ commands: - run: command: | echo "Setting SHOULD_PERSIST_ARTIFACTS variable" - echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "chore/refactor_cli_to_ts" ]]; then + echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "fix/dbus-messages" ]]; then export SHOULD_PERSIST_ARTIFACTS=true fi' >> "$BASH_ENV" name: Set environment variable to determine whether or not to persist artifacts @@ -3717,6 +3717,9 @@ workflows: - equal: - update-v8-snapshot-cache-on-develop - << pipeline.git.branch >> + - equal: + - fix/dbus-messages + - << pipeline.git.branch >> - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -3771,6 +3774,9 @@ workflows: - equal: - update-v8-snapshot-cache-on-develop - << pipeline.git.branch >> + - equal: + - fix/dbus-messages + - << pipeline.git.branch >> - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -3836,6 +3842,9 @@ workflows: - equal: - update-v8-snapshot-cache-on-develop - << pipeline.git.branch >> + - equal: + - fix/dbus-messages + - << pipeline.git.branch >> - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -4321,6 +4330,9 @@ workflows: - equal: - update-v8-snapshot-cache-on-develop - << pipeline.git.branch >> + - equal: + - fix/dbus-messages + - << pipeline.git.branch >> - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -5156,6 +5168,9 @@ workflows: - equal: - update-v8-snapshot-cache-on-develop - << pipeline.git.branch >> + - equal: + - fix/dbus-messages + - << pipeline.git.branch >> - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 547f1d3b5c..caf49bf8be 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,4 +1,12 @@ +## 15.3.1 + +_Released 10/07/2025 (PENDING)_ + +**Bugfixes:** + +- Fixed a regression introduced in [`15.0.0`](https://docs.cypress.io/guides/references/changelog#15-0-0) where `dbus` connection error messages appear in docker containers when launching Cypress. Fixes [#32290](https://github.com/cypress-io/cypress/issues/32290). + ## 15.3.0 _Released 9/23/2025_ diff --git a/cli/lib/exec/info.ts b/cli/lib/exec/info.ts index 953527c77d..d418271874 100644 --- a/cli/lib/exec/info.ts +++ b/cli/lib/exec/info.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import spawn from './spawn' +import { start as spawnStart } from './spawn' import util from '../util' import state from '../tasks/state' import os from 'os' @@ -47,7 +47,7 @@ const formatCypressVariables = (): any => { methods.start = async (options: any = {}): Promise => { const args = ['--mode=info'] - await spawn.start(args, { + await spawnStart(args, { dev: options.dev, }) diff --git a/cli/lib/exec/open.ts b/cli/lib/exec/open.ts index ecf5d47b6f..2cd5d70d04 100644 --- a/cli/lib/exec/open.ts +++ b/cli/lib/exec/open.ts @@ -1,6 +1,6 @@ import Debug from 'debug' import util from '../util' -import spawn from './spawn' +import { start as spawnStart } from './spawn' import { start as verifyStart } from '../tasks/verify' import { processTestingType, checkConfigFile } from './shared' import { exitWithError } from '../errors' @@ -79,7 +79,7 @@ export const start = async (options: any = {}): Promise => { try { const args = processOpenOptions(options) - return spawn.start(args, { + return spawnStart(args, { dev: options.dev, detached: Boolean(options.detached), }) diff --git a/cli/lib/exec/run.ts b/cli/lib/exec/run.ts index dc1704ec1f..577118aed5 100644 --- a/cli/lib/exec/run.ts +++ b/cli/lib/exec/run.ts @@ -1,7 +1,7 @@ import _ from 'lodash' import Debug from 'debug' import util from '../util' -import spawn from './spawn' +import { start as spawnStart } from './spawn' import { start } from '../tasks/verify' import { exitWithError, errors } from '../errors' import { processTestingType, throwInvalidOptionError, checkConfigFile } from './shared' @@ -179,7 +179,7 @@ const runModule = { debug('run to spawn.start args %j', args) - return spawn.start(args, { + return spawnStart(args, { dev: options.dev, }) } catch (err: any) { diff --git a/cli/lib/exec/spawn.ts b/cli/lib/exec/spawn.ts index a3dcb0f1f0..6efec62e21 100644 --- a/cli/lib/exec/spawn.ts +++ b/cli/lib/exec/spawn.ts @@ -14,6 +14,8 @@ import { stdin, stdout, stderr } from 'process' const debug = Debug('cypress:cli') +const DBUS_ERROR_PATTERN = /ERROR:dbus\/(bus|object_proxy)\.cc/ + function isPlatform (platform: string): boolean { return os.platform() === platform } @@ -32,7 +34,7 @@ function needsEverythingPipedDirectly (): boolean { return isPlatform('win32') } -function getStdio (needsXvfb: boolean): any { +function getStdioStrategy (needsXvfb: boolean): string | string[] { if (needsEverythingPipedDirectly()) { return 'pipe' } @@ -50,260 +52,272 @@ function getStdio (needsXvfb: boolean): any { return 'inherit' } -const spawnModule = { - async start (args: any, options: any = {}): Promise { - const needsXvfb = xvfb.isNeeded() - let executable = state.getPathToExecutable(state.getBinaryDir()) +function createSpawnFunction ( + executable: string, + args: string[], + options: any, +) { + return (overrides: any = {}): any => { + return new Bluebird((resolve: any, reject: any) => { + _.defaults(overrides, { + onStderrData: false, + }) - if (util.getEnv('CYPRESS_RUN_BINARY')) { - executable = path.resolve(util.getEnv('CYPRESS_RUN_BINARY') as string) - } + const { onStderrData } = overrides + const envOverrides = util.getEnvOverrides(options) + const electronArgs: string[] = [] + const node11WindowsFix = isPlatform('win32') - debug('needs to start own Xvfb?', needsXvfb) + let startScriptPath: string | undefined - // Always push cwd into the args - // which additionally acts as a signal to the - // binary that it was invoked through the NPM module - args = args || [] - if (typeof args === 'string') { - args = [args] - } + if (options.dev) { + executable = 'node' + // if we're in dev then reset + // the launch cmd to be 'npm run dev' + startScriptPath = path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js') - args = [...args, '--cwd', process.cwd(), '--userNodePath', process.execPath, '--userNodeVersion', process.versions.node] + debug('in dev mode the args became %o', args) + } - _.defaults(options, { - dev: false, - env: process.env, - detached: false, - stdio: getStdio(needsXvfb), - }) + if (!options.dev && needsSandbox()) { + electronArgs.push('--no-sandbox') + } - const spawn = (overrides: any = {}): any => { - return new Bluebird((resolve: any, reject: any) => { - _.defaults(overrides, { - onStderrData: false, - }) - - const { onStderrData } = overrides - const envOverrides = util.getEnvOverrides(options) - const electronArgs: string[] = [] - const node11WindowsFix = isPlatform('win32') - - let startScriptPath: string | undefined - - if (options.dev) { - executable = 'node' - // if we're in dev then reset - // the launch cmd to be 'npm run dev' - startScriptPath = path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js') - - debug('in dev mode the args became %o', args) - } - - if (!options.dev && needsSandbox()) { - electronArgs.push('--no-sandbox') - } - - // strip dev out of child process options - /** + // strip dev out of child process options + /** * @type {import('child_process').ForkOptions} */ - let stdioOptions: any = _.pick(options, 'env', 'detached', 'stdio') + let stdioOptions: any = _.pick(options, 'env', 'detached', 'stdio') - // figure out if we're going to be force enabling or disabling colors. - // also figure out whether we should force stdout and stderr into thinking - // it is a tty as opposed to a pipe. - stdioOptions.env = _.extend({}, stdioOptions.env, envOverrides) + // figure out if we're going to be force enabling or disabling colors. + // also figure out whether we should force stdout and stderr into thinking + // it is a tty as opposed to a pipe. + stdioOptions.env = _.extend({}, stdioOptions.env, envOverrides) - if (node11WindowsFix) { - stdioOptions = _.extend({}, stdioOptions, { windowsHide: false }) - } + if (node11WindowsFix) { + stdioOptions = _.extend({}, stdioOptions, { windowsHide: false }) + } - if (util.isPossibleLinuxWithIncorrectDisplay()) { - // make sure we use the latest DISPLAY variable if any - debug('passing DISPLAY', process.env.DISPLAY) - stdioOptions.env.DISPLAY = process.env.DISPLAY - } + if (util.isPossibleLinuxWithIncorrectDisplay()) { + // make sure we use the latest DISPLAY variable if any + debug('passing DISPLAY', process.env.DISPLAY) + stdioOptions.env.DISPLAY = process.env.DISPLAY + } - if (stdioOptions.env.ELECTRON_RUN_AS_NODE) { - // Since we are running electron as node, we need to add an entry point file. - startScriptPath = path.join(state.getBinaryPkgPath(path.dirname(executable)), '..', 'index.js') - } else { - // Start arguments with "--" so Electron knows these are OUR - // arguments and does not try to sanitize them. Otherwise on Windows - // an url in one of the arguments crashes it :( - // https://github.com/cypress-io/cypress/issues/5466 - args = [...electronArgs, '--', ...args] - } + if (stdioOptions.env.ELECTRON_RUN_AS_NODE) { + // Since we are running electron as node, we need to add an entry point file. + startScriptPath = path.join(state.getBinaryPkgPath(path.dirname(executable)), '..', 'index.js') + } else { + // Start arguments with "--" so Electron knows these are OUR + // arguments and does not try to sanitize them. Otherwise on Windows + // an url in one of the arguments crashes it :( + // https://github.com/cypress-io/cypress/issues/5466 + args = [...electronArgs, '--', ...args] + } - if (startScriptPath) { - args.unshift(startScriptPath) - } + if (startScriptPath) { + args.unshift(startScriptPath) + } - if (process.env.CYPRESS_INTERNAL_DEV_DEBUG) { - args.unshift(process.env.CYPRESS_INTERNAL_DEV_DEBUG) - } + if (process.env.CYPRESS_INTERNAL_DEV_DEBUG) { + args.unshift(process.env.CYPRESS_INTERNAL_DEV_DEBUG) + } - debug('spawn args %o %o', args, _.omit(stdioOptions, 'env')) - debug('spawning Cypress with executable: %s', executable) + debug('spawn args %o %o', args, _.omit(stdioOptions, 'env')) + debug('spawning Cypress with executable: %s', executable) - const child = cp.spawn(executable, args, stdioOptions) + const child = cp.spawn(executable, args, stdioOptions) - function resolveOn (event: any): any { - return async function (code: any, signal: any): Promise { - debug('child event fired %o', { event, code, signal }) + function resolveOn (event: any): any { + return async function (code: any, signal: any): Promise { + debug('child event fired %o', { event, code, signal }) - if (code === null) { - const errorObject = errors.childProcessKilled(event, signal) + if (code === null) { + const errorObject = errors.childProcessKilled(event, signal) - const err = await getError(errorObject) + const err = await getError(errorObject) - return reject(err) - } - - resolve(code) + return reject(err) } + + resolve(code) } + } - child.on('close', resolveOn('close')) - child.on('exit', resolveOn('exit')) - child.on('error', reject) + child.on('close', resolveOn('close')) + child.on('exit', resolveOn('exit')) + child.on('error', reject) - if (isPlatform('win32')) { - const rl = readline.createInterface({ - input: stdin, - output: stdout, - }) + if (isPlatform('win32')) { + const rl = readline.createInterface({ + input: stdin, + output: stdout, + }) - // on windows, SIGINT does not propagate to the child process when ctrl+c is pressed - // this makes sure all nested processes are closed(ex: firefox inside the server) - rl.on('SIGINT', async function () { - const kill = (await import('tree-kill')).default + // on windows, SIGINT does not propagate to the child process when ctrl+c is pressed + // this makes sure all nested processes are closed(ex: firefox inside the server) + rl.on('SIGINT', async function () { + const kill = (await import('tree-kill')).default - kill(child.pid as number, 'SIGINT') - }) - } + kill(child.pid as number, 'SIGINT') + }) + } - // if stdio options is set to 'pipe', then - // we should set up pipes: - // process STDIN (read stream) => child STDIN (writeable) - // child STDOUT => process STDOUT - // child STDERR => process STDERR with additional filtering - if (child.stdin) { - debug('piping process STDIN into child STDIN') - stdin.pipe(child.stdin) - } + // if stdio options is set to 'pipe', then + // we should set up pipes: + // process STDIN (read stream) => child STDIN (writeable) + // child STDOUT => process STDOUT + // child STDERR => process STDERR with additional filtering + if (child.stdin) { + debug('piping process STDIN into child STDIN') + stdin.pipe(child.stdin) + } - if (child.stdout) { - debug('piping child STDOUT to process STDOUT') - child.stdout.pipe(stdout) - } + if (child.stdout) { + debug('piping child STDOUT to process STDOUT') + child.stdout.pipe(stdout) + } - // if this is defined then we are manually piping for linux - // to filter out the garbage - if (child.stderr) { - debug('piping child STDERR to process STDERR') - child.stderr.on('data', (data: any) => { - const str = data.toString() + // if this is defined then we are manually piping for linux + // to filter out the garbage + if (child.stderr) { + debug('piping child STDERR to process STDERR') + child.stderr.on('data', (data: any) => { + const str = data.toString() - // if we have a callback and this explicitly returns - // false then bail - if (onStderrData && onStderrData(str)) { - return - } - - // else pass it along! - stderr.write(data) - }) - } - - // https://github.com/cypress-io/cypress/issues/1841 - // https://github.com/cypress-io/cypress/issues/5241 - // In some versions of node, it will throw on windows - // when you close the parent process after piping - // into the child process. unpiping does not seem - // to have any effect. so we're just catching the - // error here and not doing anything. - stdin.on('error', (err: any) => { - if (['EPIPE', 'ENOTCONN'].includes(err.code)) { + // if we have a callback and this explicitly returns + // false then bail + if (onStderrData && onStderrData(str)) { return } - throw err + if (str.match(DBUS_ERROR_PATTERN)) { + debug(str) + } else { + // else pass it along! + stderr.write(data) + } }) + } - if (stdioOptions.detached) { - child.unref() + // https://github.com/cypress-io/cypress/issues/1841 + // https://github.com/cypress-io/cypress/issues/5241 + // In some versions of node, it will throw on windows + // when you close the parent process after piping + // into the child process. unpiping does not seem + // to have any effect. so we're just catching the + // error here and not doing anything. + stdin.on('error', (err: any) => { + if (['EPIPE', 'ENOTCONN'].includes(err.code)) { + return } + + throw err }) - } - const spawnInXvfb = async (): Promise => { - try { - await xvfb.start() - - const code = await userFriendlySpawn() - - return code - } finally { - await xvfb.stop() + if (stdioOptions.detached) { + child.unref() } - } - - const userFriendlySpawn = async (linuxWithDisplayEnv?: any): Promise => { - debug('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv)) - - let brokenGtkDisplay: boolean = false - - const overrides: any = {} - - if (linuxWithDisplayEnv) { - _.extend(overrides, { - electronLogging: true, - onStderrData (str: string): any { - // if we receive a broken pipe anywhere - // then we know that's why cypress exited early - if (util.isBrokenGtkDisplay(str)) { - brokenGtkDisplay = true - } - }, - }) - } - - try { - const code: number = await spawn(overrides) - - if (code !== 0 && brokenGtkDisplay) { - util.logBrokenGtkDisplayWarning() - - return spawnInXvfb() - } - - return code - } catch (error: any) { - // we can format and handle an error message from the code above - // prevent wrapping error again by using "known: undefined" filter - if ((error as any).known === undefined) { - const raiseErrorFn = throwFormErrorText(errors.unexpected) - - await raiseErrorFn(error.message) - } - - throw error - } - } - - if (needsXvfb) { - return spawnInXvfb() - } - - // if we are on linux and there's already a DISPLAY - // set, then we may need to rerun cypress after - // spawning our own Xvfb server - const linuxWithDisplayEnv = util.isPossibleLinuxWithIncorrectDisplay() - - return userFriendlySpawn(linuxWithDisplayEnv) - }, + }) + } } -export default spawnModule +async function spawnInXvfb (spawn: ReturnType): Promise { + try { + await xvfb.start() + + const code = await userFriendlySpawn(spawn) + + return code + } finally { + await xvfb.stop() + } +} + +async function userFriendlySpawn (spawn: ReturnType, linuxWithDisplayEnv?: any): Promise { + debug('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv)) + + let brokenGtkDisplay: boolean = false + + const overrides: any = {} + + if (linuxWithDisplayEnv) { + _.extend(overrides, { + electronLogging: true, + onStderrData (str: string): any { + // if we receive a broken pipe anywhere + // then we know that's why cypress exited early + if (util.isBrokenGtkDisplay(str)) { + brokenGtkDisplay = true + } + }, + }) + } + + try { + const code: number = await spawn(overrides) + + if (code !== 0 && brokenGtkDisplay) { + util.logBrokenGtkDisplayWarning() + + return spawnInXvfb(spawn) + } + + return code + } catch (error: any) { + // we can format and handle an error message from the code above + // prevent wrapping error again by using "known: undefined" filter + if ((error as any).known === undefined) { + const raiseErrorFn = throwFormErrorText(errors.unexpected) + + await raiseErrorFn(error.message) + } + + throw error + } +} + +interface StartOptions { + dev?: boolean + env?: Record + detached?: boolean + stdio?: string | string[] +} + +export async function start (args: string | string[], options: StartOptions = {}): Promise { + let executable = util.getEnv('CYPRESS_RUN_BINARY') ? + path.resolve(util.getEnv('CYPRESS_RUN_BINARY') as string) : + state.getPathToExecutable(state.getBinaryDir()) + + // Always push cwd into the args + // which additionally acts as a signal to the + // binary that it was invoked through the NPM module + const baseArgs = args ? (typeof args === 'string' ? [args] : args) : [] + const decoratedArgs = baseArgs.concat([ + '--cwd', process.cwd(), + '--userNodePath', process.execPath, + '--userNodeVersion', process.versions.node, + ]) + + const needsXvfb = xvfb.isNeeded() + + debug('needs to start own Xvfb?', needsXvfb) + + const stdio = options.stdio ?? getStdioStrategy(needsXvfb) + const dev = options.dev ?? false + const detached = options.detached ?? false + const env = options.env ?? process.env + + const spawn = createSpawnFunction(executable, decoratedArgs, { stdio, dev, detached, env }) + + if (needsXvfb) { + return spawnInXvfb(spawn) + } + + // if we are on linux and there's already a DISPLAY + // set, then we may need to rerun cypress after + // spawning our own Xvfb server + const linuxWithDisplayEnv = util.isPossibleLinuxWithIncorrectDisplay() + + return userFriendlySpawn(spawn, linuxWithDisplayEnv) +} diff --git a/cli/test/lib/exec/info.spec.ts b/cli/test/lib/exec/info.spec.ts index ef6590c86a..25ab3e2f5d 100644 --- a/cli/test/lib/exec/info.spec.ts +++ b/cli/test/lib/exec/info.spec.ts @@ -6,7 +6,7 @@ import si, { Systeminformation } from 'systeminformation' import util from '../../../lib/util' import state from '../../../lib/tasks/state' import info from '../../../lib/exec/info' -import spawn from '../../../lib/exec/spawn' +import { start as spawnStart } from '../../../lib/exec/spawn' vi.mock('os', async (importActual) => { const actual = await importActual() @@ -34,15 +34,9 @@ vi.mock('systeminformation', async (importActual) => { } }) -vi.mock('../../../lib/exec/spawn', async (importActual) => { - const actual = await importActual() - +vi.mock('../../../lib/exec/spawn', async () => { return { - default: { - // @ts-expect-error - ...actual.default, - start: vi.fn(), - }, + start: vi.fn(), } }) @@ -105,7 +99,7 @@ describe('exec info', () => { vi.stubEnv('NO_PROXY', undefined) vi.stubEnv('CYPRESS_COMMERCIAL_RECOMMENDATIONS', undefined) // common stubs - vi.mocked(spawn.start).mockResolvedValue(null) + vi.mocked(spawnStart).mockResolvedValue(null) vi.mocked(os.platform).mockReturnValue('linux') vi.mocked(os.totalmem).mockReturnValue(1.2e+9) vi.mocked(os.freemem).mockReturnValue(4e+8) @@ -142,7 +136,7 @@ describe('exec info', () => { expect(output()).toMatchSnapshot('cypress info without browsers or vars') - expect(spawn.start).toBeCalledWith(['--mode=info'], { dev: undefined }) + expect(spawnStart).toBeCalledWith(['--mode=info'], { dev: undefined }) }) it('prints proxy and cypress env vars', async () => { diff --git a/cli/test/lib/exec/open.spec.ts b/cli/test/lib/exec/open.spec.ts index cb32dd0b51..9317fb996a 100644 --- a/cli/test/lib/exec/open.spec.ts +++ b/cli/test/lib/exec/open.spec.ts @@ -1,7 +1,7 @@ import { vi, describe, it, beforeEach, expect } from 'vitest' import util from '../../../lib/util' import { start as verifyStart } from '../../../lib/tasks/verify' -import spawn from '../../../lib/exec/spawn' +import { start as spawnStart } from '../../../lib/exec/spawn' import open from '../../../lib/exec/open' vi.mock('../../../lib/util', async (importActual) => { @@ -16,15 +16,9 @@ vi.mock('../../../lib/util', async (importActual) => { } }) -vi.mock('../../../lib/exec/spawn', async (importActual) => { - const actual = await importActual() - +vi.mock('../../../lib/exec/spawn', async () => { return { - default: { - // @ts-expect-error - ...actual.default, - start: vi.fn(), - }, + start: vi.fn(), } }) @@ -42,7 +36,7 @@ describe('exec open', function () { vi.mocked(util.isInstalledGlobally).mockReturnValue(true) vi.mocked(verifyStart).mockResolvedValue(undefined) - vi.mocked(spawn.start).mockResolvedValue(undefined) + vi.mocked(spawnStart).mockResolvedValue(undefined) }) it('verifies download', async () => { @@ -52,7 +46,7 @@ describe('exec open', function () { it('calls spawn with correct options', async () => { await open.start({ dev: true }) - expect(spawn.start).toHaveBeenCalledWith([], { + expect(spawnStart).toHaveBeenCalledWith([], { detached: false, dev: true, }) @@ -60,12 +54,12 @@ describe('exec open', function () { it('spawns with port', async () => { await open.start({ port: '1234' }) - expect(spawn.start).toHaveBeenCalledWith(['--port', '1234'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--port', '1234'], expect.anything()) }) it('spawns with --env', async () => { await open.start({ env: 'host=http://localhost:1337,name=brian' }) - expect(spawn.start).toHaveBeenCalledWith( + expect(spawnStart).toHaveBeenCalledWith( ['--env', 'host=http://localhost:1337,name=brian'], expect.anything(), ) @@ -73,7 +67,7 @@ describe('exec open', function () { it('spawns with --config', async () => { await open.start({ config: 'watchForFileChanges=false,baseUrl=localhost' }) - expect(spawn.start).toHaveBeenCalledWith( + expect(spawnStart).toHaveBeenCalledWith( ['--config', 'watchForFileChanges=false,baseUrl=localhost'], expect.anything(), ) @@ -81,7 +75,7 @@ describe('exec open', function () { it('spawns with --config-file set', async () => { await open.start({ configFile: 'special-cypress.config.js' }) - expect(spawn.start).toHaveBeenCalledWith( + expect(spawnStart).toHaveBeenCalledWith( ['--config-file', 'special-cypress.config.js'], expect.anything(), ) @@ -91,14 +85,14 @@ describe('exec open', function () { vi.mocked(util.isInstalledGlobally).mockReturnValue(false) await open.start() - expect(spawn.start).toHaveBeenCalledWith(['--project', process.cwd()], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--project', process.cwd()], expect.anything()) }) it('spawns without --project if not installed globally and passing --global option', async () => { vi.mocked(util.isInstalledGlobally).mockReturnValue(false) await open.start({ global: true }) - expect(spawn.start).not.toHaveBeenCalledWith( + expect(spawnStart).not.toHaveBeenCalledWith( ['--project', process.cwd()], ) }) @@ -107,7 +101,7 @@ describe('exec open', function () { vi.mocked(util.isInstalledGlobally).mockReturnValue(false) await open.start({ project: '/path/to/project' }) - expect(spawn.start).toHaveBeenCalledWith( + expect(spawnStart).toHaveBeenCalledWith( ['--project', '/path/to/project'], expect.anything(), ) @@ -115,7 +109,7 @@ describe('exec open', function () { it('spawns with --project if specified and installed globally', async () => { await open.start({ project: '/path/to/project' }) - expect(spawn.start).toHaveBeenCalledWith( + expect(spawnStart).toHaveBeenCalledWith( ['--project', '/path/to/project'], expect.anything(), ) @@ -123,22 +117,22 @@ describe('exec open', function () { it('spawns without --project if not specified and installed globally', async () => { await open.start() - expect(spawn.start).toHaveBeenCalledWith([], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith([], expect.anything()) }) it('spawns without --testing-type when not specified', async () => { await open.start() - expect(spawn.start).toHaveBeenCalledWith([], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith([], expect.anything()) }) it('spawns with --testing-type e2e', async () => { await open.start({ testingType: 'e2e' }) - expect(spawn.start).toHaveBeenCalledWith(['--testing-type', 'e2e'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--testing-type', 'e2e'], expect.anything()) }) it('spawns with --testing-type component', async () => { await open.start({ testingType: 'component' }) - expect(spawn.start).toHaveBeenCalledWith(['--testing-type', 'component'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--testing-type', 'component'], expect.anything()) }) it('throws if --testing-type is invalid', () => { diff --git a/cli/test/lib/exec/run.spec.ts b/cli/test/lib/exec/run.spec.ts index 6c57ebf2a5..da70596707 100644 --- a/cli/test/lib/exec/run.spec.ts +++ b/cli/test/lib/exec/run.spec.ts @@ -2,7 +2,7 @@ import { vi, describe, it, beforeEach, expect } from 'vitest' import os from 'os' import util from '../../../lib/util' import run from '../../../lib/exec/run' -import spawn from '../../../lib/exec/spawn' +import { start as spawnStart } from '../../../lib/exec/spawn' import { start as verifyStart } from '../../../lib/tasks/verify' vi.mock('os', async (importActual) => { @@ -29,15 +29,9 @@ vi.mock('../../../lib/util', async (importActual) => { } }) -vi.mock('../../../lib/exec/spawn', async (importActual) => { - const actual = await importActual() - +vi.mock('../../../lib/exec/spawn', async () => { return { - default: { - // @ts-expect-error - ...actual.default, - start: vi.fn(), - }, + start: vi.fn(), } }) @@ -152,7 +146,7 @@ describe('exec run', () => { describe('.start', () => { beforeEach(() => { - vi.mocked(spawn.start).mockResolvedValue(undefined) + vi.mocked(spawnStart).mockResolvedValue(undefined) vi.mocked(verifyStart).mockResolvedValue(undefined) }) @@ -163,77 +157,77 @@ describe('exec run', () => { it('spawns with --key and xvfb', async () => { await run.start({ port: '1234' }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--port', '1234'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--port', '1234'], expect.anything()) }) it('spawns with --env', async () => { await run.start({ env: 'host=http://localhost:1337,name=brian' }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--env', 'host=http://localhost:1337,name=brian'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--env', 'host=http://localhost:1337,name=brian'], expect.anything()) }) it('spawns with --config', async () => { await run.start({ config: 'watchForFileChanges=false,baseUrl=localhost' }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--config', 'watchForFileChanges=false,baseUrl=localhost'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--config', 'watchForFileChanges=false,baseUrl=localhost'], expect.anything()) }) it('spawns with --config-file set', async () => { await run.start({ configFile: 'special-cypress.config.js' }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--config-file', 'special-cypress.config.js'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--config-file', 'special-cypress.config.js'], expect.anything()) }) it('spawns with --record false', async () => { await run.start({ record: false }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--record', false], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--record', false], expect.anything()) }) it('spawns with --headed true', async () => { await run.start({ headed: true }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--headed', true], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--headed', true], expect.anything()) }) it('spawns with --no-exit', async () => { await run.start({ exit: false }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--no-exit'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--no-exit'], expect.anything()) }) it('spawns with --output-path', async () => { await run.start({ outputPath: '/path/to/output' }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--output-path', '/path/to/output'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--output-path', '/path/to/output'], expect.anything()) }) it('spawns with --testing-type e2e when given --e2e', async () => { await run.start({ e2e: true }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--testing-type', 'e2e'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--testing-type', 'e2e'], expect.anything()) }) it('spawns with --testing-type component when given --component', async () => { await run.start({ component: true }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--testing-type', 'component'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--testing-type', 'component'], expect.anything()) }) it('spawns with --tag value', async () => { await run.start({ tag: 'nightly' }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--tag', 'nightly'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--tag', 'nightly'], expect.anything()) }) it('spawns with several --tag words unchanged', async () => { await run.start({ tag: 'nightly, sanity' }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--tag', 'nightly, sanity'], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--tag', 'nightly, sanity'], expect.anything()) }) it('spawns with --auto-cancel-after-failures value', async () => { await run.start({ autoCancelAfterFailures: 4 }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--auto-cancel-after-failures', 4], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--auto-cancel-after-failures', 4], expect.anything()) }) it('spawns with --auto-cancel-after-failures value false', async () => { await run.start({ autoCancelAfterFailures: false }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--auto-cancel-after-failures', false], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--auto-cancel-after-failures', false], expect.anything()) }) it('spawns with --runner-ui', async () => { await run.start({ runnerUi: true }) - expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--runner-ui', true], expect.anything()) + expect(spawnStart).toHaveBeenCalledWith(['--run-project', process.cwd(), '--runner-ui', true], expect.anything()) }) }) }) diff --git a/cli/test/lib/exec/spawn.spec.ts b/cli/test/lib/exec/spawn.spec.ts index f95e0f1328..00dde0b4ef 100644 --- a/cli/test/lib/exec/spawn.spec.ts +++ b/cli/test/lib/exec/spawn.spec.ts @@ -12,7 +12,7 @@ import { stdin, stdout, stderr } from 'process' import state from '../../../lib/tasks/state' import xvfb from '../../../lib/exec/xvfb' -import spawn from '../../../lib/exec/spawn' +import { start } from '../../../lib/exec/spawn' import { needsSandbox } from '../../../lib/tasks/verify' import util from '../../../lib/util' @@ -240,7 +240,7 @@ describe('lib/exec/spawn', function () { vi.mocked(needsSandbox).mockReturnValue(false) // start the process - const startPromise = spawn.start('--foo', { foo: 'bar' }) + const startPromise = start('--foo', { foo: 'bar' }) // simulate the process closing successfully spawnedProcess.emit('close', 0) @@ -266,7 +266,7 @@ describe('lib/exec/spawn', function () { it('uses --no-sandbox when needed', async function () { vi.mocked(needsSandbox).mockReturnValue(true) - const startPromise = spawn.start('--foo', { foo: 'bar' }) + const startPromise = start('--foo', { foo: 'bar' }) spawnedProcess.emit('close', 0) @@ -297,7 +297,7 @@ describe('lib/exec/spawn', function () { it('uses npm command when running in dev mode', async () => { vi.mocked(needsSandbox).mockReturnValue(false) - const startPromise = spawn.start('--foo', { dev: true, foo: 'bar' }) + const startPromise = start('--foo', { dev: true, foo: 'bar' }) spawnedProcess.emit('close', 0) @@ -324,7 +324,7 @@ describe('lib/exec/spawn', function () { it('does not pass --no-sandbox when running in dev mode', async function () { vi.mocked(needsSandbox).mockReturnValue(true) - const startPromise = spawn.start('--foo', { dev: true, foo: 'bar' }) + const startPromise = start('--foo', { dev: true, foo: 'bar' }) spawnedProcess.emit('close', 0) @@ -351,7 +351,7 @@ describe('lib/exec/spawn', function () { it('starts xvfb when needed', async () => { vi.mocked(xvfb.isNeeded).mockReturnValue(true) - const startPromise = spawn.start('--foo') + const startPromise = start('--foo') await flushPromises() @@ -365,7 +365,7 @@ describe('lib/exec/spawn', function () { describe('closes', function () { ['close', 'exit'].forEach((event) => { it(`if '${event}' event fired`, async () => { - const startPromise = spawn.start('--foo') + const startPromise = start('--foo') spawnedProcess.emit(event, 0) @@ -376,7 +376,7 @@ describe('lib/exec/spawn', function () { }) it('if exit event fired and close event fired', async () => { - const startPromise = spawn.start('--foo') + const startPromise = start('--foo') spawnedProcess.emit('exit', 0) spawnedProcess.emit('close', 0) @@ -390,7 +390,7 @@ describe('lib/exec/spawn', function () { describe('detects kill signal', async () => { it('exits with error on SIGKILL', async () => { try { - const startPromise = spawn.start('--foo') + const startPromise = start('--foo') spawnedProcess.emit('exit', null, 'SIGKILL') @@ -405,7 +405,7 @@ describe('lib/exec/spawn', function () { }) it('does not start xvfb when its not needed', async () => { - const startPromise = spawn.start('--foo') + const startPromise = start('--foo') await flushPromises() @@ -419,7 +419,7 @@ describe('lib/exec/spawn', function () { it('stops xvfb when spawn closes', async () => { vi.mocked(xvfb.isNeeded).mockReturnValue(true) - const startPromise = spawn.start('--foo') + const startPromise = start('--foo') await flushPromises() @@ -431,7 +431,7 @@ describe('lib/exec/spawn', function () { }) it('resolves with spawned close code in the message', async () => { - const startPromise = spawn.start('--foo') + const startPromise = start('--foo') spawnedProcess.emit('close', 10) @@ -455,7 +455,7 @@ describe('lib/exec/spawn', function () { vi.mocked(os.platform).mockReturnValue('linux') - const startPromise = spawn.start('--foo') + const startPromise = start('--foo') // mock display error due to missing display spawnedProcess.emit('close', 1) @@ -478,7 +478,7 @@ describe('lib/exec/spawn', function () { it('rejects with error from spawn', async () => { const msg = 'the error message' - const startPromise = spawn.start('--foo') + const startPromise = start('--foo') spawnedProcess.emit('error', new Error(msg)) @@ -493,7 +493,7 @@ describe('lib/exec/spawn', function () { }) it('unrefs if options.detached is true', async () => { - const startPromise = spawn.start(null, { detached: true }) + const startPromise = start(null, { detached: true }) spawnedProcess.emit('close', 0) @@ -504,7 +504,7 @@ describe('lib/exec/spawn', function () { it('does not unref by default', async () => { // @ts-expect-error - invalid number of arguments for given type - const startPromise = spawn.start() + const startPromise = start() spawnedProcess.emit('close', 0) @@ -517,7 +517,7 @@ describe('lib/exec/spawn', function () { vi.stubEnv('FOO', 'bar') // @ts-expect-error - invalid number of arguments for given type - const startPromise = spawn.start() + const startPromise = start() spawnedProcess.emit('close', 0) @@ -533,7 +533,7 @@ describe('lib/exec/spawn', function () { vi.mocked(util.supportsColor).mockReturnValue(true) vi.mocked(tty.isatty).mockReturnValue(true) - const startPromise = spawn.start([], { env: {} }) + const startPromise = start([], { env: {} }) spawnedProcess.emit('close', 0) @@ -548,7 +548,7 @@ describe('lib/exec/spawn', function () { it('sets windowsHide:false property in windows', async () => { vi.mocked(os.platform).mockReturnValue('win32') - const startPromise = spawn.start([], { env: {} }) + const startPromise = start([], { env: {} }) spawnedProcess.emit('close', 0) @@ -564,7 +564,7 @@ describe('lib/exec/spawn', function () { spawnedProcess.pid = 7 vi.mocked(os.platform).mockReturnValue('win32') - const startPromise = spawn.start([], { env: {} }) + const startPromise = start([], { env: {} }) spawnedProcess.emit('close', 0) @@ -578,7 +578,7 @@ describe('lib/exec/spawn', function () { }) it('does not set windowsHide property when in darwin', async () => { - const startPromise = spawn.start([], { env: {} }) + const startPromise = start([], { env: {} }) spawnedProcess.emit('close', 0) @@ -594,7 +594,7 @@ describe('lib/exec/spawn', function () { vi.mocked(util.supportsColor).mockReturnValue(false) vi.mocked(tty.isatty).mockReturnValue(false) - const startPromise = spawn.start([], { env: {} }) + const startPromise = start([], { env: {} }) spawnedProcess.emit('close', 0) @@ -612,7 +612,7 @@ describe('lib/exec/spawn', function () { vi.mocked(xvfb.isNeeded).mockReturnValue(false) // @ts-expect-error - invalid number of arguments for given type - const startPromise = spawn.start() + const startPromise = start() spawnedProcess.emit('close', 0) @@ -632,7 +632,7 @@ describe('lib/exec/spawn', function () { vi.mocked(xvfb.isNeeded).mockReturnValue(false) // @ts-expect-error - invalid number of arguments for given type - const startPromise = spawn.start() + const startPromise = start() spawnedProcess.emit('close', 0) @@ -649,7 +649,7 @@ describe('lib/exec/spawn', function () { vi.mocked(xvfb.isNeeded).mockReturnValue(true) // @ts-expect-error - invalid number of arguments for given type - const startPromise = spawn.start() + const startPromise = start() await flushPromises() @@ -668,7 +668,7 @@ describe('lib/exec/spawn', function () { vi.mocked(xvfb.isNeeded).mockReturnValue(false) // @ts-expect-error - invalid number of arguments for given type - const startPromise = spawn.start() + const startPromise = start() await flushPromises() @@ -697,7 +697,7 @@ describe('lib/exec/spawn', function () { }) // @ts-expect-error - invalid number of arguments for given type - const startPromise = spawn.start() + const startPromise = start() spawnedProcess.emit('close', 0) @@ -709,6 +709,44 @@ describe('lib/exec/spawn', function () { expect(spawnedProcess.stdout.pipe).toHaveBeenCalledExactlyOnceWith(stdout) }) + it('filters out dbus errors on linux', async () => { + vi.mocked(os.platform).mockReturnValue('linux') + + const dbusErrors = [ + Buffer.from('ERROR:dbus/bus.cc:123: Failed to connect to session bus'), + Buffer.from('[246:0820/083339.099956:ERROR:dbus/object_proxy.cc:590] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type:'), + ] + + const normalError = Buffer.from('Some other error message') + + let dataCallback: (data: Buffer) => void + + // mock stderr data handler + spawnedProcess.stderr.on.mockImplementation((event, callback) => { + if (event === 'data') { + dataCallback = callback + } + }) + + // @ts-expect-error - invalid number of arguments for given type + const startPromise = start() + + // Emit dbus error - should be filtered out (not written to stderr) + dbusErrors.forEach((err) => { + dataCallback!(err) + expect(stderr.write).not.toHaveBeenCalledWith(err) + }) + + // Emit normal error - should be written to stderr + dataCallback!(normalError) + + expect(stderr.write).toHaveBeenCalledWith(normalError) + + spawnedProcess.emit('close', 0) + + await startPromise + }) + // https://github.com/cypress-io/cypress/issues/1841 // https://github.com/cypress-io/cypress/issues/5241 const errCodes = ['EPIPE', 'ENOTCONN'] @@ -732,7 +770,7 @@ describe('lib/exec/spawn', function () { expect(() => { // kick off the mock process // @ts-expect-error - invalid number of arguments for given type - spawn.start() + start() const err: any = new Error() @@ -747,7 +785,7 @@ describe('lib/exec/spawn', function () { expect(() => { // kick off the mock process // @ts-expect-error - invalid number of arguments for given type - spawn.start() + start() const err: any = new Error('wattttt')