Skip to content

Commit c206a0b

Browse files
committed
Refactor Android boot fast lookup for testability
1 parent 74d4f08 commit c206a0b

4 files changed

Lines changed: 73 additions & 13 deletions

File tree

src/daemon/handlers/__tests__/session.test.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ test('boot --headless launches Android emulator when no running device matches',
379379
session: 'default',
380380
command: 'boot',
381381
positionals: [],
382-
flags: { platform: 'android', target: 'mobile', device: 'Pixel_9_Pro_XL', headless: true },
382+
flags: { platform: 'android', device: 'Pixel_9_Pro_XL', headless: true },
383383
},
384384
sessionName: 'default',
385385
logPath: path.join(os.tmpdir(), 'daemon.log'),
@@ -391,6 +391,7 @@ test('boot --headless launches Android emulator when no running device matches',
391391
resolveTargetDevice: async () => {
392392
throw new AppError('DEVICE_NOT_FOUND', 'No devices found');
393393
},
394+
resolveAndroidBootSelectorDevice: async () => undefined,
394395
ensureAndroidEmulatorBoot: async ({ avdName, serial, headless }) => {
395396
launchCalls.push({ avdName, serial, headless });
396397
return {
@@ -424,7 +425,7 @@ test('boot launches Android emulator with GUI when no running device matches', a
424425
session: 'default',
425426
command: 'boot',
426427
positionals: [],
427-
flags: { platform: 'android', target: 'mobile', device: 'Pixel_9_Pro_XL' },
428+
flags: { platform: 'android', device: 'Pixel_9_Pro_XL' },
428429
},
429430
sessionName: 'default',
430431
logPath: path.join(os.tmpdir(), 'daemon.log'),
@@ -434,6 +435,7 @@ test('boot launches Android emulator with GUI when no running device matches', a
434435
resolveTargetDevice: async () => {
435436
throw new AppError('DEVICE_NOT_FOUND', 'No devices found');
436437
},
438+
resolveAndroidBootSelectorDevice: async () => undefined,
437439
ensureAndroidEmulatorBoot: async ({ avdName, serial, headless }) => {
438440
launchCalls.push({ avdName, serial, headless });
439441
return {
@@ -466,7 +468,7 @@ test('boot --headless requires avd selector when device cannot be resolved', asy
466468
session: 'default',
467469
command: 'boot',
468470
positionals: [],
469-
flags: { platform: 'android', target: 'mobile', serial: 'emulator-5554', headless: true },
471+
flags: { platform: 'android', serial: 'emulator-5554', headless: true },
470472
},
471473
sessionName: 'default',
472474
logPath: path.join(os.tmpdir(), 'daemon.log'),
@@ -476,6 +478,7 @@ test('boot --headless requires avd selector when device cannot be resolved', asy
476478
resolveTargetDevice: async () => {
477479
throw new AppError('DEVICE_NOT_FOUND', 'No devices found');
478480
},
481+
resolveAndroidBootSelectorDevice: async () => undefined,
479482
ensureAndroidEmulatorBoot: async () => {
480483
bootCalled = true;
481484
throw new Error('unexpected');
@@ -491,6 +494,45 @@ test('boot --headless requires avd selector when device cannot be resolved', asy
491494
}
492495
});
493496

497+
test('boot uses fast Android selector lookup for already booted device', async () => {
498+
const sessionStore = makeSessionStore();
499+
let ensureCalls = 0;
500+
const response = await handleSessionCommands({
501+
req: {
502+
token: 't',
503+
session: 'default',
504+
command: 'boot',
505+
positionals: [],
506+
flags: { platform: 'android', device: 'Pixel 9 Pro XL' },
507+
},
508+
sessionName: 'default',
509+
logPath: path.join(os.tmpdir(), 'daemon.log'),
510+
sessionStore,
511+
invoke: noopInvoke,
512+
ensureReady: async () => {
513+
ensureCalls += 1;
514+
},
515+
resolveTargetDevice: async () => {
516+
throw new Error('resolveTargetDevice should not be called when fast lookup succeeds');
517+
},
518+
resolveAndroidBootSelectorDevice: async () => ({
519+
platform: 'android',
520+
id: 'emulator-5554',
521+
name: 'Pixel 9 Pro XL',
522+
kind: 'emulator',
523+
target: 'mobile',
524+
booted: true,
525+
}),
526+
});
527+
assert.ok(response);
528+
assert.equal(response?.ok, true);
529+
assert.equal(ensureCalls, 0);
530+
if (response && response.ok) {
531+
assert.equal(response.data?.platform, 'android');
532+
assert.equal(response.data?.id, 'emulator-5554');
533+
}
534+
});
535+
494536
test('appstate on iOS requires active session on selected device', async () => {
495537
const sessionStore = makeSessionStore();
496538
const sessionName = 'default';

src/daemon/handlers/session.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ type EnsureAndroidEmulatorBoot = (params: {
5454
headless?: boolean;
5555
}) => Promise<DeviceInfo>;
5656

57+
type ResolveAndroidBootSelectorDevice = (params: {
58+
deviceName?: string;
59+
serial?: string;
60+
}) => Promise<DeviceInfo | undefined>;
61+
5762
const IOS_APPSTATE_SESSION_REQUIRED_MESSAGE =
5863
'iOS appstate requires an active session on the target device. Run open first (for example: open --session sim --platform ios --device "<name>" <app>).';
5964
const BATCH_PARENT_FLAG_KEYS: Array<keyof CommandFlags> = ['platform', 'target', 'device', 'udid', 'serial', 'verbose', 'out'];
@@ -253,6 +258,11 @@ const defaultEnsureAndroidEmulatorBoot: EnsureAndroidEmulatorBoot = async ({ avd
253258
return await ensureAndroidEmulatorBooted({ avdName, serial, headless });
254259
};
255260

261+
const defaultResolveAndroidBootSelectorDevice: ResolveAndroidBootSelectorDevice = async ({ deviceName, serial }) => {
262+
const { resolveAndroidBootSelectorDevice } = await import('../../platforms/android/devices.ts');
263+
return await resolveAndroidBootSelectorDevice({ deviceName, serial });
264+
};
265+
256266
const defaultReinstallOps: ReinstallOps = {
257267
ios: async (device, app, appPath) => {
258268
const { reinstallIosApp } = await import('../../platforms/ios/index.ts');
@@ -470,6 +480,7 @@ export async function handleSessionCommands(params: {
470480
stop: typeof stopAppLog;
471481
};
472482
ensureAndroidEmulatorBoot?: EnsureAndroidEmulatorBoot;
483+
resolveAndroidBootSelectorDevice?: ResolveAndroidBootSelectorDevice;
473484
resolveAndroidPackageForOpen?: (
474485
device: DeviceInfo,
475486
openTarget: string | undefined,
@@ -491,6 +502,7 @@ export async function handleSessionCommands(params: {
491502
stop: stopAppLog,
492503
},
493504
ensureAndroidEmulatorBoot: ensureAndroidEmulatorBootOverride = defaultEnsureAndroidEmulatorBoot,
505+
resolveAndroidBootSelectorDevice: resolveAndroidBootSelectorDeviceOverride = defaultResolveAndroidBootSelectorDevice,
494506
resolveAndroidPackageForOpen: resolveAndroidPackageForOpenOverride = resolveAndroidPackageForOpen,
495507
} = params;
496508
const dispatch = dispatchOverride ?? dispatchCommand;
@@ -587,23 +599,24 @@ export async function handleSessionCommands(params: {
587599
const normalizedPlatform = normalizePlatformSelector(flags.platform) ?? session?.device.platform;
588600
const targetsAndroid = normalizedPlatform === 'android';
589601
const wantsAndroidHeadless = flags.headless === true;
590-
const shouldUseFastAndroidSelectorLookup = targetsAndroid && !flags.target && Boolean(flags.device || flags.serial);
602+
const shouldUseFastAndroidSelectorLookup = targetsAndroid && Boolean(flags.device || flags.serial);
591603
const fallbackAvdName = resolveAndroidEmulatorAvdName({
592604
flags,
593605
sessionDevice: session?.device,
594606
});
595607
const canFallbackLaunchAndroidEmulator = targetsAndroid && Boolean(fallbackAvdName);
596608
let device: DeviceInfo;
597609
let launchedAndroidEmulator = false;
598-
const fastLookupDevice = shouldUseFastAndroidSelectorLookup
599-
? await (async () => {
600-
const { resolveAndroidBootSelectorDevice } = await import('../../platforms/android/devices.ts');
601-
return await resolveAndroidBootSelectorDevice({
602-
deviceName: flags.device,
603-
serial: flags.serial,
604-
});
605-
})()
610+
const fastLookupDeviceCandidate = shouldUseFastAndroidSelectorLookup
611+
? await resolveAndroidBootSelectorDeviceOverride({
612+
deviceName: flags.device,
613+
serial: flags.serial,
614+
})
606615
: undefined;
616+
const targetMismatch = Boolean(flags.target)
617+
&& fastLookupDeviceCandidate
618+
&& (fastLookupDeviceCandidate.target ?? 'mobile') !== flags.target;
619+
const fastLookupDevice = targetMismatch ? undefined : fastLookupDeviceCandidate;
607620
try {
608621
device = fastLookupDevice
609622
?? (await resolveCommandDevice({

src/platforms/android/__tests__/devices.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ test('resolveAndroidBootSelectorDevice matches emulator by device name', async (
189189
assert.ok(device);
190190
assert.equal(device?.id, 'emulator-5554');
191191
assert.equal(device?.kind, 'emulator');
192+
assert.equal(device?.target, 'mobile');
192193
assert.equal(device?.booted, true);
193194
});
194195
});

src/platforms/android/devices.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,16 @@ export async function resolveAndroidBootSelectorDevice(params: {
230230
}
231231

232232
if (!matched) return undefined;
233-
const booted = await isAndroidBooted(matched.serial);
233+
const [booted, target] = await Promise.all([
234+
isAndroidBooted(matched.serial),
235+
resolveAndroidTarget(matched.serial),
236+
]);
234237
return {
235238
platform: 'android',
236239
id: matched.serial,
237240
name: matchedName ?? (matched.rawModel.replace(/_/g, ' ').trim() || matched.serial),
238241
kind: isEmulatorSerial(matched.serial) ? 'emulator' : 'device',
242+
target,
239243
booted,
240244
};
241245
}

0 commit comments

Comments
 (0)