Repository navigation
fix(e2e): release only resources a run recorded; machine wipe is manual-only - #9444
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9444 +/- ##
=============================================
+ Coverage 51.87% 75.01% +23.15%
- Complexity 0 2299 +2299
=============================================
Files 252 629 +377
Lines 11916 41712 +29796
Branches 2557 5242 +2685
=============================================
+ Hits 6180 31286 +25106
- Misses 5052 8849 +3797
- Partials 684 1577 +893
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
efb12b5 to
948c8b3
Compare
653a95e to
e5c9b43
Compare
bacc6c9 to
b92c983
Compare
e5c9b43 to
308ab03
Compare
…al-only E2e runs now record what they start (Metro, the Firebase emulator suite, Jet, emulators and simulators, with a process instance token) under .e2e-run/<identity>/, and release stops only recorded resources. Holders a run did not start are reported with their owner and working directory, and startup fails fast with a free-slot suggestion instead of killing them. The machine-wide sweep is now manual-only (--all-slots), and --include-unrecorded is a manual single-identity recovery. test:full --clear-whole-host exits 2. Shared CI helper scripts no longer kill other owners' Metro or simulator log streams outside CI, and android-e2e-detox.sh refuses to run outside CI.
…mulator A launchApp retry that cold-booted the Android emulator (or rebooted the iOS simulator) never recorded the new instance, so a scoped release reported the host clear and left that run's emulator running. Both reboot paths now record the device they bring up, even when the reboot fails, and only the emulator on this run's serial that traces to this checkout. The cold boot no longer kills every emulator on the AVD by name. It stops only this run's emulator (its console port or recorded instance, traced to this checkout and confirmed by AVD name), and sends emu kill only when that emulator is on its serial. Check and release now report an unrecorded emulator on the identity's AVD instead of staying silent, and another owner's emulator on this identity's serial is BUSY.
tests-macos/.jetrc.js now records Jet's own process (the listener on the Jet and control ports, which is nyc's child under --coverage) from before(), before the Metro wait and the app spawn. After a hung or interrupted macOS run, the default release frees the Jet ports again instead of needing --include-unrecorded. The unrecorded-process hint now lists its causes as examples, including a step that does not record what it starts. Two test files that run the real check and release scripts get a 30 s Jest timeout.
…it runs on Linux On Linux node renames its main thread to MainThread, so pgrep -x never matched the symlinked node and the test failed on the Linux Jest job. A symlink to /bin/sleep keeps the product name as the process name on both macOS and Linux.
The GitHub Linux runner's /bin/sleep is the uutils coreutils multi-call binary, which refuses to run under another name, so the symlinked sleep never started there. Go back to a symlinked node and, on Linux, set process.title, which libuv applies with PR_SET_NAME after node names its main thread MainThread.
…uter retry drainJetAttempt stopped the Jet process the attempt started with. After a launch retry respawned Jet, that process was already gone, so the drain left the respawned Jet holding the Jet port and depended on lsof to find and kill it. The attempt's orchestrate also kept running during the drain and could respawn Jet again. When lsof listed no listener, the drain waited for the port to close and timed out. The attempt context now keeps the Jet session, whose process follows respawns. The drain abandons the session before waiting for its orchestrate, so a launch retry can no longer respawn Jet, then kills the current Jet's process tree while its root is alive. The process is the run's own child, so no port lookup or ownership check is needed. A close-wait timeout now names what lsof sees on the port and each holder's owner.
The drain waits a bounded time for the failed attempt's orchestrate, so that orchestrate can still be running when the next attempt starts. It could then kill the Jet holding the Jet port, which is the next attempt's: a launch retry killed the old Jet before checking whether the attempt was abandoned, the orchestrate failure handler killed it whatever the error, and the kill only skipped a Jet that had been signalled, so a Jet that exited by itself went on to the port cleanup. A launch finishing late also told the next attempt's Jet to start its run. The Jet session moves to tests/e2e/jetSession.js with its spawn, kill and respawn wait injected. An abandoned session refuses to respawn before it kills anything and throws before signalling launch-ready. The failure handler skips the kill once the attempt is abandoned. The launch-retry and stall kill goes through killJetForRetry, which acts only on a Jet that is still running and kills its process tree first, with no SIGTERM to the root that would orphan the listener. The drain's orchestrate wait clears its timer, so it no longer logs a timeout after the orchestrate settled or holds the event loop for 30 s.
…ff the next device - A drained attempt's launch retry now checks for abandonment first, before an Android cold boot, so it cannot reboot the emulator the next attempt uses. Its late launch-ok and orchestrate-failed posts are skipped too. - The drain's terminateApp is bounded (15 s, RNFB_DRAIN_TERMINATE_TIMEOUT_MS). Detox's terminateApp has no timeout; on iOS it ran 81 s before the 30 s orchestrate wait could start. - e2eRunRecord.test.js no longer reads the host's devices: ps, pgrep and pkill hide qemu and launchd_sim processes this run did not start, and adb and simctl list no devices. Another run of the suite, or a real emulator on an identity serial, changed its results, and release's pkill could stop a same-AVD emulator it did not start. - The orphan-listener tests kill their listener by pid, even when they fail.
- The drain starts one terminateApp. The first wait is still capped at 15 s, and the same call is joined again after the Jet port close, until 90 s after it started (RNFB_DRAIN_TERMINATE_JOIN_TIMEOUT_MS). A terminate that outlives the cap no longer lands on the next attempt's freshly launched app; a fast one adds no time. Only a terminate still running after 90 s is left behind, with a warning. - e2eRunRecord.test.js host stubs: ps hides unmarked devices in every listing form (-ax, -A, -e) and per-pid query. pkill accepts only -f, -<signal number> and -<SIGNAME> with one pattern, and exits 2 on anything else instead of reading -P or -U as a signal.
After an await-exit stall the session kills its Jet, then the orchestrate failure cleanup ran killJetForRetry on the same process before its exit was observed, logging launch-retry-kill-jet. The session now records the Jet it killed, and the failure cleanup skips that same process. A respawned Jet is a new process and is still killed.
b92c983 to
5fd8751
Compare
|
This looks like quite a bit of churn I know but I'm making a concerted effort to solve build/test capacity as a bottleneck Parallel design had an initial take with slot-based runs and that was successful enough to prompt further investigations. Now I'm scaling parallel design to a federated pool of hosts and with multiple users potentially using the same host now we need a bit more safety with regard to what is started/stopped with regards to our test resources This PR implements that for RNFB Quite a few changes stacked up - all infra + test - need to get them through, merging this |
Stack position: 3 of 4. Base:
generic-tooling-fixes. Merge after #9447. Stack order: #9446 (build concurrency cap) → #9447 (tooling and test fixes) → this PR → #9448 (shared-host e2e hardening).Description
An e2e run should only ever touch what it started. Until now,
yarn tests:e2e:release, the Metro start, the Jet harness andyarn test:fullfreed resources by identity: every listener on the slot's ports,killallby app name, the simulator or AVD by name, and any strayemulator-*serial. When two checkouts shared an identity or a port, one could kill the other's live run.This PR records what each run starts and frees only that. Wiping the whole machine becomes a manual step.
Run record
scripts/e2e/lib/e2e-run-record.shandtests/e2e/runRecord.js, stored under.e2e-run/<slot-N|serial>/(git-ignored;RNFB_E2E_RUN_RECORD_DIRoverrides the location).tests-macos/.jetrc.js), plus the macOS app and the emulator or simulator instance the run booted or used.-port, or thelaunchd_simpid + start time). Devices are re-recorded after every harness reboot, including when the reboot throws, and when Detox booted one but the test never started. Another owner's emulator, on any port, is never recorded.emu killonly afteremu avd namematches; one SIGKILL if SIGTERM is ignored). It no longer runspkillby AVD name.Release and check
yarn tests:e2e:releasefrees recorded resources only: recorded listeners on the identity's ports, the recorded macOS app pid (neverkillall), and with--devicesthe recorded simulator by UDID and the recorded emulator (or one a recorded process spawned), only while that exact instance is alive. The adb server is never stopped. Anything else is reportedowner=… left runningwith its command, working directory and a hint, and release exits non-zero if the resource is still busy.yarn tests:e2e:checklabels every holderowner=this-checkout(release frees it),owner=this-checkout-unrecorded(printsyarn tests:e2e:release --include-unrecorded --only <category>) orowner=unrecorded(names the first free slot and itsexport-slot-envoverride).emulator-*serials are BUSY only when this checkout recorded or spawned them; others are INFO with owner, working directory and AVD. Another owner's emulator on the identity's own serial is BUSY. Serial iOS check fails only when the identity's own simulator is booted.Fail fast instead of taking over
killTcpPortListener) and macOS.jetrc.jsbefore()fail fast on a holder they did not start.lsofto find the rest; whenlsoflisted nothing, the wait for the Jet port to close timed out. A close-wait timeout now names whatlsofsees on the port and each holder's owner.terminateApphas no timeout of its own; on iOS one ran 81 s before the orchestrate wait could start.adbpoll is slow). On Android the drain also force-stops the two apps once before the orchestrate wait. If the instrumentation wait or the port close times out, it throws and the drain fails instead of waiting longer.launchAppthe attempt already had in flight may still finish on the device: the 120 s launch timeout stops the wait, not the launch, and the drain waits for it only within the 30 s orchestrate wait. In detail:launch-oknor signals launch-ready to the next attempt's Jet.tests/e2e/jetSession.js, with its spawn and kill injected, so these paths are tested with real processes.test-expoandtest-rn-bareios:launch-smokescripts no longerkill -9everything on:8081; they stop only the Metro they spawned.simulator-logging.shkillsbootedlog streams only on single-tenant runners (CI=trueorRNFB_SIM_UI_RESET=1). Local iOS Jet retries no longer kill the Simulator or DeviceHub UI.android-e2e-detox.shrefuses to run outside CI.Manual-only recovery
--include-unrecorded, run in that identity's env, also frees this checkout's unrecorded holders (ports, macOS app by name, and with--devicesthis checkout's unrecorded emulator on the identity's AVD). Another owner's device and apps are reported and left running.--all-slotsis the whole-machine wipe.yarn test:full --clear-whole-hostand--clear-allare removed; both exit 2 and point to the manual command. No script,yarn test:fullor CI path passes either flag.Unchanged
The slot model, the port formula, env names and the
yarn tests:e2e:*surface. GitHub-hosted CI never calls check or release; recording is additive there and the fail-fast paths cannot trigger on a fresh runner.One-time upgrade note
Processes started before this change are unrecorded. The first
yarn tests:e2e:checkafter upgrading may report themowner=this-checkout-unrecorded. Stop them, or run the printedyarn tests:e2e:release --include-unrecorded --only <category>(add--devicesfor an emulator) once in that identity's env. An unrecorded simulator cannot be traced to a checkout and showsowner=unrecorded; if it is yours, runxcrun simctl shutdown <udid>.Docs:
okf-bundle/testing/running-e2e.md(run record, kill policy, owner labels, manual machine wipe, upgrade note),okf-bundle/testing/e2e-parallel-design.md,okf-bundle/testing/agent-command-policy.md.Related issues
None. Related PRs in this stack:
Release Summary
None (e2e scripts, tests and contributor docs only).
Checklist
AndroidiOSOther(macOS, web)e2etests added or updated inpackages/\*\*/e2ejesttests added or updated inpackages/\*\*/__tests__Test Plan
Jest
New
e2eRunRecord.test.jscovers pid ownership and reuse, device instance tokens (exact AVD match, qemu-portserial), recorded-only release,--include-unrecordedowner and AVD filtering, check owner labels and hints, harness and.jetrc.jsfail-fast, owned-only listener recording, failed-start cleanup, the Detox "booted during this run" hook, every harness reboot path re-recording, the cold-boot stop leaving another owner's emulator running, macOS Jet recording itself,simulator-logginggating, the launch-smoke sweep, the CI-only guard and the removedtest:fullflags exiting 2.startPackager.test.js,createE2eDevices.test.jsande2eResourceEnv.test.jscover Metro recorded at spawn, simulator release by owner, and stray-emulator BUSY/INFO.Each guard was checked by reverting it and confirming a test fails.
The same-name macOS app fail-fast test now sets
process.titleon Linux. Node renames its main threadMainThreadthere, sopgrep -xnever matched the symlinkednode. A symlinkedsleepis not an option because the runner's uutils coreutils refuses to run under another name.e2eRunRecord.test.jspasses 45/45 on macOS and in anubuntu:26.04container with Node 24 (the runner image). The CIJestjob is green (119 suites; 1927 passed, 7 skipped).New
jetAttemptDrain.test.jsuses real processes on a real port. It covers the drain stopping the Jet a launch retry respawned (not the stale first one), killing a listener whose parent would exit on SIGTERM and orphan it, treating a signalled but still-running Jet as alive, and abandoning the session. It also checks the harness wiring: the session is kept as the attempt context, the drain abandons before the orchestrate wait, an abandoned session never respawns Jet, and the close-wait timeout names the port's holders. Reverting the harness wiring fails 4 tests; restoring the old!killedliveness check fails 2; putting SIGTERM back before the tree kill fails the orphan test. Passes on macOS and inubuntu:26.04with Node 24.The same file covers a late attempt-1 orchestrate: a real session is drained, then attempt 2's stand-in Jet holds the same port. Each check is revert-proven: removing it fails one test.
shroot can orphan the listener.terminateAppwait ends after its bound, and a later rejection is handled.The orphan-listener tests record the listener's pid and kill it after each test, so a failing run leaves no listener behind (checked by failing them on purpose).
e2eRunRecord.test.jsno longer reads the host's devices. Its fake devices carry a per-run marker.ps(every listing form and per-pid queries),pgrepandpkillhide qemu andlaunchd_simprocesses without it, andadbandsimctllist no devices. Thepkillstub accepts only-f, a signal and one pattern, and exits 2 on any other flag.--all-slotswipe test ran the realpkill, which stops every same-AVD emulator on the host, a real one included. Scoped release neverpkills by AVD; only the manual--all-slotswipe does.@TestingAVD-1 -port 5558running, the suite passes and the process survives; without the stubs, the 2 tests fail. New tests check each discovery path, everypsform and thepkillflag handling.A stalled Jet whose exit is not yet observed is killed once, not again by the orchestrate failure cleanup; a respawned Jet is still killed. Removing the session's check, or not recording the stall kill, fails the new test.
The seven affected suites pass on macOS (154 tests) and in
ubuntu:26.04with Node 24.Restacked onto build: size lerna prepare and gradle workers to the CPUs and memory the build can use #9446's current tip (fix: harden ruby:install, lint:android and host-dependent app tests #9447 restacked first). fix: harden ruby:install, lint:android and host-dependent app tests #9447 had kept build: size lerna prepare and gradle workers to the CPUs and memory the build can use #9446's first commit as its base, so no commit was dropped. All nine commits of this PR rebased with no conflicts, and
git range-diffshows each one unchanged. On the new tip:yarn lint0, the seven affected suites 0 (154 tests,--detectOpenHandles), and the stack's top (fix(e2e): harden shared-host e2e scripts for slots, foreign Jet ports and TMPDIR #9448) runs fullyarn tests:jest0 and the affected suites 0 inubuntu:26.04with Node 24.Validation after rebasing onto fix: harden ruby:install, lint:android and host-dependent app tests #9447:
yarn lerna:prepare,yarn tsc:compile,yarn tsc:compile:consumer,yarn reference:api,yarn lint,yarn tests:jest(119 suites, 1934 tests),yarn format:jsandyarn compare:typesall exit 0. The rebase changed no e2e harness or runtime file.Local serial e2e
Run on one shared host, with no slot environment and the full harness, Android then iOS then macOS. All three passed with 0 failing and coverage merged: Android 1557 passing, iOS 1546, macOS 1340.
yarn tests:e2e:check --services --platform=<p>from a second shell showed Metro, the emulator suite, Jet, the test app and the device asowner=this-checkout.yarn tests:e2e:release --devices --platform=<p>freed exactly the recorded resources and emptied the record. The final release and check were clean.yarn tests:e2e:releasefreed:8090/:8091and the app with no manual flag.stopped the attempt's current Jet, and the port check found:8090already free.:8090had no listener and no Jet process right after the drain and 15 s later.yarn tests:e2e:release --devices --platform=iosandyarn tests:e2e:check --services --platform=ioswere clean. Another owner's booted simulator was reported INFO and left running.CI e2e workflows
Timed out waiting for 127.0.0.1:8090 to closein the outer-retry drain, with a Jet respawned during the drain still holding the port. The other runs were cancelled by newer pushes, or were still queued when a newer push superseded them.:8090already free, and attempt 2 passed.Timed out waiting for 127.0.0.1:8090 to close.The first iOS release attempt failed in "Detox Test Release": in both outer attempts
launchApp timed out after 120000msalthough the app's JS had connected to Jet. The launch retry respawned Jet, the still-running first instance connected to it and was dropped (1006) by the retry's terminate, and the 30 s reconnect grace expired (Jet attempt 2 failed (retryable=false, exit=1)). This launch-retry sequence (respawn before terminate, 30 s grace) is the same on main and predates this PR. It involves no stall, so the stall-kill change does not reach it. The rerun passed.Think
react-native-firebaseis great? Please consider supporting the project with any of the below:React Native FirebaseandInvertaseon Twitter