From fc6c82825eb70b073261fad8a821218905d05c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Thu, 23 Apr 2026 17:35:09 +0200 Subject: [PATCH 1/4] Collect staged Android emulator logs --- .../src/__tests__/utils/context.ts | 6 +- .../steps/__tests__/easFunctionGroups.test.ts | 35 +++++++ .../build-tools/src/steps/easFunctions.ts | 2 + .../src/steps/functionGroups/maestroTest.ts | 7 ++ .../__tests__/collectEmulatorLogs.test.ts | 97 +++++++++++++++++++ .../steps/functions/collectEmulatorLogs.ts | 83 ++++++++++++++++ 6 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 packages/build-tools/src/steps/functions/__tests__/collectEmulatorLogs.test.ts create mode 100644 packages/build-tools/src/steps/functions/collectEmulatorLogs.ts diff --git a/packages/build-tools/src/__tests__/utils/context.ts b/packages/build-tools/src/__tests__/utils/context.ts index 298968c42c..a385ee6c51 100644 --- a/packages/build-tools/src/__tests__/utils/context.ts +++ b/packages/build-tools/src/__tests__/utils/context.ts @@ -42,6 +42,7 @@ interface BuildContextParams { runtimePlatform?: BuildRuntimePlatform; projectSourceDirectory?: string; projectTargetDirectory?: string; + buildLogsDirectory?: string; relativeWorkingDirectory?: string; staticContextContent?: Record; } @@ -53,6 +54,7 @@ export function createStepContextMock({ runtimePlatform, projectSourceDirectory, projectTargetDirectory, + buildLogsDirectory, relativeWorkingDirectory, staticContextContent, }: BuildContextParams = {}): BuildStepContext { @@ -63,6 +65,7 @@ export function createStepContextMock({ runtimePlatform, projectSourceDirectory, projectTargetDirectory, + buildLogsDirectory, relativeWorkingDirectory, staticContextContent, }); @@ -78,6 +81,7 @@ export function createGlobalContextMock({ runtimePlatform, projectSourceDirectory, projectTargetDirectory, + buildLogsDirectory, relativeWorkingDirectory, staticContextContent, }: BuildContextParams = {}): BuildStepGlobalContext { @@ -92,7 +96,7 @@ export function createGlobalContextMock({ relativeWorkingDirectory ? path.resolve(resolvedProjectTargetDirectory, relativeWorkingDirectory) : resolvedProjectTargetDirectory, - '/non/existent/dir', + buildLogsDirectory ?? '/non/existent/dir', staticContextContent ?? {} ), skipCleanup ?? false diff --git a/packages/build-tools/src/steps/__tests__/easFunctionGroups.test.ts b/packages/build-tools/src/steps/__tests__/easFunctionGroups.test.ts index 668901c05f..525c5411d4 100644 --- a/packages/build-tools/src/steps/__tests__/easFunctionGroups.test.ts +++ b/packages/build-tools/src/steps/__tests__/easFunctionGroups.test.ts @@ -1,4 +1,8 @@ +import { Platform } from '@expo/eas-build-job'; + +import { createGlobalContextMock } from '../../__tests__/utils/context'; import { getEasFunctionGroups } from '../easFunctionGroups'; +import { createEasMaestroTestFunctionGroup } from '../functionGroups/maestroTest'; describe(getEasFunctionGroups, () => { it('includes eas/maestro_test for non-build jobs', () => { @@ -24,4 +28,35 @@ describe(getEasFunctionGroups, () => { ); expect(functionGroupIds).toEqual(expect.arrayContaining(['eas/build', 'eas/maestro_test'])); }); + + it('collects Android emulator logs before uploading Maestro results', () => { + const ctx = { + job: { + platform: Platform.ANDROID, + }, + } as unknown as Parameters[0]; + const globalCtx = createGlobalContextMock(); + + const steps = createEasMaestroTestFunctionGroup(ctx).createBuildStepsFromFunctionGroupCall( + globalCtx, + { + callInputs: { + flow_path: 'maestro/home.yml\nmaestro/login.yml', + }, + } + ); + + const stepDisplayNames = steps.map(step => step.displayName); + expect(stepDisplayNames).toEqual([ + 'Install Maestro', + 'Start Android Emulator', + 'Install app to Emulator', + 'maestro test maestro/home.yml', + 'maestro test maestro/login.yml', + 'Collect Android emulator logs', + 'Upload Maestro test results', + ]); + expect(steps[5].ifCondition).toBe('${ always() }'); + expect(steps[6].ifCondition).toBe('${ always() }'); + }); }); diff --git a/packages/build-tools/src/steps/easFunctions.ts b/packages/build-tools/src/steps/easFunctions.ts index b92ea54ba1..7031fabd8c 100644 --- a/packages/build-tools/src/steps/easFunctions.ts +++ b/packages/build-tools/src/steps/easFunctions.ts @@ -2,6 +2,7 @@ import { BuildFunction } from '@expo/steps'; import { calculateEASUpdateRuntimeVersionFunction } from './functions/calculateEASUpdateRuntimeVersion'; import { createCheckoutBuildFunction } from './functions/checkout'; +import { createCollectEmulatorLogsBuildFunction } from './functions/collectEmulatorLogs'; import { configureAndroidVersionFunction } from './functions/configureAndroidVersion'; import { configureEASUpdateIfInstalledFunction } from './functions/configureEASUpdateIfInstalled'; import { configureIosCredentialsFunction } from './functions/configureIosCredentials'; @@ -49,6 +50,7 @@ import { CustomBuildContext } from '../customBuildContext'; export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] { const functions = [ createCheckoutBuildFunction(), + createCollectEmulatorLogsBuildFunction(), createDownloadArtifactFunction(), createUploadArtifactBuildFunction(ctx), createSetUpNpmrcBuildFunction(), diff --git a/packages/build-tools/src/steps/functionGroups/maestroTest.ts b/packages/build-tools/src/steps/functionGroups/maestroTest.ts index 6f4be9f422..ba7cf091c6 100644 --- a/packages/build-tools/src/steps/functionGroups/maestroTest.ts +++ b/packages/build-tools/src/steps/functionGroups/maestroTest.ts @@ -7,6 +7,7 @@ import { } from '@expo/steps'; import { CustomBuildContext } from '../../customBuildContext'; +import { createCollectEmulatorLogsBuildFunction } from '../functions/collectEmulatorLogs'; import { createInstallMaestroBuildFunction } from '../functions/installMaestro'; import { createStartAndroidEmulatorBuildFunction } from '../functions/startAndroidEmulator'; import { createStartIosSimulatorBuildFunction } from '../functions/startIosSimulator'; @@ -139,6 +140,12 @@ export function createEasMaestroTestFunctionGroup( ); } + steps.push( + createCollectEmulatorLogsBuildFunction().createBuildStepFromFunctionCall(globalCtx, { + ifCondition: '${ always() }', + }) + ); + steps.push( createUploadArtifactBuildFunction(buildToolsContext).createBuildStepFromFunctionCall( globalCtx, diff --git a/packages/build-tools/src/steps/functions/__tests__/collectEmulatorLogs.test.ts b/packages/build-tools/src/steps/functions/__tests__/collectEmulatorLogs.test.ts new file mode 100644 index 0000000000..580778ae8e --- /dev/null +++ b/packages/build-tools/src/steps/functions/__tests__/collectEmulatorLogs.test.ts @@ -0,0 +1,97 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { createGlobalContextMock } from '../../../__tests__/utils/context'; +import { createMockLogger } from '../../../__tests__/utils/logger'; +import { createCollectEmulatorLogsBuildFunction } from '../collectEmulatorLogs'; + +function createStep(callInputs?: Record, envOverrides?: NodeJS.ProcessEnv) { + const logger = createMockLogger(); + const fn = createCollectEmulatorLogsBuildFunction(); + const buildLogsDirectory = + typeof envOverrides?.BUILD_LOGS_DIRECTORY === 'string' + ? envOverrides.BUILD_LOGS_DIRECTORY + : undefined; + const globalCtx = createGlobalContextMock({ logger, buildLogsDirectory }); + globalCtx.updateEnv({ HOME: '/home/expo', ...envOverrides }); + const step = fn.createBuildStepFromFunctionCall(globalCtx, { + callInputs, + }); + return Object.assign(step, { logger }); +} + +describe(createCollectEmulatorLogsBuildFunction, () => { + it('copies staged logs to the destination path', async () => { + const buildLogsDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'build-logs-')); + const sourcePath = path.join(buildLogsDirectory, 'android-emulator-logcat'); + const destinationPath = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'logcat-destination-') + ); + await fs.promises.mkdir(sourcePath, { recursive: true }); + const logPath = path.join(sourcePath, 'emulator-5554.log'); + await fs.promises.writeFile(logPath, 'log line\n'); + + await createStep( + { + destination_path: destinationPath, + }, + { + BUILD_LOGS_DIRECTORY: buildLogsDirectory, + } + ).executeAsync(); + + await expect( + fs.promises.readFile(path.join(destinationPath, 'emulator-5554.log'), 'utf-8') + ).resolves.toBe('log line\n'); + }); + + it('copies staged logs in parallel and skips metadata files', async () => { + const buildLogsDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'build-logs-')); + const sourcePath = path.join(buildLogsDirectory, 'android-emulator-logcat'); + const destinationPath = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'logcat-destination-') + ); + await fs.promises.mkdir(sourcePath, { recursive: true }); + await fs.promises.writeFile(path.join(sourcePath, '1234-emulator-5554.log'), 'first\n'); + await fs.promises.writeFile(path.join(sourcePath, '5678-emulator-5556.log'), 'second\n'); + await fs.promises.writeFile(path.join(sourcePath, '5678-emulator-5556.log.json'), '{}\n'); + + const step = createStep( + { + destination_path: destinationPath, + }, + { + BUILD_LOGS_DIRECTORY: buildLogsDirectory, + } + ); + await expect(step.executeAsync()).resolves.toBeUndefined(); + await expect( + fs.promises.readFile(path.join(destinationPath, '1234-emulator-5554.log'), 'utf-8') + ).resolves.toBe('first\n'); + await expect( + fs.promises.readFile(path.join(destinationPath, '5678-emulator-5556.log'), 'utf-8') + ).resolves.toBe('second\n'); + await expect( + fs.promises.access(path.join(destinationPath, '5678-emulator-5556.log.json')) + ).rejects.toThrow(); + }); + + it('warns but does not fail when the staging directory is missing', async () => { + const buildLogsDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'build-logs-')); + const sourcePath = path.join(buildLogsDirectory, 'android-emulator-logcat'); + const step = createStep( + { + destination_path: await fs.promises.mkdtemp(path.join(os.tmpdir(), 'logcat-destination-')), + }, + { + BUILD_LOGS_DIRECTORY: buildLogsDirectory, + } + ); + + await expect(step.executeAsync()).resolves.toBeUndefined(); + expect(step.ctx.logger.warn).toHaveBeenCalledWith( + `No Android emulator logcat staging directory found at ${sourcePath}.` + ); + }); +}); diff --git a/packages/build-tools/src/steps/functions/collectEmulatorLogs.ts b/packages/build-tools/src/steps/functions/collectEmulatorLogs.ts new file mode 100644 index 0000000000..252dcadcab --- /dev/null +++ b/packages/build-tools/src/steps/functions/collectEmulatorLogs.ts @@ -0,0 +1,83 @@ +import { bunyan } from '@expo/logger'; +import { BuildFunction, BuildStepInput, BuildStepInputValueTypeName } from '@expo/steps'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { AndroidEmulatorUtils } from '../../utils/AndroidEmulatorUtils'; + +export function createCollectEmulatorLogsBuildFunction(): BuildFunction { + return new BuildFunction({ + namespace: 'eas', + id: 'collect_emulator_logs', + name: 'Collect Android emulator logs', + __metricsId: 'eas/collect_emulator_logs', + inputProviders: [ + BuildStepInput.createProvider({ + id: 'destination_path', + required: false, + defaultValue: '${{ env.HOME }}/.maestro/tests/emulator-logs', + allowedValueTypeName: BuildStepInputValueTypeName.STRING, + }), + ], + fn: async ({ logger, global }, { inputs }) => { + const sourcePath = AndroidEmulatorUtils.getLogcatStagingDirectoryPath({ + buildLogsDirectory: global.buildLogsDirectory, + }); + const destinationPath = `${inputs.destination_path.value}`; + + await collectEmulatorLogsAsync({ sourcePath, destinationPath, logger }); + }, + }); +} + +async function collectEmulatorLogsAsync({ + sourcePath, + destinationPath, + logger, +}: { + sourcePath: string; + destinationPath: string; + logger: bunyan; +}): Promise { + let directoryEntries: string[]; + try { + directoryEntries = await fs.promises.readdir(sourcePath); + } catch (err: any) { + if (err?.code === 'ENOENT') { + logger.warn(`No Android emulator logcat staging directory found at ${sourcePath}.`); + return; + } + logger.warn({ err }, `Failed to read Android emulator logcat staging directory ${sourcePath}.`); + return; + } + + const logFiles = directoryEntries.filter(entry => entry.endsWith('.log')); + if (logFiles.length === 0) { + logger.warn(`No Android emulator logcat files found at ${sourcePath}.`); + return; + } + + try { + await fs.promises.mkdir(destinationPath, { recursive: true }); + } catch (err) { + logger.warn({ err }, `Failed to create Android emulator log destination ${destinationPath}.`); + return; + } + + const copyResults = await Promise.all( + logFiles.map(async logFile => { + const sourceFilePath = path.join(sourcePath, logFile); + const destinationFilePath = path.join(destinationPath, logFile); + try { + await fs.promises.copyFile(sourceFilePath, destinationFilePath); + return true; + } catch (err) { + logger.warn({ err }, `Failed to copy Android emulator log ${sourceFilePath}.`); + return false; + } + }) + ); + const copiedCount = copyResults.filter(Boolean).length; + + logger.info(`Collected ${copiedCount} Android emulator logcat file(s) to ${destinationPath}.`); +} From d507962b8e81e609cc1ad0034c1dec7352e59788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Thu, 23 Apr 2026 18:45:40 +0200 Subject: [PATCH 2/4] Add collect emulator logs integration test --- .../collectEmulatorLogs.test.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts diff --git a/packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts b/packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts new file mode 100644 index 0000000000..eb46c32dab --- /dev/null +++ b/packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts @@ -0,0 +1,54 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { createGlobalContextMock } from '../../../__tests__/utils/context'; +import { createMockLogger } from '../../../__tests__/utils/logger'; +import { AndroidEmulatorUtils } from '../../../utils/AndroidEmulatorUtils'; +import { createCollectEmulatorLogsBuildFunction } from '../collectEmulatorLogs'; + +jest.unmock('fs'); +jest.unmock('node:fs'); + +describe('createCollectEmulatorLogsBuildFunction', () => { + it('copies staged emulator logcat files from the build logs directory', async () => { + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'collect-emulator-logs-')); + try { + const buildLogsDirectory = path.join(tempDir, 'build-logs'); + const destinationPath = path.join(tempDir, 'maestro-tests', 'emulator-logs'); + const sourcePath = AndroidEmulatorUtils.getLogcatStagingDirectoryPath({ buildLogsDirectory }); + + await fs.promises.mkdir(sourcePath, { recursive: true }); + await fs.promises.writeFile(path.join(sourcePath, '1111-emulator-5554.log'), 'first log\n'); + await fs.promises.writeFile(path.join(sourcePath, '2222-emulator-5556.log'), 'second log\n'); + await fs.promises.writeFile(path.join(sourcePath, '2222-emulator-5556.log.json'), '{}\n'); + + const collectEmulatorLogs = createCollectEmulatorLogsBuildFunction(); + const step = collectEmulatorLogs.createBuildStepFromFunctionCall( + createGlobalContextMock({ + logger: createMockLogger({ logToConsole: true }), + buildLogsDirectory, + }), + { + callInputs: { + destination_path: destinationPath, + }, + } + ); + + await expect(step.executeAsync()).resolves.not.toThrow(); + + await expect( + fs.promises.readFile(path.join(destinationPath, '1111-emulator-5554.log'), 'utf-8') + ).resolves.toBe('first log\n'); + await expect( + fs.promises.readFile(path.join(destinationPath, '2222-emulator-5556.log'), 'utf-8') + ).resolves.toBe('second log\n'); + await expect( + fs.promises.access(path.join(destinationPath, '2222-emulator-5556.log.json')) + ).rejects.toThrow(); + } finally { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + } + }); +}); From 8d145b3b561527b992f51f96dc2e7e2f2247664e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Thu, 23 Apr 2026 18:49:29 +0200 Subject: [PATCH 3/4] Add emulator logcat end-to-end integration test --- .../collectEmulatorLogs.test.ts | 127 +++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts b/packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts index eb46c32dab..fa99ab6584 100644 --- a/packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts +++ b/packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts @@ -1,10 +1,18 @@ +import { asyncResult } from '@expo/results'; +import spawn from '@expo/turtle-spawn'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { randomUUID } from 'node:crypto'; import { createGlobalContextMock } from '../../../__tests__/utils/context'; import { createMockLogger } from '../../../__tests__/utils/logger'; -import { AndroidEmulatorUtils } from '../../../utils/AndroidEmulatorUtils'; +import { + AndroidEmulatorUtils, + AndroidDeviceSerialId, + AndroidVirtualDeviceName, +} from '../../../utils/AndroidEmulatorUtils'; +import { retryAsync } from '../../../utils/retry'; import { createCollectEmulatorLogsBuildFunction } from '../collectEmulatorLogs'; jest.unmock('fs'); @@ -51,4 +59,121 @@ describe('createCollectEmulatorLogsBuildFunction', () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); } }); + + it('collects streamed logcat output from a running emulator', async () => { + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'collect-emulator-logs-')); + const deviceName = + `android-emulator-logcat-e2e-${randomUUID().slice(0, 8)}` as AndroidVirtualDeviceName; + let emulatorPromise: Promise | null = null; + let serialId: AndroidDeviceSerialId | null = null; + + try { + const logger = createMockLogger({ logToConsole: true }); + const buildLogsDirectory = path.join(tempDir, 'build-logs'); + const destinationPath = path.join(tempDir, 'maestro-tests', 'emulator-logs'); + const sourcePath = AndroidEmulatorUtils.getLogcatStagingDirectoryPath({ buildLogsDirectory }); + const marker = `ENG-20762-${randomUUID()}`; + + await AndroidEmulatorUtils.createAsync({ + deviceName, + systemImagePackage: AndroidEmulatorUtils.defaultSystemImagePackage, + deviceIdentifier: null, + env: process.env, + logger, + }); + + const startResult = await AndroidEmulatorUtils.startAsync({ + deviceName, + env: { ...process.env, ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: '1' }, + }); + serialId = startResult.serialId; + emulatorPromise = asyncResult(startResult.emulatorPromise); + + const streamResult = await AndroidEmulatorUtils.startLogcatStreamingAsync({ + serialId: startResult.serialId, + outputDir: sourcePath, + env: process.env, + logger, + }); + expect(streamResult).not.toBeNull(); + + await AndroidEmulatorUtils.waitForReadyAsync({ + serialId: startResult.serialId, + env: process.env, + }); + + await spawn( + 'adb', + ['-s', startResult.serialId, 'shell', 'log', '-t', 'EAS_CLI_TEST', marker], + { env: process.env } + ); + + await retryAsync( + async () => { + const stagedLogFiles = (await fs.promises.readdir(sourcePath)).filter(entry => + entry.endsWith('.log') + ); + expect(stagedLogFiles.length).toBeGreaterThan(0); + const stagedLogPath = path.join(sourcePath, stagedLogFiles[0]); + const contents = await fs.promises.readFile(stagedLogPath, 'utf-8'); + if (!contents.includes(marker)) { + throw new Error(`Did not find marker ${marker} in staged log yet.`); + } + }, + { + logger, + retryOptions: { + retries: 10, + retryIntervalMs: 1_000, + }, + } + ); + + const collectEmulatorLogs = createCollectEmulatorLogsBuildFunction(); + const step = collectEmulatorLogs.createBuildStepFromFunctionCall( + createGlobalContextMock({ + logger, + buildLogsDirectory, + }), + { + callInputs: { + destination_path: destinationPath, + }, + } + ); + + await expect(step.executeAsync()).resolves.not.toThrow(); + + const collectedLogFiles = (await fs.promises.readdir(destinationPath)).filter(entry => + entry.endsWith('.log') + ); + expect(collectedLogFiles.length).toBeGreaterThan(0); + const collectedLogPath = path.join(destinationPath, collectedLogFiles[0]); + await expect(fs.promises.readFile(collectedLogPath, 'utf-8')).resolves.toContain(marker); + } finally { + try { + if (serialId) { + await AndroidEmulatorUtils.deleteAsync({ + serialId, + deviceName, + env: process.env, + }); + } else { + await AndroidEmulatorUtils.deleteAsync({ + deviceName, + env: process.env, + }); + } + } catch (error) { + console.warn( + 'Failed to clean up emulator during collectEmulatorLogs integration test', + error + ); + } + if (emulatorPromise) { + await emulatorPromise; + } + await fs.promises.rm(tempDir, { recursive: true, force: true }); + } + }, 180_000); }); From f0cc102ea1a02133a4089ab7e9452bf26b786b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Thu, 23 Apr 2026 18:54:29 +0200 Subject: [PATCH 4/4] Format collect emulator logs integration test --- .../functions/__integration-tests__/collectEmulatorLogs.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts b/packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts index fa99ab6584..78ccde35f3 100644 --- a/packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts +++ b/packages/build-tools/src/steps/functions/__integration-tests__/collectEmulatorLogs.test.ts @@ -8,8 +8,8 @@ import { randomUUID } from 'node:crypto'; import { createGlobalContextMock } from '../../../__tests__/utils/context'; import { createMockLogger } from '../../../__tests__/utils/logger'; import { - AndroidEmulatorUtils, AndroidDeviceSerialId, + AndroidEmulatorUtils, AndroidVirtualDeviceName, } from '../../../utils/AndroidEmulatorUtils'; import { retryAsync } from '../../../utils/retry';