diff --git a/detox/src/devices/android/ADB.js b/detox/src/devices/android/ADB.js index d1bb1daa2d..4e9ef00926 100644 --- a/detox/src/devices/android/ADB.js +++ b/detox/src/devices/android/ADB.js @@ -1,18 +1,19 @@ const _ = require('lodash'); const path = require('path'); const {execWithRetriesAndLogs, spawnAndLog} = require('../../utils/exec'); -const pipeCommands = require('../../utils/pipeCommands'); +const {escape} = require('../../utils/pipeCommands'); const EmulatorTelnet = require('./EmulatorTelnet'); const Environment = require('../../utils/environment'); class ADB { constructor() { + this._cachedApiLevels = new Map(); this.adbBin = path.join(Environment.getAndroidSDKPath(), 'platform-tools', 'adb'); } async devices() { const output = (await this.adbCmd('', 'devices')).stdout; - return await this.parseAdbDevicesConsoleOutput(output); + return this.parseAdbDevicesConsoleOutput(output); } async unlockScreen(deviceId) { @@ -31,8 +32,7 @@ class ADB { } async _getPowerStatus(deviceId) { - const grep = pipeCommands.search.regexp; - const stdout = await this.shell(deviceId, `dumpsys power | ${grep('^[ ]*m[UW].*=')}`); + const stdout = await this.shell(deviceId, `dumpsys power | grep "^[ ]*m[UW].*="`); return stdout .split('\n') @@ -89,7 +89,7 @@ class ADB { } async now(deviceId) { - return this.shell(deviceId, `date "+\\"%Y-%m-%d %T.000\\""`); + return this.shell(deviceId, `date +"%Y-%m-%d %T.000"`); } async install(deviceId, apkPath) { @@ -110,10 +110,9 @@ class ADB { } async pidof(deviceId, bundleId) { - const bundleIdRegex = pipeCommands.escape.inQuotedRegexp(bundleId) + '[ ]*$'; - const grep = pipeCommands.search.regexp; + const bundleIdRegex = escape.inQuotedRegexp(bundleId) + '$'; - const processes = await this.shell(deviceId, `ps | ${grep(bundleIdRegex)}`).catch(() => ''); + const processes = await this.shell(deviceId, `ps | grep "${bundleIdRegex}"`).catch(() => ''); if (!processes) { return NaN; } @@ -121,12 +120,8 @@ class ADB { return parseInt(processes.split(' ').filter(Boolean)[1], 10); } - async shell(deviceId, cmd, options) { - return (await this.adbCmd(deviceId, `shell ${cmd}`, options)).stdout.trim(); - } - async getFileSize(deviceId, filename) { - const { stdout, stderr } = await this.adbCmd(deviceId, 'shell wc -c ' + filename).catch(e => e); + const { stdout, stderr } = await this.adbCmd(deviceId, 'shell du ' + filename).catch(e => e); if (stderr.includes('No such file or directory')) { return -1; @@ -136,7 +131,7 @@ class ADB { } async isFileOpen(deviceId, filename) { - const openedByProcesses = await this.shell(deviceId, 'lsof ' + filename); + const openedByProcesses = await this.shell(deviceId, `lsof | grep -e "${escape.inQuotedString(filename)}" || true`); return openedByProcesses.length > 0; } @@ -150,12 +145,14 @@ class ADB { } async apiLevel(deviceId) { - const lvl = await this.shell(deviceId, `getprop ro.build.version.sdk`); - return Number(lvl); + const lvl = Number(await this.shell(deviceId, `getprop ro.build.version.sdk`)); + this._cachedApiLevels.set(deviceId, lvl); + + return lvl; } async screencap(deviceId, path) { - return this.adbCmd(deviceId, `shell screencap ${path}`); + await this.shell(deviceId, `screencap ${path}`); } /*** @@ -185,37 +182,57 @@ class ADB { /*** * @returns ChildProcessPromise */ - logcat(deviceId, { expression, file, pid, time }) { - const logcatArgs = []; - - if (expression) { - logcatArgs.push('-e'); - logcatArgs.push(expression); - } - - if (file) { - logcatArgs.push('-f'); - logcatArgs.push(file); + logcat(deviceId, { file, pid, time }) { + let shellCommand = 'logcat -v brief'; + + // HACK: cannot make this function async, otherwise ChildProcessPromise.childProcess field will get lost, + // and this will break interruptProcess() call for any logcat promise. + const apiLevel = this._cachedApiLevels.get(deviceId); + if (time && apiLevel >= 21) { + shellCommand += ` -T "${time}"`; } if (pid > 0) { - logcatArgs.push(`--pid=${pid}`); - } + const __pid = String(pid).padStart(5); + shellCommand += ` | grep "(${__pid}):"`; + } - if (time) { - logcatArgs.push('-T'); - logcatArgs.push(time); + if (file) { + shellCommand += ` >> ${file}`; } - return this.spawn(deviceId, ['logcat', ...logcatArgs]); + return this.spawn(deviceId, ['shell', shellCommand]); } async pull(deviceId, src, dst = '') { - return this.adbCmd(deviceId, `pull "${src}" "${dst}"`); + await this.adbCmd(deviceId, `pull "${src}" "${dst}"`); } async rm(deviceId, path, force = false) { - return this.adbCmd(deviceId, `shell rm ${force ? '-f' : ''} "${path}"`); + await this.shell(deviceId, `rm ${force ? '-f' : ''} "${path}"`); + } + + async listInstrumentation(deviceId) { + return this.shell(deviceId, 'pm list instrumentation'); + } + + async getInstrumentationRunner(deviceId, bundleId) { + const instrumentationRunners = await this.listInstrumentation(deviceId); + const instrumentationRunner = this._instrumentationRunnerForBundleId(instrumentationRunners, bundleId); + if (instrumentationRunner === 'undefined') { + throw new Error(`No instrumentation runner found on device ${deviceId} for package ${bundleId}`); + } + + return instrumentationRunner; + } + + _instrumentationRunnerForBundleId(instrumentationRunners, bundleId) { + const runnerForBundleRegEx = new RegExp(`^instrumentation:(.*) \\(target=${bundleId.replace(new RegExp('\\.', 'g'), "\\.")}\\)$`, 'gm'); + return _.get(runnerForBundleRegEx.exec(instrumentationRunners), [1], 'undefined'); + } + + async shell(deviceId, cmd, options) { + return (await this.adbCmd(deviceId, `shell "${escape.inQuotedString(cmd)}"`, options)).stdout.trim(); } async adbCmd(deviceId, params, options) { @@ -224,7 +241,7 @@ class ADB { const retries = _.get(options, 'retries', 1); _.unset(options, 'retries'); - return await execWithRetriesAndLogs(cmd, options, undefined, retries); + return execWithRetriesAndLogs(cmd, options, undefined, retries); } /*** @@ -234,25 +251,6 @@ class ADB { const serial = deviceId ? ['-s', deviceId] : []; return spawnAndLog(this.adbBin, [...serial, ...params]); } - - async listInstrumentation(deviceId) { - return await this.shell(deviceId, 'pm list instrumentation'); - } - - instrumentationRunnerForBundleId(instrumentationRunners, bundleId) { - const runnerForBundleRegEx = new RegExp(`^instrumentation:(.*) \\(target=${bundleId.replace(new RegExp('\\.', 'g'), "\\.")}\\)$`, 'gm'); - return _.get(runnerForBundleRegEx.exec(instrumentationRunners), [1], 'undefined'); - } - - async getInstrumentationRunner(deviceId, bundleId) { - const instrumentationRunners = await this.listInstrumentation(deviceId); - const instrumentationRunner = this.instrumentationRunnerForBundleId(instrumentationRunners, bundleId); - if (instrumentationRunner === 'undefined') { - throw new Error(`No instrumentation runner found on device ${deviceId} for package ${bundleId}`); - } - - return instrumentationRunner; - } } module.exports = ADB; diff --git a/detox/src/devices/android/ADB.test.js b/detox/src/devices/android/ADB.test.js index 16c5da82cb..abd7f230db 100644 --- a/detox/src/devices/android/ADB.test.js +++ b/detox/src/devices/android/ADB.test.js @@ -46,12 +46,14 @@ describe('ADB', () => { }); it(`pidof (success)`, async () => { - adb.shell = async () => `u0_a19 2199 1701 3554600 70264 0 0 s com.google.android.ext.services `; + jest.spyOn(adb, 'shell').mockImplementation(async () => + `u0_a19 2199 1701 3554600 70264 0 0 s com.google.android.ext.services `); + expect(await adb.pidof('', 'com.google.android.ext.services')).toBe(2199); }); it(`pidof (failure)`, async () => { - adb.shell = async () => ``; + jest.spyOn(adb, 'shell').mockImplementation(async () => ''); expect(await adb.pidof('', 'com.google.android.ext.services')).toBe(NaN); }); @@ -59,7 +61,7 @@ describe('ADB', () => { const deviceId = 'mockEmulator'; async function unlockScreenWithPowerStatus(mWakefulness, mUserActivityTimeoutOverrideFromWindowManager) { - adb.shell = jest.fn().mockReturnValue(` + jest.spyOn(adb, 'shell').mockImplementation(async () => ` mWakefulness=${mWakefulness} mWakefulnessChanging=false mWakeLockSummary=0x0 @@ -116,11 +118,11 @@ describe('ADB', () => { it(`listInstrumentation passes the right deviceId`, async () => { const deviceId = 'aDeviceId'; - const spyShell = jest.spyOn(adb, 'shell'); + jest.spyOn(adb, 'shell'); await adb.listInstrumentation(deviceId); - expect(spyShell).toBeCalledWith(deviceId, expect.any(String)); + expect(adb.shell).toBeCalledWith(deviceId, 'pm list instrumentation'); }); it(`Parse 'adb device' output`, async () => { @@ -144,18 +146,7 @@ describe('ADB', () => { expect(actual).toEqual(parsedDevices); }); - it(`getInstrumentationRunner passes the right deviceId`, async () => { - const deviceId = 'aDeviceId'; - const spyRunnerForBundle = jest.spyOn(adb, 'instrumentationRunnerForBundleId'); - spyRunnerForBundle.mockReturnValue(''); - const spyShell = jest.spyOn(adb, 'shell'); - - await adb.getInstrumentationRunner(deviceId, 'com.whatever.package'); - - expect(spyShell).toBeCalledWith(deviceId, expect.any(String)); - }); - - it(`instrumentationRunnerForBundleId parses the correct runner for the package`, async () => { + it(`getInstrumentationRunner parses the correct runner for the package`, async () => { const expectedRunner = "com.example.android.apis/.app.LocalSampleInstrumentation"; const expectedPackage = "com.example.android.apis"; const instrumentationRunnersShellOutput = @@ -164,8 +155,11 @@ describe('ADB', () => { `instrumentation:${expectedRunner} (target=${expectedPackage})\n` + "instrumentation:org.chromium.webview_shell/.WebViewLayoutTestRunner (target=org.chromium.webview_shell)\n"; - const result = await adb.instrumentationRunnerForBundleId(instrumentationRunnersShellOutput, expectedPackage); + jest.spyOn(adb, 'shell').mockImplementation(async () => instrumentationRunnersShellOutput); + + const result = await adb.getInstrumentationRunner('aDeviceId', expectedPackage); + expect(adb.shell).toBeCalledWith('aDeviceId', 'pm list instrumentation'); expect(result).toEqual(expectedRunner); }); }); diff --git a/detox/src/devices/android/EmulatorTelnet.js b/detox/src/devices/android/EmulatorTelnet.js index 94765eb688..b5af8c8766 100644 --- a/detox/src/devices/android/EmulatorTelnet.js +++ b/detox/src/devices/android/EmulatorTelnet.js @@ -2,10 +2,13 @@ const Telnet = require('telnet-client'); const path = require('path'); const os = require('os'); const fs = require('fs-extra'); +const log = require('../../utils/logger').child({ __filename }); class EmulatorTelnet { constructor() { this.connection = new Telnet(); + this.connection.on('timeout', () => log.error({ event: 'TELNET_TIMEOUT' })); + this.connection.on('error', (err) => log.error({ event: 'TELNET_ERROR', err })); } async connect(port) { @@ -20,6 +23,7 @@ class EmulatorTelnet { stripShellPrompt: true }; + log.trace({ event: 'TELNET_CONNECTING' }, `port: ${port}, host: ${params.host}`); await this.connection.connect(params); const auth = await fs.readFile(path.join(os.homedir(), '.emulator_console_auth_token'), 'utf8'); await this.exec(`auth ${auth}`); @@ -36,18 +40,18 @@ class EmulatorTelnet { this.connection.shell((error, stream) => { stream.write(`${command}\n`); stream.on('data', (data) => { - const result = data.toString(); - if (result.includes('\n')) { - resolve(result); + const result = data.toString(); + if (result.includes('\n')) { + resolve(result); + } } - } ); }); }); } async avdName() { - return await this.exec('avd name'); + return this.exec('avd name'); } async kill() { @@ -61,7 +65,7 @@ class EmulatorTelnet { } async rotate() { - return await this.shell('rotate'); + return this.shell('rotate'); } } diff --git a/detox/src/utils/exec.js b/detox/src/utils/exec.js index 9b38f6ffe7..3a662544e9 100644 --- a/detox/src/utils/exec.js +++ b/detox/src/utils/exec.js @@ -54,6 +54,14 @@ async function execWithRetriesAndLogs(bin, options, statusLogs, retries = 10, in // log.error(`${_operationCounter}: stderr:`, result.stderr); //} + if (typeof result.stdout === 'string') { + result.stdout = result.stdout.replace(/\r\n/g, '\n'); + } + + if (typeof result.stderr === 'string') { + result.stderr = result.stderr.replace(/\r\n/g, '\n'); + } + return result; } diff --git a/detox/src/utils/pipeCommands.js b/detox/src/utils/pipeCommands.js index 0653c7f1dc..c0be14988d 100644 --- a/detox/src/utils/pipeCommands.js +++ b/detox/src/utils/pipeCommands.js @@ -1,13 +1,17 @@ +const SPECIAL_CHARS = /([\^\$\[\]\*\.\\])/g; + +const escapeInQuotedString = (fragment) => fragment.replace(/"/g, '\\"'); +const escapeInQuotedRegexp = (fragment) => fragment.replace(SPECIAL_CHARS, "\\$1"); + function win32Implementation() { - const escapeInQuotedStringWin32 = (fragment) => fragment.replace(/"/g, '""'); - const escapeInQuotedRegexpWin32 = escapeInQuotedStringWin32; - const searchRegexpWin32 = (pattern) => `findstr /R /C:"${escapeInQuotedStringWin32(pattern)}"`; - const searchFragmentWin32 = (fragment) => `findstr /C:"${escapeInQuotedStringWin32(fragment)}"`; + const addCRLF = 'find /v ""'; + const searchRegexpWin32 = (pattern) => `${addCRLF} | findstr /R /C:"${escapeInQuotedString(pattern)}"`; + const searchFragmentWin32 = (fragment) => `${addCRLF} | findstr /C:"${escapeInQuotedString(fragment)}"`; return { escape: { - inQuotedString: escapeInQuotedStringWin32, - inQuotedRegexp: escapeInQuotedRegexpWin32, + inQuotedString: escapeInQuotedString, + inQuotedRegexp: escapeInQuotedRegexp, }, search: { regexp: searchRegexpWin32, @@ -17,17 +21,13 @@ function win32Implementation() { } function nixImplementation() { - const SPECIAL_CHARS = /(["\^\$\[\]\*\.\\])/g; - - const escapeInQuotedStringNix = (fragment) => fragment.replace(/"/g, '\\"'); - const escapeInQuotedRegexpNix = (fragment) => fragment.replace(SPECIAL_CHARS, "\\$1"); - const searchRegexpNix = (pattern) => `grep "${escapeInQuotedStringNix(pattern)}"`; - const searchFragmentNix = (fragment) => `grep -e "${escapeInQuotedStringNix(fragment)}"`; + const searchRegexpNix = (pattern) => `grep "${escapeInQuotedString(pattern)}"`; + const searchFragmentNix = (fragment) => `grep -e "${escapeInQuotedString(fragment)}"`; return { escape: { - inQuotedString: escapeInQuotedStringNix, - inQuotedRegexp: escapeInQuotedRegexpNix, + inQuotedString: escapeInQuotedString, + inQuotedRegexp: escapeInQuotedRegexp, }, search: { regexp: searchRegexpNix, @@ -36,6 +36,8 @@ function nixImplementation() { }; } -module.exports = process.platform === 'win32' +const isRunningInCMDEXE = process.platform === 'win32' && !process.env['SHELL']; + +module.exports = isRunningInCMDEXE ? win32Implementation() : nixImplementation();