Skip to content

Commit 8e498e5

Browse files
committed
test: mitigate e2e simulator hang / retry flakes
1 parent 7cf7c1a commit 8e498e5

4 files changed

Lines changed: 186 additions & 14 deletions

File tree

.github/workflows/scripts/boot-simulator.sh

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,51 @@ describe_booted_device() {
4242
|| true
4343
}
4444

45+
resolve_device_udid() {
46+
local device="$1"
47+
local udid
48+
49+
udid="$(
50+
xcrun simctl list devices booted 2>/dev/null \
51+
| grep -F "${device} (" \
52+
| grep -v 'unavailable' \
53+
| head -1 \
54+
| sed -E 's/.*\(([A-F0-9-]+)\).*/\1/' \
55+
|| true
56+
)"
57+
if [[ -n "$udid" ]]; then
58+
echo "$udid"
59+
return 0
60+
fi
61+
62+
udid="$(
63+
xcrun simctl list devices available 2>/dev/null \
64+
| grep -F "${device} (" \
65+
| grep -v 'unavailable' \
66+
| head -1 \
67+
| sed -E 's/.*\(([A-F0-9-]+)\).*/\1/' \
68+
|| true
69+
)"
70+
echo "$udid"
71+
}
72+
73+
kill_resolved_simulator() {
74+
local device="$1"
75+
local udid
76+
77+
udid="$(resolve_device_udid "$device")"
78+
if [[ -z "$udid" ]]; then
79+
log_boot_status "phase=kill_resolved device=\"${device}\" not found, skipping"
80+
return 0
81+
fi
82+
83+
log_boot_status "phase=kill_resolved udid=${udid} device=\"${device}\""
84+
killall Simulator 2>/dev/null || true
85+
xcrun simctl terminate "$udid" com.invertase.testing 2>/dev/null || true
86+
xcrun simctl shutdown "$udid" 2>/dev/null || true
87+
xcrun simctl shutdown "$device" 2>/dev/null || true
88+
}
89+
4590
log_migration_status() {
4691
local device="$1"
4792
local migration_output probe_rc
@@ -117,10 +162,8 @@ popd >/dev/null || exit 1
117162

118163
log_boot_status "phase=resolve_device name=\"${SIM}\" (from tests/.detoxrc.js)"
119164

120-
# Clear up any existing attempts in case we are re-trying
121-
log_boot_status "phase=shutdown_existing killing Simulator.app if running..."
122-
killall Simulator 2>/dev/null || true
123-
xcrun simctl shutdown "$SIM" 2>/dev/null || true
165+
# Kill the resolved simulator first when present (CI pre-boot and e2e Jet retries).
166+
kill_resolved_simulator "$SIM"
124167

125168
log_boot_status "phase=boot_command starting simctl boot..."
126169
set +e

.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,54 @@
11
diff --git a/dist/Server.js b/dist/Server.js
2-
index ad9debe2086ab9b96e97a69aec966da8114ad102..e3c1c964956023df876dfcac3f73000b4eaf1bba 100644
2+
index ad9debe2086ab9b96e97a69aec966da8114ad102..c8f4e2a1b9d3f6e8a7c5d4b2e1f0a9c8b7d6e5f4 100644
33
--- a/dist/Server.js
44
+++ b/dist/Server.js
5-
@@ -130,6 +130,17 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
5+
@@ -73,6 +73,8 @@
6+
/** The options to send to the next connecting running client */
7+
this.clientOptions = {};
8+
this._listening = false;
9+
+ this._clientResetTimer = null;
10+
+ this._awaitingInitialClientRun = false;
11+
this.handleConnection = (ws, req) => {
12+
this.debug("Client connected");
13+
// Check that the protocol matches
14+
@@ -88,6 +90,11 @@
15+
ws.close(1002, `Expected "${expectedProtocol}" protocol got "${ws.protocol}"`);
16+
return;
17+
}
18+
+ if (this._clientResetTimer) {
19+
+ clearTimeout(this._clientResetTimer);
20+
+ this._clientResetTimer = null;
21+
+ console.warn(`[mocha-remote-ws] reconnect_recovered preserving_runner`);
22+
+ }
23+
if (this.client) {
24+
this.debug("A client was already connected");
25+
this.client.close(1013 /* try again later */, "Got a connection from another client");
26+
@@ -97,14 +104,20 @@
27+
// Hang onto the client
28+
this.client = ws;
29+
this.client.on("message", this.handleMessage.bind(this, this.client));
30+
- this.client.once("close", this.handleReset);
31+
+ this.client.once("close", (code) => this.handleClientDisconnect(code));
32+
// If we already have a runner, it can run now that we have a client
33+
if (this.runner) {
34+
- if (this.clientOptions) {
35+
- this.send({ action: "run", options: this.clientOptions });
36+
+ if (this._awaitingInitialClientRun) {
37+
+ if (this.clientOptions) {
38+
+ this.send({ action: "run", options: this.clientOptions });
39+
+ this._awaitingInitialClientRun = false;
40+
+ }
41+
+ else {
42+
+ throw new Error("Internal error: Expected a clientOptions");
43+
+ }
44+
}
45+
else {
46+
- throw new Error("Internal error: Expected a clientOptions");
47+
+ this.debug("Client reconnected while runner active; resuming without re-run");
48+
}
49+
}
50+
else if (this.config.autoRun) {
51+
@@ -130,6 +143,17 @@
652
throw new Error("Received a message from the client, but server wasn't running");
753
}
854
}
@@ -20,3 +66,58 @@ index ad9debe2086ab9b96e97a69aec966da8114ad102..e3c1c964956023df876dfcac3f73000b
2066
else if (msg.action === "error") {
2167
if (typeof msg.message !== "string") {
2268
throw new Error("Expected 'error' action to have an error argument with a message");
69+
@@ -149,13 +173,46 @@
70+
}
71+
else {
72+
throw err;
73+
+ }
74+
+ }
75+
+ };
76+
+ this.handleClientDisconnect = (code) => {
77+
+ const transientCodes = [1006, 1001];
78+
+ const graceMs = this.config.reconnectGraceMs ?? 15000;
79+
+ if (this.runner && transientCodes.includes(code)) {
80+
+ if (this._clientResetTimer) {
81+
+ clearTimeout(this._clientResetTimer);
82+
}
83+
+ console.warn(`[mocha-remote-ws] transient_disconnect code=${code} grace_ms=${graceMs} preserving_runner`);
84+
+ const client = this.client;
85+
+ delete this.client;
86+
+ if (client) {
87+
+ client.removeAllListeners();
88+
+ }
89+
+ this._clientResetTimer = setTimeout(() => {
90+
+ this._clientResetTimer = null;
91+
+ if (!this.client) {
92+
+ console.error(`[mocha-remote-ws] fatal_disconnect code=${code} grace_expired_ms=${graceMs}`);
93+
+ this.handleReset();
94+
+ }
95+
+ }, graceMs);
96+
+ return;
97+
}
98+
+ if (this._clientResetTimer) {
99+
+ clearTimeout(this._clientResetTimer);
100+
+ this._clientResetTimer = null;
101+
+ }
102+
+ this.handleReset();
103+
};
104+
/**
105+
* Resets the server for another test run.
106+
*/
107+
this.handleReset = () => {
108+
+ if (this._clientResetTimer) {
109+
+ clearTimeout(this._clientResetTimer);
110+
+ this._clientResetTimer = null;
111+
+ }
112+
+ this._awaitingInitialClientRun = false;
113+
// Forget everything about the runner and the client
114+
const { runner, client } = this;
115+
delete this.runner;
116+
@@ -263,6 +320,7 @@
117+
// this.runner = new Mocha.Runner(this.suite, this.options.delay || false);
118+
// TODO: Stub this to match the Runner's interface even better
119+
this.runner = new FakeRunner_1.FakeRunner();
120+
+ this._awaitingInitialClientRun = true;
121+
// Attach event listeners to update stats
122+
(0, stats_collector_1.createStatsCollector)(this.runner);
123+
// Set the client options, to be passed to the next running client

tests/e2e/firebase.test.js

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*
1717
*/
18-
const { spawn } = require('child_process');
18+
const { execFileSync, spawn } = require('child_process');
1919
const net = require('net');
2020
const path = require('path');
2121

@@ -30,7 +30,9 @@ const JET_RECONNECT_RECOVERED_RE = /\[jet-ws\] reconnect_recovered code=(1006|10
3030
const JET_SERVER_NOT_RUNNING_RE = /server wasn't running/i;
3131
const JET_COVERAGE_LOST_RE = /Coverage summary:[\s\S]*?Unknown% \( 0\/0 \)/;
3232
const RETRYABLE_LAUNCH_RE =
33-
/launchApp timed out|RCTJavaScriptDidFailToLoad|packager-probe|Metro not responding/i;
33+
/launchApp timed out|RCTJavaScriptDidFailToLoad|packager-probe|Metro not responding|Unknown application display identifier|Simulator device failed to launch/i;
34+
35+
let cachedUsesLiveMetro;
3436

3537
function resolveDetoxConfigurationName() {
3638
if (process.env.DETOX_CONFIGURATION) {
@@ -55,6 +57,10 @@ function resolveAppBinaryPath() {
5557
}
5658

5759
function usesLiveMetro() {
60+
if (cachedUsesLiveMetro !== undefined) {
61+
return cachedUsesLiveMetro;
62+
}
63+
5864
const configName = resolveDetoxConfigurationName();
5965
if (/debug/i.test(configName)) {
6066
return true;
@@ -74,6 +80,22 @@ function usesLiveMetro() {
7480
return false;
7581
}
7682

83+
function cacheUsesLiveMetro() {
84+
cachedUsesLiveMetro = usesLiveMetro();
85+
console.log(`[rnfb-e2e] cached usesLiveMetro=${cachedUsesLiveMetro}`);
86+
}
87+
88+
function rebootIosSimulator(testsDir) {
89+
const repoRoot = path.resolve(testsDir, '..');
90+
const bootScript = path.join(repoRoot, '.github/workflows/scripts/boot-simulator.sh');
91+
console.warn(`[rnfb-e2e] Rebooting iOS simulator via ${bootScript}`);
92+
execFileSync('bash', [bootScript], {
93+
cwd: repoRoot,
94+
stdio: 'inherit',
95+
timeout: 12 * 60 * 1000,
96+
});
97+
}
98+
7799
function waitForTcpPort(port, host = '127.0.0.1', timeoutMs = 120000) {
78100
const start = Date.now();
79101

@@ -289,6 +311,8 @@ describe('Jet Tests', function () {
289311
const deviceId = detox.device.id;
290312
const testsDir = path.resolve(__dirname, '..');
291313

314+
cacheUsesLiveMetro();
315+
292316
let lastFailure;
293317

294318
for (let attempt = 1; attempt <= 2; attempt++) {
@@ -304,10 +328,14 @@ describe('Jet Tests', function () {
304328
} else if (isRetryableLaunchFailure(lastFailure)) {
305329
console.warn('[rnfb-e2e] Retrying after Metro/bundle load launch failure');
306330
}
307-
try {
308-
await device.terminateApp();
309-
} catch (_) {
310-
// No-op
331+
if (platform === 'ios' && process.platform === 'darwin') {
332+
rebootIosSimulator(testsDir);
333+
} else {
334+
try {
335+
await device.terminateApp();
336+
} catch (_) {
337+
// No-op
338+
}
311339
}
312340
}
313341

yarn.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19554,13 +19554,13 @@ __metadata:
1955419554

1955519555
"mocha-remote-server@patch:mocha-remote-server@npm%3A1.13.2#~/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch":
1955619556
version: 1.13.2
19557-
resolution: "mocha-remote-server@patch:mocha-remote-server@npm%3A1.13.2#~/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch::version=1.13.2&hash=5b470d"
19557+
resolution: "mocha-remote-server@patch:mocha-remote-server@npm%3A1.13.2#~/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch::version=1.13.2&hash=9f1e51"
1955819558
dependencies:
1955919559
debug: "npm:^4.3.4"
1956019560
flatted: "npm:^3.3.1"
1956119561
mocha-remote-common: "npm:1.13.2"
1956219562
ws: "npm:^8.17.1"
19563-
checksum: 10/c5861226362636fac484237e3653e6442c88a7b0b12b240ab33482961b09b5c2a1a54190c03237b2c2274616be6fe7e5c8484b355a00bb28ee401cb7b81351b6
19563+
checksum: 10/026256e831efc672c04312c7083927de218b2cf911eb7710abbc3c9ccb76730241eefc5bfb1f93873120244c80cb29cbd447a7edfa99bc2ce8fdc937ddc15c8c
1956419564
languageName: node
1956519565
linkType: hard
1956619566

0 commit comments

Comments
 (0)