diff --git a/detox/local-cli/utils/frameworkUtils.js b/detox/local-cli/utils/frameworkUtils.js index 5b82978c87..f6ffa50b45 100644 --- a/detox/local-cli/utils/frameworkUtils.js +++ b/detox/local-cli/utils/frameworkUtils.js @@ -1,13 +1,12 @@ const os = require('os'); const path = require('path'); -const { spawn } = require('child-process-promise'); const fs = require('fs-extra'); +const { spawn } = require('promisify-child-process'); const detox = require('../../internals'); const { getFrameworkDirPath, getXCUITestRunnerDirPath } = require('../../src/utils/environment'); - const frameworkBuildScript = '../../scripts/build_local_framework.ios.sh'; const xcuitestBuildScript = '../../scripts/build_local_xcuitest.ios.sh'; diff --git a/detox/package.json b/detox/package.json index 7f475d297a..d8b59c41a2 100644 --- a/detox/package.json +++ b/detox/package.json @@ -64,7 +64,7 @@ "prettier": "^3.1.1", "react-native": "0.76.3", "react-native-codegen": "^0.0.8", - "typescript": "^5.3.3", + "typescript": "~5.3.3", "wtfnode": "^0.9.1" }, "dependencies": { @@ -73,7 +73,6 @@ "bunyan-debug-stream": "^3.1.0", "caf": "^15.0.1", "chalk": "^4.0.0", - "child-process-promise": "^2.2.0", "detox-copilot": "^0.0.24", "execa": "^5.1.1", "find-up": "^5.0.0", @@ -81,6 +80,7 @@ "funpermaproxy": "^1.1.0", "glob": "^8.0.3", "ini": "^1.3.4", + "promisify-child-process": "^4.1.2", "jest-environment-emit": "^1.0.8", "json-cycle": "^1.3.0", "lodash": "^4.17.11", diff --git a/detox/src/devices/common/drivers/android/exec/ADB.js b/detox/src/devices/common/drivers/android/exec/ADB.js index 434b0d65c8..7413d8604e 100644 --- a/detox/src/devices/common/drivers/android/exec/ADB.js +++ b/detox/src/devices/common/drivers/android/exec/ADB.js @@ -366,6 +366,7 @@ class ADB { const _spawnOptions = { ...spawnOptions, capture: ['stdout'], + encoding: 'utf8', }; return spawnWithRetriesAndLogs(this.adbBin, _flags, _spawnOptions); } diff --git a/detox/src/devices/common/drivers/android/exec/ADB.test.js b/detox/src/devices/common/drivers/android/exec/ADB.test.js index 3c8bb59453..a2d9eb9948 100644 --- a/detox/src/devices/common/drivers/android/exec/ADB.test.js +++ b/detox/src/devices/common/drivers/android/exec/ADB.test.js @@ -133,6 +133,7 @@ describe('ADB', () => { retries: 3, timeout: 45000, capture: ['stdout'], + encoding: 'utf8', }; await adb.install(deviceId, 'path/to/bin.apk'); @@ -213,6 +214,7 @@ describe('ADB', () => { retries: 3, timeout: 45000, capture: ['stdout'], + encoding: 'utf8', }; const binaryPath = '/mock-path/filename.mock'; await adb.install(deviceId, binaryPath); diff --git a/detox/src/devices/common/drivers/android/exec/BinaryExec.js b/detox/src/devices/common/drivers/android/exec/BinaryExec.js index 209ec3a751..725e51b8a5 100644 --- a/detox/src/devices/common/drivers/android/exec/BinaryExec.js +++ b/detox/src/devices/common/drivers/android/exec/BinaryExec.js @@ -1,5 +1,4 @@ -// @ts-nocheck -const spawn = require('child-process-promise').spawn; +const { spawn } = require('promisify-child-process'); const exec = require('../../../../../utils/childProcess').execWithRetriesAndLogs; @@ -27,15 +26,17 @@ class BinaryExec { } async exec(command) { - return (await exec(`"${this.binary}" ${command._getArgsString()}`)).stdout; + const result = await exec(`"${this.binary}" ${command._getArgsString()}`); + + return result.stdout; } spawn(command, stdout, stderr) { - return spawn(this.binary, command._getArgs(), { detached: true, stdio: ['ignore', stdout, stderr] }); + return spawn(this.binary, command._getArgs(), { detached: true, encoding: 'utf8', stdio: ['ignore', stdout, stderr] }); } } module.exports = { ExecCommand, - BinaryExec, + BinaryExec }; diff --git a/detox/src/devices/common/drivers/android/exec/BinaryExec.test.js b/detox/src/devices/common/drivers/android/exec/BinaryExec.test.js index 41791e1497..9f1e131dcf 100644 --- a/detox/src/devices/common/drivers/android/exec/BinaryExec.test.js +++ b/detox/src/devices/common/drivers/android/exec/BinaryExec.test.js @@ -1,3 +1,4 @@ +/* eslint-disable node/no-extraneous-require */ describe('BinaryExec', () => { const binaryPath = '/binary/mock'; @@ -12,10 +13,10 @@ describe('BinaryExec', () => { })); exec = require('../../../../../utils/childProcess').execWithRetriesAndLogs; - jest.mock('child-process-promise', () => ({ + jest.mock('promisify-child-process', () => ({ spawn: jest.fn() })); - spawn = require('child-process-promise').spawn; + spawn = require('promisify-child-process').spawn; const { BinaryExec } = require('./BinaryExec'); binaryExec = new BinaryExec(binaryPath); @@ -87,7 +88,7 @@ describe('BinaryExec', () => { expect(spawn).toHaveBeenCalledWith(binaryPath, commandArgs, expect.anything()); }); - it('should chain-return child-process-promise from spawn', async () => { + it('should chain-return promisify-child-process from spawn', async () => { const childProcessPromise = Promise.resolve('mock result'); spawn.mockReturnValue(childProcessPromise); diff --git a/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js b/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js index 7bc241cd51..7c0dae3269 100644 --- a/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js +++ b/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js @@ -1,8 +1,8 @@ // @ts-nocheck const path = require('path'); -const exec = require('child-process-promise').exec; const _ = require('lodash'); +const { exec } = require('promisify-child-process'); const temporaryPath = require('../../../../artifacts/utils/temporaryPath'); const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError'); @@ -16,7 +16,6 @@ const traceInvocationCall = require('../../../../utils/traceInvocationCall').bin const IosDriver = require('./IosDriver'); - /** * @typedef SimulatorDriverDeps { DeviceDriverDeps } * @property applesimutils { AppleSimUtils } @@ -111,8 +110,9 @@ class SimulatorDriver extends IosDriver { if (Number.isNaN(pid)) { throw new DetoxRuntimeError({ message: `Failed to find a process corresponding to the app bundle identifier (${bundleId}).`, - hint: `Make sure that the app is running on the device (${udid}), visually or via CLI:\n` + - `xcrun simctl spawn ${this.udid} launchctl list | grep -F '${bundleId}'\n`, + hint: + `Make sure that the app is running on the device (${udid}), visually or via CLI:\n` + + `xcrun simctl spawn ${this.udid} launchctl list | grep -F '${bundleId}'\n` }); } else { log.info({}, `Found the app (${bundleId}) with process ID = ${pid}. Proceeding...`); @@ -141,7 +141,7 @@ class SimulatorDriver extends IosDriver { const xcuitestRunner = new XCUITestRunner({ runtimeDevice: { id: this.getExternalId(), _bundleId } }); let x = point?.x ?? 100; let y = point?.y ?? 100; - let _pressDuration = pressDuration ? (pressDuration / 1000) : 1; + let _pressDuration = pressDuration ? pressDuration / 1000 : 1; const traceDescription = actionDescription.longPress({ x, y }, _pressDuration); return this.withAction(xcuitestRunner, 'coordinateLongPress', traceDescription, x.toString(), y.toString(), _pressDuration.toString()); } @@ -210,7 +210,7 @@ class SimulatorDriver extends IosDriver { await this.emitter.emit('createExternalArtifact', { pluginId: 'screenshot', artifactName: screenshotName || path.basename(tempPath, '.png'), - artifactPath: tempPath, + artifactPath: tempPath }); return tempPath; @@ -223,7 +223,7 @@ class SimulatorDriver extends IosDriver { await this.emitter.emit('createExternalArtifact', { pluginId: 'uiHierarchy', artifactName: artifactName, - artifactPath: viewHierarchyURL, + artifactPath: viewHierarchyURL }); return viewHierarchyURL; diff --git a/detox/src/ios/XCUITestRunner.js b/detox/src/ios/XCUITestRunner.js index 04693fafdd..5d4f41e791 100644 --- a/detox/src/ios/XCUITestRunner.js +++ b/detox/src/ios/XCUITestRunner.js @@ -1,4 +1,4 @@ -const { exec } = require('child-process-promise'); +const { exec } = require('promisify-child-process'); const DetoxRuntimeError = require('../errors/DetoxRuntimeError'); const environment = require('../utils/environment'); diff --git a/detox/src/ios/XCUITestRunner.test.js b/detox/src/ios/XCUITestRunner.test.js index a6f5847ae0..da328a5d95 100644 --- a/detox/src/ios/XCUITestRunner.test.js +++ b/detox/src/ios/XCUITestRunner.test.js @@ -1,12 +1,12 @@ const XCUITestRunner = require('./XCUITestRunner'); -jest.mock('child-process-promise', () => { +jest.mock('promisify-child-process', () => { return { exec: jest.fn(), }; }); -const { exec } = jest.requireMock('child-process-promise'); +const { exec } = jest.requireMock('promisify-child-process'); const environment = jest.requireMock('../utils/environment'); jest.mock('../utils/environment'); diff --git a/detox/src/utils/childProcess/exec.js b/detox/src/utils/childProcess/exec.js index c24ca635e8..8a90d01dc7 100644 --- a/detox/src/utils/childProcess/exec.js +++ b/detox/src/utils/childProcess/exec.js @@ -1,6 +1,6 @@ // @ts-nocheck -const { exec } = require('child-process-promise'); const _ = require('lodash'); +const { exec } = require('promisify-child-process'); const DetoxRuntimeError = require('../../errors/DetoxRuntimeError'); const rootLogger = require('../logger').child({ cat: ['child-process', 'child-process-exec'] }); @@ -8,6 +8,12 @@ const retry = require('../retry'); const execsCounter = require('./opsCounter'); +/** + * Executes a command with retries and logs + * @param {*} bin - command to execute + * @param {*} options - options + * @returns {Promise} + */ async function execWithRetriesAndLogs(bin, options = {}) { const { retries = 9, diff --git a/detox/src/utils/childProcess/exec.test.js b/detox/src/utils/childProcess/exec.test.js index cd5bcff9a7..44cdeedf68 100644 --- a/detox/src/utils/childProcess/exec.test.js +++ b/detox/src/utils/childProcess/exec.test.js @@ -7,8 +7,8 @@ describe('Exec utils', () => { jest.mock('../logger'); logger = require('../logger'); - jest.mock('child-process-promise'); - cpp = require('child-process-promise'); + jest.mock('promisify-child-process'); + cpp = require('promisify-child-process'); exec = require('./exec'); }); @@ -91,8 +91,8 @@ describe('Exec utils', () => { args: `--argument 123`, statusLogs: { trying: 'trying status log', - successful: 'successful status log', - }, + successful: 'successful status log' + } }; await execWithRetriesAndLogs('bin', options); @@ -110,8 +110,8 @@ describe('Exec utils', () => { const options = { args: `--argument 123`, statusLogs: { - retrying: true, - }, + retrying: true + } }; logger.debug.mockClear(); @@ -233,20 +233,20 @@ describe('Exec utils', () => { }); const returnSuccessfulWithValue = (value) => ({ - stdout: JSON.stringify(value), - stderr: 'err', - childProcess: { - exitCode: 0 - } - }); + stdout: JSON.stringify(value), + stderr: 'err', + childProcess: { + exitCode: 0 + } +}); const returnErrorWithValue = (value) => ({ - stdout: 'out', - stderr: value, - childProcess: { - exitCode: 1 - } - }); + stdout: 'out', + stderr: value, + childProcess: { + exitCode: 1 + } +}); function mockCppSuccessful(cpp) { const successfulResult = returnSuccessfulWithValue('successful result'); diff --git a/detox/src/utils/childProcess/spawn.js b/detox/src/utils/childProcess/spawn.js index bd803d853f..0e0ec992d8 100644 --- a/detox/src/utils/childProcess/spawn.js +++ b/detox/src/utils/childProcess/spawn.js @@ -1,6 +1,6 @@ // @ts-nocheck -const { spawn } = require('child-process-promise'); const _ = require('lodash'); +const { spawn } = require('promisify-child-process'); const rootLogger = require('../logger').child({ cat: ['child-process', 'child-process-spawn'] }); const { escape } = require('../pipeCommands'); @@ -32,7 +32,8 @@ async function spawnWithRetriesAndLogs(binary, flags, options = {}) { let result; await retry({ retries, interval }, async (tryCount, lastError) => { _logSpawnRetrying(logger, tryCount, lastError); - result = await _spawnAndLog(logger, binary, flags, command, spawnOptions, tryCount); + result = _spawnAndLog(logger, binary, flags, command, spawnOptions, tryCount); + await result; }); return result; } @@ -69,8 +70,20 @@ async function interruptProcess(childProcessPromise, schedule) { function _spawnAndLog(logger, binary, flags, command, options, tryCount) { const { logLevelPatterns, silent, ...spawnOptions } = { stdio: ['ignore', 'pipe', 'pipe'], ...options }; const cpPromise = spawn(binary, flags, spawnOptions); + const childProcess = cpPromise.childProcess = cpPromise; + const originalThen = cpPromise.then.bind(cpPromise); + const augmentPromise = (fn) => { + return typeof fn === 'function' + ? (result) => fn(Object.assign(result, { + childProcess, + exitCode: childProcess.exitCode, + pid: childProcess.pid + })) + : fn; + }; + cpPromise.then = (onFulfilled, onRejected) => originalThen(augmentPromise(onFulfilled), augmentPromise(onRejected)); + cpPromise.catch = (onRejected) => cpPromise.then(undefined, onRejected); - const { childProcess } = cpPromise; const { exitCode, stdout, stderr } = childProcess; const _logger = logger.child({ cpid: childProcess.pid }); @@ -85,8 +98,12 @@ function _spawnAndLog(logger, binary, flags, command, options, tryCount) { stderr && stderr.on('data', _spawnStderrLoggerFn(_logger, logLevelPatterns)); } + /** + * + * @param {import('promisify-child-process').Output} resultOrErr + */ function onEnd(resultOrErr) { - const signal = resultOrErr.childProcess.signalCode || ''; + const signal = resultOrErr.signal || ''; const { code } = resultOrErr; const action = signal ? `terminated with ${signal}` : `exited with code #${code}`; diff --git a/detox/src/utils/childProcess/spawn.test.js b/detox/src/utils/childProcess/spawn.test.js index 7893763017..78d84f3a8a 100644 --- a/detox/src/utils/childProcess/spawn.test.js +++ b/detox/src/utils/childProcess/spawn.test.js @@ -3,7 +3,6 @@ jest.retryTimes(2); describe('Spawn utils', () => { - describe('spawning', () => { let retry; let log; @@ -13,8 +12,8 @@ describe('Spawn utils', () => { jest.mock('../logger'); log = require('../logger'); - jest.mock('child-process-promise'); - cpp = require('child-process-promise'); + jest.mock('promisify-child-process'); + cpp = require('promisify-child-process'); jest.mock('../retry'); retry = require('../retry'); @@ -25,21 +24,17 @@ describe('Spawn utils', () => { mockSpawnResult(0, { pid: 2018, stdout: toStream('hello'), - stderr: toStream('world'), + stderr: toStream('world') }); }); - const mockSpawnResult = (code, childProcess) => { - const cpPromise = Promise.resolve({ code, childProcess }); - cpp.spawn.mockReturnValue(Object.assign(cpPromise, { childProcess })); - }; + const mockSpawnResult = (code, childProcess, reset = true) => { + if (reset) { + cpp.spawn.mockReset(); + } - const mockSpawnResults = (childProcess1, childProcess2) => { - const cpPromise1 = Promise.resolve({ childProcess: childProcess1 }); - const cpPromise2 = Promise.resolve({ childProcess: childProcess2 }); - cpp.spawn - .mockReturnValueOnce(Object.assign(cpPromise1, { childProcess: childProcess1 })) - .mockReturnValueOnce(Object.assign(cpPromise2, { childProcess: childProcess2 })); + const cpPromise = Promise.resolve({ code }); + cpp.spawn.mockReturnValueOnce(Object.assign(cpPromise, childProcess)); }; const advanceOpsCounter = (count) => { @@ -47,104 +42,115 @@ describe('Spawn utils', () => { for (let i = 0; i < count; i++) opsCounter.inc(); }; - [ - 'spawnAndLog', - 'spawnWithRetriesAndLogs', - ].forEach((func) => { - describe(func, () => { - it('should spawn an attached command with ignored input and piped output', async () => { - const command = 'command'; - const flags = ['--some', '--value', Math.random()]; + describe.each([ + ['spawnAndLog'], + ['spawnWithRetriesAndLogs'] + ])('%s function', (func) => { + it('should spawn an attached command with ignored input and piped output', async () => { + const command = 'command'; + const flags = ['--some', '--value', Math.random()]; - await spawn[func](command, flags); + await spawn[func](command, flags); - expect(cpp.spawn).toBeCalledWith(command, flags, expect.objectContaining({ + expect(cpp.spawn).toBeCalledWith( + command, + flags, + expect.objectContaining({ stdio: ['ignore', 'pipe', 'pipe'] - })); - }); + }) + ); + }); - it('should log spawn command upon child-process start and finish', async () => { - jest.spyOn(log, 'child'); - await spawn[func]('mockCommand', []); + it('should log spawn command upon child-process start and finish', async () => { + jest.spyOn(log, 'child'); + await spawn[func]('mockCommand', []); - expect(log.debug).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_CMD' }), 'mockCommand'); - expect(log.debug).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_END' }), 'mockCommand exited with code #0'); - }); + expect(log.debug).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_CMD' }), 'mockCommand'); + expect(log.debug).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_END' }), 'mockCommand exited with code #0'); + }); - it('should collect output and log it', async () => { - await spawn[func]('mockCommand', []); - await nextCycle(); + it('should collect output and log it', async () => { + await spawn[func]('mockCommand', []); + await nextCycle(); - expect(log.trace).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_STDOUT', stdout: true }), 'hello'); - expect(log.error).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_STDERR', stderr: true }), 'world'); - }); + expect(log.trace).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_STDOUT', stdout: true }), 'hello'); + expect(log.error).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_STDERR', stderr: true }), 'world'); + }); - it('should form and use a child-logger', async () => { - const trackingId = 7; - advanceOpsCounter(trackingId); + it('should form and use a child-logger', async () => { + const trackingId = 7; + advanceOpsCounter(trackingId); - jest.spyOn(log, 'child'); - await spawn[func]('mockCommand', []); + jest.spyOn(log, 'child'); + await spawn[func]('mockCommand', []); - expect(log.child).toHaveBeenCalledWith(expect.objectContaining({ trackingId, command: 'mockCommand', fn: func })); - expect(log.child).toHaveBeenCalledWith(expect.objectContaining({ cpid: 2018 })); - }); + expect(log.child).toHaveBeenCalledWith(expect.objectContaining({ trackingId, command: 'mockCommand', fn: func })); + expect(log.child).toHaveBeenCalledWith(expect.objectContaining({ cpid: 2018 })); + }); - it('should override log levels if configured', async () => { - jest.spyOn(log, 'child'); - await spawn[func]('command', [], { - logLevelPatterns: { - debug: [/hello/], - warn: [/world/], - }, - }); + it('should override log levels if configured', async () => { + jest.spyOn(log, 'child'); + await spawn[func]('command', [], { + logLevelPatterns: { + debug: [/hello/], + warn: [/world/] + } + }); - await nextCycle(); + await nextCycle(); - expect(log.debug).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_STDOUT' }), 'hello'); - expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_STDERR' }), 'world'); - }); + expect(log.debug).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_STDOUT' }), 'hello'); + expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ event: 'SPAWN_STDERR' }), 'world'); + }); - it('should not log output if silent: true', async () => { - await spawn[func]('mockCommand', [], { silent: true }); - await nextCycle(); + it('should not log output if silent: true', async () => { + await spawn[func]('mockCommand', [], { silent: true }); + await nextCycle(); + + expect(log.debug).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_CMD' }), 'mockCommand'); + expect(log.debug).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_END' }), 'mockCommand exited with code #0'); + expect(log.trace).not.toBeCalledWith( + expect.objectContaining(expect.objectContaining({ event: 'SPAWN_STDOUT' })), + expect.any(String) + ); + expect(log.error).not.toBeCalledWith( + expect.objectContaining(expect.objectContaining({ event: 'SPAWN_STDERR' })), + expect.any(String) + ); + }); - expect(log.debug).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_CMD' }), 'mockCommand'); - expect(log.debug).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_END' }), 'mockCommand exited with code #0'); - expect(log.trace).not.toBeCalledWith(expect.objectContaining(expect.objectContaining({ event: 'SPAWN_STDOUT' })), expect.any(String)); - expect(log.error).not.toBeCalledWith(expect.objectContaining(expect.objectContaining({ event: 'SPAWN_STDERR' })), expect.any(String)); + it('should log erroneously finished spawns', async () => { + mockSpawnResult(-2, { + pid: 8102, + stdout: toStream(''), + stderr: toStream('Some error.') }); - it('should log erroneously finished spawns', async () => { - mockSpawnResult(-2, { - pid: 8102, - stdout: toStream(''), - stderr: toStream('Some error.'), - }); + await spawn[func]('mockCommand', []).catch(() => {}); + await nextCycle(); - await spawn[func]('mockCommand', []).catch(() => {}); - await nextCycle(); + expect(log.debug).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_CMD' }), 'mockCommand'); + expect(log.debug).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_END' }), 'mockCommand exited with code #-2'); + expect(log.error).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_STDERR', stderr: true }), 'Some error.'); + }); - expect(log.debug).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_CMD' }), 'mockCommand'); - expect(log.debug).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_END' }), 'mockCommand exited with code #-2'); - expect(log.error).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_STDERR', stderr: true }), 'Some error.'); + it('should log immediate spawn errors', async () => { + mockSpawnResult(undefined, { + pid: null, + exitCode: -2, + stdout: toStream(''), + stderr: toStream('Command `command` not found.') }); - it('should log immediate spawn errors', async () => { - mockSpawnResult(undefined, { - pid: null, - exitCode: -2, - stdout: toStream(''), - stderr: toStream('Command `command` not found.'), - }); + await spawn[func]('command', []); + await nextCycle(); - await spawn[func]('command', []); - await nextCycle(); - - expect(log.debug).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_CMD' }), 'command'); - expect(log.error).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_ERROR' }), 'command failed with code = -2'); - expect(log.error).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_STDERR', stderr: true }), 'Command `command` not found.'); - }); + expect(log.debug).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_CMD' }), 'command'); + expect(log.error).toBeCalledWith(expect.objectContaining({ event: 'SPAWN_ERROR' }), 'command failed with code = -2'); + expect(log.error).toBeCalledWith( + expect.objectContaining({ event: 'SPAWN_STDERR', stderr: true }), + 'Command `command` not found.' + ); }); }); @@ -160,9 +166,13 @@ describe('Spawn utils', () => { it('should spawn an attached command with stderr capturing, by default', async () => { await spawn.spawnWithRetriesAndLogs(command, flags); - expect(cpp.spawn).toBeCalledWith(command, flags, expect.objectContaining({ - capture: ['stderr'], - })); + expect(cpp.spawn).toBeCalledWith( + command, + flags, + expect.objectContaining({ + capture: ['stderr'] + }) + ); }); it('should retry once, by default', async () => { @@ -200,34 +210,39 @@ describe('Spawn utils', () => { it('should honor output-capturing options, but force stderr', async () => { await spawn.spawnWithRetriesAndLogs(command, flags, { capture: ['stdout'] }); - expect(cpp.spawn).toBeCalledWith(command, flags, expect.objectContaining({ - capture: ['stdout', 'stderr'], - })); + expect(cpp.spawn).toBeCalledWith( + command, + flags, + expect.objectContaining({ + capture: ['stdout', 'stderr'] + }) + ); }); it('should return result of last retry attempt', async () => { - const childProcess1 = { + mockSpawnResult(1, { pid: 100, exitCode: 1, - stderr: toStream(''), - }; - const childProcess2 = { + stderr: toStream('') + }); + mockSpawnResult(0, { pid: 101, exitCode: 0, - stdout: toStream('okay great'), - }; - mockSpawnResults(childProcess1, childProcess2); + stdout: toStream('okay great') + }, false); - retry.mockImplementation(async (opts, callback) => { + retry.mockImplementation(async (_opts, callback) => { await callback(1); await callback(2, spawnTryError('mocked stderr')); }); const result = await spawn.spawnWithRetriesAndLogs(command, flags); - expect(result.childProcess).toEqual(expect.objectContaining({ - pid: 101, - exitCode: 0, - })); + expect(result).toEqual( + expect.objectContaining({ + pid: 101, + exitCode: 0 + }) + ); expect(cpp.spawn).toHaveBeenCalledTimes(2); }); }); @@ -242,17 +257,13 @@ describe('Spawn utils', () => { }, 500); it('should throw exception if child process exited with an error', async () => { - const script = - "process.on('SIGINT', () => {});" + - 'setTimeout(()=>process.exit(1), 100);'; + const script = "process.on('SIGINT', () => {});" + 'setTimeout(()=>process.exit(1), 100);'; await interruptProcess(spawnAndLog('node', ['-e', script])); }, 1000); it('should SIGTERM a stuck process after specified time', async () => { - const script = - "process.on('SIGINT', () => {});" + - 'setTimeout(()=>process.exit(1), 10000);'; + const script = "process.on('SIGINT', () => {});" + 'setTimeout(()=>process.exit(1), 10000);'; const theProcess = spawnAndLog('node', ['-e', script]); await interruptProcess(theProcess, { @@ -263,7 +274,7 @@ describe('Spawn utils', () => { }); function nextCycle() { - return new Promise(resolve => setTimeout(resolve)); + return new Promise((resolve) => setTimeout(resolve)); } function toStream(string) { diff --git a/detox/src/utils/environment.js b/detox/src/utils/environment.js index aa53771efb..2ccb76c631 100644 --- a/detox/src/utils/environment.js +++ b/detox/src/utils/environment.js @@ -3,9 +3,9 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); -const exec = require('child-process-promise').exec; const ini = require('ini'); const _ = require('lodash'); +const { exec } = require('promisify-child-process'); const _which = require('which'); const DetoxRuntimeError = require('../errors/DetoxRuntimeError'); @@ -147,8 +147,7 @@ function throwMissingAvdINIError(avdName, avdIniPath) { function throwMissingAvdError(avdName, avdPath, avdIniPath) { throw new DetoxRuntimeError( - `Failed to find AVD ${avdName} directory at path: ${avdPath}\n` + - `Please verify "path" property in the INI file: ${avdIniPath}` + `Failed to find AVD ${avdName} directory at path: ${avdPath}\n` + `Please verify "path" property in the INI file: ${avdIniPath}` ); } @@ -169,7 +168,9 @@ function throwSdkIntegrityError(errMessage) { } function throwMissingGmsaasError() { - throw new DetoxRuntimeError(`Failed to locate Genymotion's gmsaas executable. Please add it to your $PATH variable!\nPATH is currently set to: ${process.env.PATH}`); + throw new DetoxRuntimeError( + `Failed to locate Genymotion's gmsaas executable. Please add it to your $PATH variable!\nPATH is currently set to: ${process.env.PATH}` + ); } const getDetoxVersion = _.once(() => { @@ -178,11 +179,15 @@ const getDetoxVersion = _.once(() => { const getBuildFolderName = _.once(async () => { const detoxVersion = getDetoxVersion(); - const xcodeVersion = await exec('xcodebuild -version').then(result => result.stdout.trim()); - - return crypto.createHash('sha1') - .update(`${detoxVersion}\n${xcodeVersion}\n`) - .digest('hex'); + const xcodeVersion = await exec('xcodebuild -version').then((result) => { + if (typeof result.stdout === 'string') { + return result.stdout.trim(); + } else { + throw new DetoxRuntimeError(`Failed trim stdout result of command: xcodebuild -version`); + } + }); + + return crypto.createHash('sha1').update(`${detoxVersion}\n${xcodeVersion}\n`).digest('hex'); }); const getFrameworkDirPath = `${DETOX_LIBRARY_ROOT_PATH}/ios/framework`; @@ -197,8 +202,14 @@ const getXCUITestRunnerDirPath = `${DETOX_LIBRARY_ROOT_PATH}/ios/xcuitest-runner const getXCUITestRunnerPath = _.once(async () => { const buildFolder = await getBuildFolderName(); const derivedDataPath = `${getXCUITestRunnerDirPath}/${buildFolder}`; - const xctestrunPath = await exec(`find ${derivedDataPath} -name "*.xctestrun" -print -quit`) - .then(result => result.stdout.trim()); + const command = `find ${derivedDataPath} -name "*.xctestrun" -print -quit`; + const xctestrunPath = await exec(command).then((result) => { + if (typeof result.stdout === 'string') { + return result.stdout.trim(); + } else { + throw new DetoxRuntimeError(`Failed trim stdout result of command: ${command}`); + } + }); if (!xctestrunPath) { throw new DetoxRuntimeError(`Failed to find .xctestrun file in ${derivedDataPath}`); @@ -246,5 +257,5 @@ module.exports = { getDetoxLockFilePath, getDeviceRegistryPath, getLastFailedTestsPath, - getHomeDir, + getHomeDir };