Skip to content

Commit b02ef7f

Browse files
committed
fix(test): cold-boot Android emulator on ANR and missing package/activity services
1 parent 96b03c4 commit b02ef7f

3 files changed

Lines changed: 188 additions & 5 deletions

File tree

‎packages/app/__tests__/androidAdbRange.test.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
adbRangeDiagnosis,
1212
isMetroWaitFailure,
1313
shouldColdBootAndroidOnLaunchRetry,
14+
errorProbeText,
1415
} = require('../../../tests/e2e/androidAdbRange');
1516

1617
const repoRoot = path.resolve(__dirname, '../../..');
@@ -146,6 +147,9 @@ describe('shouldColdBootAndroidOnLaunchRetry', function () {
146147
expect(shouldColdBootAndroidOnLaunchRetry(new Error("cmd: Can't find service: activity"))).toBe(
147148
true,
148149
);
150+
expect(shouldColdBootAndroidOnLaunchRetry(new Error("cmd: Can't find service: settings"))).toBe(
151+
true,
152+
);
149153
expect(
150154
shouldColdBootAndroidOnLaunchRetry(
151155
new Error("APP_UNREACHABLE Detox can't seem to connect to the test app(s)!"),
@@ -156,6 +160,38 @@ describe('shouldColdBootAndroidOnLaunchRetry', function () {
156160
).toBe(true);
157161
});
158162

163+
it('cold-boots when ANR is only on jetOutput (Jet session desync path)', function () {
164+
const err = new Error('Jet tests failed!');
165+
err.jetOutput =
166+
'com.github.anrwatchdog.ANRError: Application Not Responding for at least 5000 ms.';
167+
expect(shouldColdBootAndroidOnLaunchRetry(err)).toBe(true);
168+
// Full device-side probe must not see jetOutput (product titles can say "offline").
169+
expect(errorProbeText(err)).toBe('Jet tests failed!');
170+
});
171+
172+
it("cold-boots when Can't find service is only on Detox stderr (exit code 20)", function () {
173+
const err = new Error('Process exited with code 20');
174+
err.stderr = "cmd: Can't find service: package";
175+
expect(shouldColdBootAndroidOnLaunchRetry(err)).toBe(true);
176+
expect(errorProbeText(err)).toMatch(/Can't find service: package/);
177+
});
178+
179+
it('does not cold-boot when jetOutput only contains a Mocha title with offline', function () {
180+
const err = new Error('Jet tests failed!');
181+
err.jetOutput =
182+
' 1) database()\n Reference\n removes a node whilst offline:\n Error: expected true to equal false';
183+
expect(shouldColdBootAndroidOnLaunchRetry(err)).toBe(false);
184+
expect(errorProbeText(err)).toBe('Jet tests failed!');
185+
});
186+
187+
it('cold-boots when launch timeout is enriched with logcat ANR signal', function () {
188+
expect(
189+
shouldColdBootAndroidOnLaunchRetry(
190+
new Error('[rnfb-e2e] launchApp timed out after 300000ms\nANR detected in recent logcat'),
191+
),
192+
).toBe(true);
193+
});
194+
159195
it('does not cold-boot for Jet session noise that is not device-side', function () {
160196
expect(shouldColdBootAndroidOnLaunchRetry(new Error('Jet WS closed 1006'))).toBe(false);
161197
expect(shouldColdBootAndroidOnLaunchRetry(new Error('launchApp timed out'))).toBe(false);

‎tests/e2e/androidAdbRange.js‎

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,14 @@ const METRO_WAIT_FAILURE_RE = /Metro not responding|Metro bundle not available|p
5656
// Android cold-boot is only for device-side launch/health faults (ANR, offline, qemu/adb,
5757
// package-manager / ActivityManager collapse, install SIGTERM under load, unreachable app).
5858
// Metro wait failures are excluded via isMetroWaitFailure before this regex runs.
59+
// Includes Detox SPAWN_STDERR / Jet output phrasing: "Can't find service: package|activity|settings".
5960
const ANDROID_DEVICE_SIDE_LAUNCH_RE =
60-
/ANR|\boffline\b|qemu-without-adb|did not become 'device'|cold-boot spawn did not register|unknown to FrontBoard|FBSOpenApplicationServiceErrorDomain|Can't find service: package|Can't find service: activity|terminated with SIGTERM|Process was killed with SIGTERM|APP_UNREACHABLE|Failed to run application/i;
61+
/ANR|\boffline\b|qemu-without-adb|did not become 'device'|cold-boot spawn did not register|unknown to FrontBoard|FBSOpenApplicationServiceErrorDomain|Can't find service: (?:package|activity|settings)|terminated with SIGTERM|Process was killed with SIGTERM|APP_UNREACHABLE|Failed to run application/i;
62+
63+
// jetOutput is Mocha/Jet log text (product titles like "whilst offline"). Only guest-death
64+
// phrases — never the full device-side token set (\boffline\b, SIGTERM, APP_UNREACHABLE, …).
65+
const ANDROID_JET_OUTPUT_GUEST_DEATH_RE =
66+
/ANRError|Application Not Responding|Can't find service: (?:package|activity|settings)/i;
6167

6268
function errorMessage(messageOrErr) {
6369
if (typeof messageOrErr === 'string') {
@@ -66,18 +72,44 @@ function errorMessage(messageOrErr) {
6672
return messageOrErr?.message || '';
6773
}
6874

75+
// Detox often surfaces only "Process exited with code N" in err.message while the
76+
// actionable "Can't find service: …" lives on err.stderr / stdout. Full device-side
77+
// regex applies here only — not to jetOutput (see ANDROID_JET_OUTPUT_GUEST_DEATH_RE).
78+
function errorProbeText(messageOrErr) {
79+
if (typeof messageOrErr === 'string') {
80+
return messageOrErr;
81+
}
82+
if (!messageOrErr || typeof messageOrErr !== 'object') {
83+
return '';
84+
}
85+
return [messageOrErr.message, messageOrErr.stderr, messageOrErr.stdout]
86+
.filter(part => part != null && String(part).length > 0)
87+
.map(String)
88+
.join('\n');
89+
}
90+
91+
function errorJetOutputText(messageOrErr) {
92+
if (typeof messageOrErr === 'string' || !messageOrErr || typeof messageOrErr !== 'object') {
93+
return '';
94+
}
95+
return messageOrErr.jetOutput != null ? String(messageOrErr.jetOutput) : '';
96+
}
97+
6998
function isMetroWaitFailure(messageOrErr) {
7099
return METRO_WAIT_FAILURE_RE.test(errorMessage(messageOrErr));
71100
}
72101

73102
// True only when an Android Jet retry should kill+relaunch qemu. Metro wait timeouts
74103
// must leave the emulator alone and re-wait for packager-status:running / bundle.
75104
function shouldColdBootAndroidOnLaunchRetry(messageOrErr) {
76-
const message = errorMessage(messageOrErr);
77-
if (isMetroWaitFailure(message)) {
105+
// Metro exclusion stays message-primary so a polluted jetOutput cannot force a cold boot.
106+
if (isMetroWaitFailure(messageOrErr)) {
78107
return false;
79108
}
80-
return ANDROID_DEVICE_SIDE_LAUNCH_RE.test(message);
109+
if (ANDROID_DEVICE_SIDE_LAUNCH_RE.test(errorProbeText(messageOrErr))) {
110+
return true;
111+
}
112+
return ANDROID_JET_OUTPUT_GUEST_DEATH_RE.test(errorJetOutputText(messageOrErr));
81113
}
82114

83115
function adbRangeDiagnosis(serial, avdName) {
@@ -116,6 +148,8 @@ module.exports = {
116148
adbRangeDiagnosis,
117149
isMetroWaitFailure,
118150
shouldColdBootAndroidOnLaunchRetry,
151+
errorProbeText,
119152
METRO_WAIT_FAILURE_RE,
120153
ANDROID_DEVICE_SIDE_LAUNCH_RE,
154+
ANDROID_JET_OUTPUT_GUEST_DEATH_RE,
121155
};

‎tests/e2e/firebase.test.js‎

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,89 @@ function adbShell(serial, command, timeoutMs = 15000) {
531531
}).trim();
532532
}
533533

534+
// After ANR / system_server death, boot_completed can still be 1 while package /
535+
// activity binders are gone — Detox then fails pm install with "Can't find service".
536+
function androidCoreServicesPresent(serial) {
537+
try {
538+
const list = adbShell(serial, 'service list', 15000);
539+
return {
540+
hasPackage: /\bpackage:/.test(list),
541+
hasActivity: /\bactivity:/.test(list),
542+
list,
543+
};
544+
} catch (err) {
545+
return {
546+
hasPackage: false,
547+
hasActivity: false,
548+
list: '',
549+
error: err?.message || String(err),
550+
};
551+
}
552+
}
553+
554+
// promisify-child-process leaves "Can't find service: …" on err.stderr while
555+
// err.message is only "Process exited with code N". Fold so cold-boot matching
556+
// and retry logs see the device-side fault without reading Detox SPAWN_STDERR alone.
557+
function foldDetoxChildProcessOutputIntoError(err) {
558+
if (!err || typeof err !== 'object') {
559+
return err;
560+
}
561+
const extras = [err.stderr, err.stdout]
562+
.filter(part => part != null && String(part).trim().length > 0)
563+
.map(part => String(part).trim());
564+
if (extras.length === 0) {
565+
return err;
566+
}
567+
const folded = extras.join('\n');
568+
const current = String(err.message || '');
569+
if (current.includes(folded) || folded.split('\n').every(line => current.includes(line))) {
570+
return err;
571+
}
572+
err.message = current ? `${current}\n${folded}` : folded;
573+
return err;
574+
}
575+
576+
// launchApp timeouts / Jet desync often leave ANR only in Detox console + logcat,
577+
// or leave boot_completed=1 while package/activity binders are already gone.
578+
// Best-effort append so shouldColdBootAndroidOnLaunchRetry sees device-side faults.
579+
function enrichAndroidLaunchFailureWithGuestSignals(err) {
580+
if (!err || typeof err !== 'object') {
581+
return err;
582+
}
583+
if (shouldColdBootAndroidOnLaunchRetry(err)) {
584+
return err;
585+
}
586+
try {
587+
const serial = resolveAndroidSerial();
588+
const services = androidCoreServicesPresent(serial);
589+
if (!services.hasPackage || !services.hasActivity) {
590+
const missing = [
591+
!services.hasPackage ? "Can't find service: package" : null,
592+
!services.hasActivity ? "Can't find service: activity" : null,
593+
]
594+
.filter(Boolean)
595+
.join('\n');
596+
err.message = `${err.message || ''}\n${missing}`.trim();
597+
console.warn(
598+
`[rnfb-e2e] android guest-signal: core services missing package=${services.hasPackage} activity=${services.hasActivity}`,
599+
);
600+
return err;
601+
}
602+
const adb = resolveAdbPath();
603+
const logTail = execSync(`${adb} -s ${serial} logcat -d -t 400`, {
604+
encoding: 'utf8',
605+
timeout: 15000,
606+
});
607+
if (/ANRError|Application Not Responding|ANR in /i.test(logTail)) {
608+
err.message = `${err.message || ''}\nANR detected in recent logcat`.trim();
609+
console.warn('[rnfb-e2e] android guest-signal: ANR found in recent logcat');
610+
}
611+
} catch (probeErr) {
612+
console.warn(`[rnfb-e2e] android guest-signal probe failed: ${probeErr?.message || probeErr}`);
613+
}
614+
return err;
615+
}
616+
534617
function parseGuestLoad1Min(loadavgLine) {
535618
const first = loadavgLine.trim().split(/\s+/)[0];
536619
const load = parseFloat(first);
@@ -827,6 +910,21 @@ async function waitForAndroidEmulatorReady() {
827910
continue;
828911
}
829912

913+
// Gate before wait-for-handler / pm install: binders must be listed.
914+
const coreServices = androidCoreServicesPresent(serial);
915+
if (!coreServices.hasPackage || !coreServices.hasActivity) {
916+
stableLoadPolls = 0;
917+
packageHandlerDone = false;
918+
console.log(
919+
`[rnfb-e2e] android-ready probe core-services package=${coreServices.hasPackage} ` +
920+
`activity=${coreServices.hasActivity}` +
921+
(coreServices.error ? ` error=${coreServices.error}` : '') +
922+
' (waiting for service list)',
923+
);
924+
await sleep(ANDROID_READY_POLL_MS);
925+
continue;
926+
}
927+
830928
if (!packageHandlerDone) {
831929
console.log('[rnfb-e2e] android-ready probe waiting for package handler queue...');
832930
try {
@@ -1395,6 +1493,7 @@ async function killJetForLaunchRetry(jetProcess, reason = 'launch-retry') {
13951493
async function launchAppWithRetry(launchArgs, { testsDir, onBeforeRelaunch } = {}) {
13961494
const liveMetro = usesLiveMetro();
13971495
let lastLaunchFailure;
1496+
let deleteAppAfterColdBoot = false;
13981497

13991498
for (let launchAttempt = 1; launchAttempt <= LAUNCH_APP_MAX_ATTEMPTS; launchAttempt++) {
14001499
try {
@@ -1411,6 +1510,8 @@ async function launchAppWithRetry(launchArgs, { testsDir, onBeforeRelaunch } = {
14111510
);
14121511
await coldBootAndroidEmulator();
14131512
await waitForAndroidEmulatorReady();
1513+
// Reinstall after guest death — prior APK/instrumentation may be gone or corrupt.
1514+
deleteAppAfterColdBoot = true;
14141515
}
14151516
if (onBeforeRelaunch) {
14161517
await onBeforeRelaunch();
@@ -1428,11 +1529,17 @@ async function launchAppWithRetry(launchArgs, { testsDir, onBeforeRelaunch } = {
14281529
await new Promise(resolve => setTimeout(resolve, 2000));
14291530
}
14301531

1532+
const deleteApp = launchAttempt === 1 || deleteAppAfterColdBoot;
1533+
deleteAppAfterColdBoot = false;
14311534
await launchAppWithTimeout(launchArgs, {
1432-
deleteApp: launchAttempt === 1,
1535+
deleteApp,
14331536
});
14341537
return;
14351538
} catch (err) {
1539+
foldDetoxChildProcessOutputIntoError(err);
1540+
if (detoxPlatformKey() === 'android') {
1541+
enrichAndroidLaunchFailureWithGuestSignals(err);
1542+
}
14361543
lastLaunchFailure = err;
14371544
console.warn(`[rnfb-e2e] launchApp failure reason=${err?.message || err}`);
14381545
logLaunchInstallState(`after-launch-failure attempt=${launchAttempt}`);
@@ -1691,6 +1798,8 @@ describe('Jet Tests', function () {
16911798
if (platform === 'ios' && process.platform === 'darwin') {
16921799
await rebootIosSimulator(testsDir);
16931800
} else if (platform === 'android') {
1801+
foldDetoxChildProcessOutputIntoError(lastFailure);
1802+
enrichAndroidLaunchFailureWithGuestSignals(lastFailure);
16941803
if (shouldColdBootAndroidOnLaunchRetry(lastFailure)) {
16951804
console.warn('[rnfb-e2e] Android device-side launch failure — cold booting emulator');
16961805
await coldBootAndroidEmulator();
@@ -1717,6 +1826,10 @@ describe('Jet Tests', function () {
17171826
await runJetE2eAttempt(attempt);
17181827
break;
17191828
} catch (err) {
1829+
foldDetoxChildProcessOutputIntoError(err);
1830+
if (platform === 'android') {
1831+
enrichAndroidLaunchFailureWithGuestSignals(err);
1832+
}
17201833
lastFailure = err;
17211834
logRetryEligibility(err, attempt);
17221835
if (isRetryableCloudQuotaFailure(err?.jetOutput || '')) {

0 commit comments

Comments
 (0)