Skip to content

Commit b983427

Browse files
committed
test(android): cold boot emulator for reliable test runs
Force Detox TestingAVD launches and Jet retries to skip Quick Boot snapshot restore, which could leave the emulator offline on a gray screen.
1 parent cca33ce commit b983427

4 files changed

Lines changed: 114 additions & 6 deletions

File tree

okf-bundle/testing/running-e2e.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,34 @@ See also: [unit-focused-tier loop](#unit-focused-tier-iteration-loop), [dispatch
502502
- **adb empty**`adb kill-server && adb start-server && adb devices`
503503
- **Stale processes** — one Metro (`:8081`), one emulator set (`:8080`, `:9099`, `:9000`, `:4400`, …). Stray listener on `:8090` after a run → [pre-flight recovery](#pre-flight-recovery), then restart background services with [Rules §1–2](#rules) (`yarn tests:packager:jet`, `yarn tests:emulator:start`).
504504

505-
### iOS Detox framework cache (blocking)
505+
### Android emulator gray screen / Quick Boot (blocking)
506+
507+
Detox's default emulator launch **restores the AVD Quick Boot snapshot** unless told otherwise. On `TestingAVD` that can leave the device **`offline` on a gray screen**`adb devices` shows `emulator-XXXX offline` and Detox hangs on `wait-for-device`.
508+
509+
**Root cause:** warm boot paths — Quick Boot snapshot restore on Detox launch, and (pre-fix) `adb reboot` on Jet retry — skip a full cold boot.
510+
511+
**Fix (committed):** [`tests/.detoxrc.js`](../../tests/.detoxrc.js) sets `bootArgs: '-no-snapshot-load -no-snapshot-save'` on the `TestingAVD` device. Jet retry in [`tests/e2e/firebase.test.js`](../../tests/e2e/firebase.test.js) cold-restarts the same emulator (kill + relaunch with the same args) instead of `adb reboot`.
512+
513+
**Detect:**
514+
515+
```bash
516+
adb devices -l # emulator-XXXX offline
517+
pgrep -fl 'qemu-system.*TestingAVD'
518+
rg 'SPAWN_CMD.*@TestingAVD' /tmp/rnfb-e2e-android.log # no -no-snapshot-load → stale runbook / config
519+
```
520+
521+
**Recovery before `:test-cover`:**
522+
523+
```bash
524+
adb -s emulator-5554 emu kill 2>/dev/null || true
525+
pkill -f 'qemu-system.*TestingAVD' 2>/dev/null || true
526+
adb kill-server && adb start-server && adb devices # must be empty
527+
# If gray screen persists after cold-boot config, wipe Quick Boot snapshots:
528+
# rm -rf ~/.android/avd/TestingAVD.avd/snapshots
529+
```
530+
531+
Then rerun [pre-flight](#pre-flight-is-the-host-clear-to-start) and `yarn tests:android:test-cover`. Cold boot adds ~30–60s to the first Android launch vs Quick Boot — expected.
532+
506533

507534
Detox injects a prebuilt **`Detox.framework`** and XCUITest runner from a versioned cache under **`~/Library/Detox/ios/`** (hashed by Xcode version). iOS `:test-cover` / `:build` **fail before any test runs** if that cache is missing or stale (common after Xcode upgrade, first checkout, or a failed Detox postinstall).
508535

tests/.detoxrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ module.exports = {
6262
device: {
6363
avdName: 'TestingAVD',
6464
},
65+
// Quick Boot snapshot restore (default emulator start) can leave TestingAVD stuck
66+
// offline on a gray screen; force full cold boot for every Detox launch.
67+
bootArgs:
68+
process.env.RNFB_ANDROID_EMULATOR_BOOT_ARGS || '-no-snapshot-load -no-snapshot-save',
6569
},
6670
},
6771
configurations: {

tests/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ This action will launch a new simulator (if not already open) and run the tests
105105
> Or you can change this name in the `package.json` of the tests project (don't commit the change though please).
106106
> **DO NOT** rename an existing AVD to this name - it will not work, rename does not change the file path currently so Detox will
107107
> fail to find the AVD in the correct directory. Create a new one with Google Play Services.
108+
> Detox cold-boots `TestingAVD` on every run (`-no-snapshot-load`) to avoid Quick Boot gray-screen failures — see [running e2e § Android gray screen](../okf-bundle/testing/running-e2e.md#android-emulator-gray-screen--quick-boot-blocking).
108109
109110
#### Android
110111

tests/e2e/firebase.test.js

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ const ANDROID_PACKAGE_HANDLER_TIMEOUT_MS = parseInt(
6767
10,
6868
);
6969
const ANDROID_BOOT_SETTLE_MS = parseInt(process.env.RNFB_ANDROID_BOOT_SETTLE_MS || '30000', 10);
70+
const ANDROID_AVD_NAME = process.env.RNFB_ANDROID_AVD_NAME || 'TestingAVD';
71+
const ANDROID_EMULATOR_COLD_BOOT_ARGS = (
72+
process.env.RNFB_ANDROID_EMULATOR_BOOT_ARGS || '-no-snapshot-load -no-snapshot-save'
73+
).trim();
7074
const DRAIN_ORCHESTRATE_TIMEOUT_MS = parseInt(
7175
process.env.RNFB_DRAIN_ORCHESTRATE_TIMEOUT_MS || '30000',
7276
10,
@@ -277,6 +281,31 @@ function resolveAdbPath() {
277281
return sdkRoot ? path.join(sdkRoot, 'platform-tools', 'adb') : 'adb';
278282
}
279283

284+
function resolveEmulatorPath() {
285+
const sdkRoot = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
286+
if (!sdkRoot) {
287+
throw new Error('ANDROID_HOME or ANDROID_SDK_ROOT is required to cold boot the Android emulator');
288+
}
289+
return path.join(sdkRoot, 'emulator', 'emulator');
290+
}
291+
292+
function resolveAndroidEmulatorPort(serial) {
293+
const portMatch = serial.match(/^emulator-(\d+)$/);
294+
return portMatch ? portMatch[1] : '5554';
295+
}
296+
297+
function adbDeviceState(serial) {
298+
const adb = resolveAdbPath();
299+
try {
300+
return execSync(`${adb} -s ${serial} get-state`, {
301+
encoding: 'utf8',
302+
timeout: 5000,
303+
}).trim();
304+
} catch (_) {
305+
return 'unknown';
306+
}
307+
}
308+
280309
function resolveAndroidSerial() {
281310
try {
282311
if (typeof device !== 'undefined' && device?.id) {
@@ -409,13 +438,49 @@ async function ensureAndroidJetHostClear(label = 'android-jet-host-clear') {
409438
console.log(`[rnfb-e2e] ${label}: host clear`);
410439
}
411440

412-
function rebootAndroidEmulator() {
441+
function coldBootAndroidEmulator() {
413442
const adb = resolveAdbPath();
414443
const serial = resolveAndroidSerial();
415-
console.warn(`[rnfb-e2e] Rebooting Android emulator serial=${serial} via adb reboot`);
444+
const port = resolveAndroidEmulatorPort(serial);
445+
const coldBootSerial = `emulator-${port}`;
446+
const emulatorPath = resolveEmulatorPath();
447+
const extraBootArgs = ANDROID_EMULATOR_COLD_BOOT_ARGS.split(/\s+/).filter(Boolean);
448+
449+
console.warn(
450+
`[rnfb-e2e] Cold booting Android emulator serial=${serial} avd=${ANDROID_AVD_NAME} ` +
451+
`(kill + relaunch with ${extraBootArgs.join(' ')})`,
452+
);
416453

417-
execSync(`${adb} -s ${serial} reboot`, { stdio: 'inherit' });
418-
execSync(`${adb} -s ${serial} wait-for-device`, {
454+
try {
455+
execSync(`${adb} -s ${serial} emu kill`, { stdio: 'inherit', timeout: 60000 });
456+
} catch (err) {
457+
console.warn(`[rnfb-e2e] emu kill failed (continuing): ${err?.message || err}`);
458+
}
459+
460+
try {
461+
execSync(`pkill -f "qemu-system.*@${ANDROID_AVD_NAME}"`, { stdio: 'ignore', timeout: 5000 });
462+
} catch (_) {
463+
// No matching emulator process.
464+
}
465+
466+
const emulatorArgs = [
467+
'-verbose',
468+
'-no-audio',
469+
'-no-boot-anim',
470+
'-read-only',
471+
'-port',
472+
port,
473+
...extraBootArgs,
474+
`@${ANDROID_AVD_NAME}`,
475+
];
476+
477+
const child = spawn(emulatorPath, emulatorArgs, {
478+
detached: true,
479+
stdio: 'ignore',
480+
});
481+
child.unref();
482+
483+
execSync(`${adb} -s ${coldBootSerial} wait-for-device`, {
419484
stdio: 'inherit',
420485
timeout: REBOOT_ANDROID_EMULATOR_TIMEOUT_MS,
421486
});
@@ -430,6 +495,17 @@ async function waitForAndroidEmulatorReady() {
430495

431496
while (Date.now() < deadline) {
432497
try {
498+
const deviceState = adbDeviceState(serial);
499+
if (deviceState === 'offline') {
500+
stableLoadPolls = 0;
501+
console.warn(
502+
`[rnfb-e2e] android-ready probe serial=${serial} state=offline (Quick Boot gray screen?) — ` +
503+
'kill emulator and rerun; Detox should cold boot with -no-snapshot-load',
504+
);
505+
await sleep(ANDROID_READY_POLL_MS);
506+
continue;
507+
}
508+
433509
const bootCompleted = adbShell(serial, 'getprop sys.boot_completed');
434510
const bootDev = adbShell(serial, 'getprop dev.bootcomplete');
435511
const provisioned = adbShell(serial, 'settings get global device_provisioned');
@@ -1079,7 +1155,7 @@ describe('Jet Tests', function () {
10791155
if (platform === 'ios' && process.platform === 'darwin') {
10801156
await rebootIosSimulator(testsDir);
10811157
} else if (platform === 'android') {
1082-
rebootAndroidEmulator();
1158+
coldBootAndroidEmulator();
10831159
await waitForAndroidEmulatorReady();
10841160
} else {
10851161
try {

0 commit comments

Comments
 (0)