@react-native-harness/jest 1.4.0-rc.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/bridge.test.js +6 -2
- package/dist/__tests__/crash-monitor.test.js +64 -0
- package/dist/__tests__/environment-reset.test.d.ts +2 -0
- package/dist/__tests__/environment-reset.test.d.ts.map +1 -0
- package/dist/__tests__/environment-reset.test.js +238 -0
- package/dist/__tests__/execute-run.test.js +250 -4
- package/dist/__tests__/format-harness-error.test.d.ts +2 -0
- package/dist/__tests__/format-harness-error.test.d.ts.map +1 -0
- package/dist/__tests__/format-harness-error.test.js +27 -0
- package/dist/__tests__/harness-cache.test.js +62 -41
- package/dist/__tests__/harness-session-signal.test.d.ts +2 -0
- package/dist/__tests__/harness-session-signal.test.d.ts.map +1 -0
- package/dist/__tests__/harness-session-signal.test.js +298 -0
- package/dist/__tests__/harness-session.test.js +161 -2
- package/dist/__tests__/index.test.js +3 -1
- package/dist/__tests__/metro-port.test.js +2 -0
- package/dist/__tests__/run.test.d.ts +2 -0
- package/dist/__tests__/run.test.d.ts.map +1 -0
- package/dist/__tests__/run.test.js +48 -0
- package/dist/crash-monitor.d.ts +7 -0
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +19 -0
- package/dist/diagnostics/__tests__/derivers.test.d.ts +2 -0
- package/dist/diagnostics/__tests__/derivers.test.d.ts.map +1 -0
- package/dist/diagnostics/__tests__/derivers.test.js +248 -0
- package/dist/diagnostics/__tests__/report.test.d.ts +2 -0
- package/dist/diagnostics/__tests__/report.test.d.ts.map +1 -0
- package/dist/diagnostics/__tests__/report.test.js +97 -0
- package/dist/diagnostics/derivers.d.ts +24 -0
- package/dist/diagnostics/derivers.d.ts.map +1 -0
- package/dist/diagnostics/derivers.js +146 -0
- package/dist/diagnostics/index.d.ts +3 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +2 -0
- package/dist/diagnostics/report.d.ts +7 -0
- package/dist/diagnostics/report.d.ts.map +1 -0
- package/dist/diagnostics/report.js +84 -0
- package/dist/environment-reset.d.ts +51 -0
- package/dist/environment-reset.d.ts.map +1 -0
- package/dist/environment-reset.js +83 -0
- package/dist/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +121 -69
- package/dist/format-harness-error.d.ts +5 -0
- package/dist/format-harness-error.d.ts.map +1 -0
- package/dist/format-harness-error.js +43 -0
- package/dist/harness-session.d.ts +26 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +260 -87
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +21 -4
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/__tests__/bridge.test.ts +8 -3
- package/src/__tests__/crash-monitor.test.ts +84 -0
- package/src/__tests__/environment-reset.test.ts +308 -0
- package/src/__tests__/execute-run.test.ts +327 -4
- package/src/__tests__/format-harness-error.test.ts +32 -0
- package/src/__tests__/harness-cache.test.ts +91 -0
- package/src/__tests__/harness-session.test.ts +211 -1
- package/src/__tests__/index.test.ts +3 -1
- package/src/__tests__/metro-port.test.ts +2 -0
- package/src/__tests__/run.test.ts +74 -0
- package/src/crash-monitor.ts +28 -0
- package/src/diagnostics/__tests__/derivers.test.ts +300 -0
- package/src/diagnostics/__tests__/report.test.ts +128 -0
- package/src/diagnostics/derivers.ts +170 -0
- package/src/diagnostics/index.ts +2 -0
- package/src/diagnostics/report.ts +124 -0
- package/src/environment-reset.ts +138 -0
- package/src/execute-run.ts +136 -77
- package/src/format-harness-error.ts +73 -0
- package/src/harness-session.ts +369 -131
- package/src/run.ts +23 -4
- package/tsconfig.json +3 -0
- package/tsconfig.lib.json +3 -0
package/dist/harness-session.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import { createHarnessBridge, } from '@react-native-harness/bridge/server';
|
|
1
|
+
import { createHarnessBridge, AppBridgeDisconnectedError, } from '@react-native-harness/bridge/server';
|
|
2
2
|
import { HARNESS_BRIDGE_PATH, } from '@react-native-harness/bridge';
|
|
3
|
-
import {
|
|
3
|
+
import { createHarnessCache } from '@react-native-harness/cache';
|
|
4
|
+
import { getMetroInstance, waitForMetroBackedAppReady, } from '@react-native-harness/bundler-metro';
|
|
4
5
|
import { createHarnessPluginManager, } from '@react-native-harness/plugins';
|
|
5
|
-
import { createCrashArtifactWriter, logger, getTimeoutSignal,
|
|
6
|
-
import { getConfig, ConfigSchema, } from '@react-native-harness/config';
|
|
6
|
+
import { createCrashArtifactWriter, createDiagnostics, logger, getTimeoutSignal, ensureSignalsDeliverable, waitForAbort, } from '@react-native-harness/tools';
|
|
7
|
+
import { getConfig, isDiagnosticsEnabled, resolveMetroCacheEnabled, ConfigSchema, } from '@react-native-harness/config';
|
|
7
8
|
import { preRunMessage } from 'jest-util';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import { PlatformReadyTimeoutError } from './errors.js';
|
|
10
11
|
import { NoRunnerSpecifiedError, RunnerNotFoundError } from './errors.js';
|
|
11
12
|
import { createCrashMonitor } from './crash-monitor.js';
|
|
13
|
+
import { createProcessResetStrategy, createRuntimeResetStrategy, resolveResetStrategyKind, withEscalation, } from './environment-reset.js';
|
|
12
14
|
import { createHookQueue } from './hook-queue.js';
|
|
13
15
|
import { createClientLogCollector, } from './client-log-handler.js';
|
|
14
16
|
import { createActionHooksPlugin } from './action-hooks.js';
|
|
15
17
|
import { createResourceLockManager, } from './resource-lock.js';
|
|
18
|
+
import { observeBridgeEvents, observeMetroEvents } from './diagnostics/index.js';
|
|
16
19
|
import { resolveHarnessMetroPort } from './metro-port.js';
|
|
17
20
|
import { getAdditionalCliArgs } from './cli-args.js';
|
|
18
21
|
import { logMetroCacheReused, logMetroPortFallback, logNativeCoverageCollected, logRunnerStarting, logRunnerStillWaitingInQueue, logRunnerWaitingInQueue, logTestEnvironmentReady, logTestRunHeader, } from './logs.js';
|
|
@@ -41,45 +44,69 @@ export const waitForBridgeDisconnectOrTimeout = async ({ bridge, connection, tim
|
|
|
41
44
|
bridge.on('disconnected', onDisconnected);
|
|
42
45
|
});
|
|
43
46
|
};
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
const
|
|
47
|
+
// Resolves the NEXT time the bridge reports a 'connected' event, rejecting
|
|
48
|
+
// if `signal` aborts first. Deliberately does not use bridge.nextConnection(),
|
|
49
|
+
// which would resolve immediately for a stale, already-set connection.
|
|
50
|
+
export const waitForNextConnected = ({ bridge, signal, }) => new Promise((resolve, reject) => {
|
|
51
|
+
const onConnected = () => { cleanup(); resolve(); };
|
|
52
|
+
const onAbort = () => { cleanup(); reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); };
|
|
53
|
+
const cleanup = () => {
|
|
54
|
+
bridge.off('connected', onConnected);
|
|
55
|
+
signal.removeEventListener('abort', onAbort);
|
|
56
|
+
};
|
|
49
57
|
if (signal.aborted) {
|
|
50
|
-
|
|
58
|
+
onAbort();
|
|
59
|
+
return;
|
|
51
60
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
bridge.on('connected', onConnected);
|
|
62
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
63
|
+
});
|
|
64
|
+
export const getSignalExitCodeForRunState = (state) => {
|
|
65
|
+
if (state?.completed &&
|
|
66
|
+
state.status === 'passed' &&
|
|
67
|
+
!state.error &&
|
|
68
|
+
(state.summary?.failed ?? 0) === 0) {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
return 1;
|
|
55
72
|
};
|
|
56
|
-
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Internal helpers
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Bounds only the init *call* with a readiness timeout; `options.signal` (the
|
|
77
|
+
// session-lifetime signal) is passed to `work` unmodified, so a slow init
|
|
78
|
+
// call keeps observing real session teardown, not a synthetic timeout abort.
|
|
79
|
+
// On timeout this throws PlatformReadyTimeoutError without cancelling `work`
|
|
80
|
+
// itself — the caller is expected to abort the session signal as part of
|
|
81
|
+
// tearing down, which then cancels the still-inflight call cooperatively.
|
|
82
|
+
export const withPlatformReadyTimeout = async (options) => {
|
|
57
83
|
const timeoutSignal = getTimeoutSignal(options.timeout);
|
|
58
|
-
const
|
|
84
|
+
const abortWait = waitForAbort(timeoutSignal);
|
|
59
85
|
try {
|
|
60
|
-
return await
|
|
86
|
+
return await Promise.race([
|
|
87
|
+
options.work(options.signal),
|
|
88
|
+
abortWait.promise.catch(() => {
|
|
89
|
+
throw new PlatformReadyTimeoutError(options.timeout);
|
|
90
|
+
}),
|
|
91
|
+
]);
|
|
61
92
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
error.name === 'AbortError' &&
|
|
65
|
-
timeoutSignal.aborted &&
|
|
66
|
-
!options.signal.aborted) {
|
|
67
|
-
throw new PlatformReadyTimeoutError(options.timeout);
|
|
68
|
-
}
|
|
69
|
-
throw error;
|
|
93
|
+
finally {
|
|
94
|
+
abortWait.cancel();
|
|
70
95
|
}
|
|
71
96
|
};
|
|
72
97
|
export const waitForStartupCrash = async ({ crashMonitor, detectNativeCrashes, testFilePath, signal, }) => {
|
|
73
98
|
if (!detectNativeCrashes) {
|
|
74
|
-
return await waitForAbort(signal);
|
|
99
|
+
return await waitForAbort(signal).promise;
|
|
75
100
|
}
|
|
76
101
|
const watch = crashMonitor.watch(testFilePath, 'startup');
|
|
77
102
|
watch.promise.catch(ignorePromiseRejection); // suppress unhandled-rejection when abort wins race
|
|
103
|
+
const abortWait = waitForAbort(signal);
|
|
78
104
|
try {
|
|
79
|
-
return await Promise.race([watch.promise,
|
|
105
|
+
return await Promise.race([watch.promise, abortWait.promise]);
|
|
80
106
|
}
|
|
81
107
|
finally {
|
|
82
108
|
watch.cancel();
|
|
109
|
+
abortWait.cancel();
|
|
83
110
|
}
|
|
84
111
|
};
|
|
85
112
|
const waitForAppReady = async (base, testFilePath) => {
|
|
@@ -104,20 +131,7 @@ const waitForAppReady = async (base, testFilePath) => {
|
|
|
104
131
|
// is already set. waitForReady is called before startAttempt, so a stale
|
|
105
132
|
// connection from a previous run would resolve the promise before startAttempt
|
|
106
133
|
// even restarts the app — leaving bridge.connection null after the restart.
|
|
107
|
-
await
|
|
108
|
-
const onConnected = () => { cleanup(); resolve(); };
|
|
109
|
-
const onAbort = () => { cleanup(); reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); };
|
|
110
|
-
const cleanup = () => {
|
|
111
|
-
bridge.off('connected', onConnected);
|
|
112
|
-
signal.removeEventListener('abort', onAbort);
|
|
113
|
-
};
|
|
114
|
-
if (signal.aborted) {
|
|
115
|
-
onAbort();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
bridge.on('connected', onConnected);
|
|
119
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
120
|
-
});
|
|
134
|
+
await waitForNextConnected({ bridge, signal });
|
|
121
135
|
logWait('runtime ready received');
|
|
122
136
|
},
|
|
123
137
|
waitForCrash: async (signal) => {
|
|
@@ -184,9 +198,23 @@ const applyEnvVars = (harnessConfig, globalConfig) => {
|
|
|
184
198
|
process.env.RN_HARNESS_VIEW_FLATTENING = 'false';
|
|
185
199
|
}
|
|
186
200
|
};
|
|
201
|
+
/**
|
|
202
|
+
* Logs the "Reusing Metro cache" message when persistent Metro caching is
|
|
203
|
+
* enabled and the cache is warm. `projectRoot` must be the config-resolved
|
|
204
|
+
* project root so the warmth check looks at the same directory Metro caches
|
|
205
|
+
* into.
|
|
206
|
+
*/
|
|
207
|
+
export const maybeLogMetroCacheReused = (options) => {
|
|
208
|
+
if (!resolveMetroCacheEnabled(options.config)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (createHarnessCache({ projectRoot: options.projectRoot }).isWarm('metro')) {
|
|
212
|
+
logMetroCacheReused(options.platform);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
187
215
|
const loadConfig = async (globalConfig) => {
|
|
188
216
|
const projectRoot = globalConfig.rootDir;
|
|
189
|
-
const { config: rawConfig } = await getConfig(projectRoot);
|
|
217
|
+
const { config: rawConfig, projectRoot: configProjectRoot } = await getConfig(projectRoot);
|
|
190
218
|
const cliArgs = getAdditionalCliArgs();
|
|
191
219
|
let harnessConfig = cliArgs.metroPort != null
|
|
192
220
|
? ConfigSchema.parse({ ...rawConfig, metroPort: cliArgs.metroPort })
|
|
@@ -207,29 +235,40 @@ const loadConfig = async (globalConfig) => {
|
|
|
207
235
|
const platform = harnessConfig.runners.find((r) => r.name === selectedRunnerName);
|
|
208
236
|
if (!platform)
|
|
209
237
|
throw new RunnerNotFoundError(selectedRunnerName);
|
|
210
|
-
return { harnessConfig, platform, projectRoot };
|
|
238
|
+
return { harnessConfig, platform, projectRoot, configProjectRoot };
|
|
211
239
|
};
|
|
212
240
|
// ---------------------------------------------------------------------------
|
|
213
241
|
// Session creation
|
|
214
242
|
// ---------------------------------------------------------------------------
|
|
215
243
|
export const createHarnessSession = async (globalConfig, { lockManager = defaultResourceLockManager } = {}) => {
|
|
216
244
|
preRunMessage.remove(process.stderr);
|
|
217
|
-
const
|
|
245
|
+
const setupStartedAt = Date.now();
|
|
246
|
+
const { harnessConfig, platform, projectRoot, configProjectRoot } = await loadConfig(globalConfig);
|
|
218
247
|
applyEnvVars(harnessConfig, globalConfig);
|
|
248
|
+
const diagnostics = createDiagnostics({
|
|
249
|
+
enabled: isDiagnosticsEnabled(harnessConfig),
|
|
250
|
+
});
|
|
251
|
+
diagnostics.record({
|
|
252
|
+
label: 'session.config.load',
|
|
253
|
+
duration: Date.now() - setupStartedAt,
|
|
254
|
+
});
|
|
219
255
|
sessionLogger.debug('creating session for runner=%s platform=%s', platform.name, platform.platformId);
|
|
220
|
-
// Single AbortController
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
256
|
+
// Single, session-lifetime AbortController: it aborts on early SIGTERM/SIGINT
|
|
257
|
+
// during setup (below) and, once the session is up, in dispose()'s finally
|
|
258
|
+
// (unchanged lifetime). It is the one signal the plugin manager and platform
|
|
259
|
+
// runner see for the entire session, so its abort reaches every long-lived
|
|
260
|
+
// child the runner threads it into.
|
|
261
|
+
ensureSignalsDeliverable();
|
|
262
|
+
const sessionController = new AbortController();
|
|
263
|
+
const onEarlySignal = () => sessionController.abort();
|
|
225
264
|
process.once('SIGTERM', onEarlySignal);
|
|
226
265
|
process.once('SIGINT', onEarlySignal);
|
|
227
266
|
const resourceLockKey = await (platform.getResourceLockKey?.() ?? getDefaultResourceLockKey(platform));
|
|
228
267
|
let didWaitForResourceLock = false;
|
|
229
268
|
let lastStillWaitingLogAt = 0;
|
|
230
269
|
logTestRunHeader(platform);
|
|
231
|
-
const resourceLease = await lockManager.acquire(resourceLockKey, {
|
|
232
|
-
signal:
|
|
270
|
+
const resourceLease = await diagnostics.measure('session.lock.wait', () => lockManager.acquire(resourceLockKey, {
|
|
271
|
+
signal: sessionController.signal,
|
|
233
272
|
onWait: () => {
|
|
234
273
|
didWaitForResourceLock = true;
|
|
235
274
|
logRunnerWaitingInQueue(platform);
|
|
@@ -242,7 +281,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
242
281
|
logRunnerStillWaitingInQueue(platform);
|
|
243
282
|
sessionLogger.debug('still waiting in queue for runner=%s key=%s elapsedMs=%d', platform.name, resourceLockKey, elapsedMs);
|
|
244
283
|
},
|
|
245
|
-
});
|
|
284
|
+
}), { lockKey: resourceLockKey });
|
|
246
285
|
if (didWaitForResourceLock)
|
|
247
286
|
logRunnerStarting(platform);
|
|
248
287
|
sessionLogger.debug('resource lock acquired for runner=%s key=%s', platform.name, resourceLockKey);
|
|
@@ -250,26 +289,39 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
250
289
|
// port resolution but before the inner try/catch (e.g. bridge creation failure).
|
|
251
290
|
let metroPortLease = null;
|
|
252
291
|
try {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
292
|
+
const portResolveSpan = diagnostics.start('session.port.resolve');
|
|
293
|
+
let resolution;
|
|
294
|
+
try {
|
|
295
|
+
resolution = await resolveHarnessMetroPort({
|
|
296
|
+
config: harnessConfig,
|
|
297
|
+
platform,
|
|
298
|
+
resourceLockManager: lockManager,
|
|
299
|
+
signal: sessionController.signal,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
portResolveSpan.fail(error);
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
portResolveSpan.end({
|
|
307
|
+
port: resolution.config.metroPort,
|
|
308
|
+
didFallback: resolution.didFallback,
|
|
258
309
|
});
|
|
259
310
|
metroPortLease = resolution.metroPortLease;
|
|
260
311
|
const { config: runtimeConfig, initialMetroPort, didFallback } = resolution;
|
|
261
312
|
if (didFallback)
|
|
262
313
|
logMetroPortFallback(initialMetroPort, runtimeConfig.metroPort);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
314
|
+
maybeLogMetroCacheReused({
|
|
315
|
+
config: runtimeConfig,
|
|
316
|
+
projectRoot: configProjectRoot,
|
|
317
|
+
platform,
|
|
318
|
+
});
|
|
267
319
|
const pluginManager = createHarnessPluginManager({
|
|
268
320
|
plugins: (runtimeConfig.plugins ?? []),
|
|
269
321
|
projectRoot,
|
|
270
322
|
config: runtimeConfig,
|
|
271
323
|
runner: platform,
|
|
272
|
-
abortSignal:
|
|
324
|
+
abortSignal: sessionController.signal,
|
|
273
325
|
});
|
|
274
326
|
const hooks = createHookQueue();
|
|
275
327
|
let currentRun = null;
|
|
@@ -281,43 +333,68 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
281
333
|
platformId: platform.platformId,
|
|
282
334
|
rootDir: path.join(projectRoot, '.harness', 'crash-reports'),
|
|
283
335
|
});
|
|
284
|
-
const bridge = await createHarnessBridge({
|
|
336
|
+
const bridge = await diagnostics.measure('session.bridge.init', () => createHarnessBridge({
|
|
285
337
|
noServer: true,
|
|
286
338
|
timeout: runtimeConfig.bridgeTimeout,
|
|
287
339
|
context,
|
|
288
|
-
});
|
|
340
|
+
}));
|
|
289
341
|
sessionLogger.debug('bridge initialized on Metro websocket path %s', HARNESS_BRIDGE_PATH);
|
|
290
342
|
let metroInstance;
|
|
291
343
|
let platformInstance;
|
|
344
|
+
// Noop until the Metro instance resolves and the deriver is attached.
|
|
345
|
+
let disposeMetroDiagnostics = () => undefined;
|
|
292
346
|
try {
|
|
293
347
|
[metroInstance, platformInstance] = await Promise.all([
|
|
294
|
-
getMetroInstance({
|
|
348
|
+
diagnostics.measure('metro.init', () => getMetroInstance({
|
|
295
349
|
projectRoot,
|
|
296
350
|
harnessConfig: runtimeConfig,
|
|
297
351
|
websocketEndpoints: {
|
|
298
352
|
[HARNESS_BRIDGE_PATH]: bridge.ws,
|
|
299
353
|
},
|
|
300
|
-
|
|
354
|
+
watchMode: globalConfig.watch || globalConfig.watchAll,
|
|
355
|
+
}, sessionController.signal).then((instance) => {
|
|
301
356
|
sessionLogger.debug('Metro initialized');
|
|
357
|
+
// Attach the Metro diagnostics deriver before the eager prewarm
|
|
358
|
+
// fires so its bundle build/request events land in the trace.
|
|
359
|
+
disposeMetroDiagnostics = observeMetroEvents(instance.events, diagnostics);
|
|
360
|
+
if (runtimeConfig.eagerPrewarm !== false) {
|
|
361
|
+
// Kick off the first bundle build while the platform is still booting.
|
|
362
|
+
// prewarm() memoizes this first call, so its session-lifetime setup
|
|
363
|
+
// signal is the one that matters — later callers' signals cannot
|
|
364
|
+
// cancel it. Not awaited: metro.init must resolve immediately.
|
|
365
|
+
const prewarmSpan = diagnostics.start('metro.prewarm');
|
|
366
|
+
instance
|
|
367
|
+
.prewarm({
|
|
368
|
+
platform: platform.platformId,
|
|
369
|
+
signal: sessionController.signal,
|
|
370
|
+
})
|
|
371
|
+
.then((completed) => prewarmSpan.end({ completed }))
|
|
372
|
+
// Swallow AbortError on teardown so it never becomes an
|
|
373
|
+
// unhandled rejection; non-abort failures resolve false.
|
|
374
|
+
.catch((error) => prewarmSpan.fail(error));
|
|
375
|
+
}
|
|
302
376
|
return instance;
|
|
303
|
-
}),
|
|
304
|
-
withPlatformReadyTimeout({
|
|
377
|
+
})),
|
|
378
|
+
diagnostics.measure('platform.init', () => withPlatformReadyTimeout({
|
|
305
379
|
timeout: runtimeConfig.platformReadyTimeout,
|
|
306
|
-
signal:
|
|
380
|
+
signal: sessionController.signal,
|
|
307
381
|
work: async (signal) => {
|
|
308
382
|
return await import(platform.runner).then((module) => module.default(platform.config, runtimeConfig, {
|
|
309
383
|
signal,
|
|
310
384
|
crashArtifactWriter,
|
|
385
|
+
diagnostics,
|
|
311
386
|
})).then((instance) => {
|
|
312
387
|
sessionLogger.debug('platform runner initialized');
|
|
313
388
|
return instance;
|
|
314
389
|
});
|
|
315
390
|
},
|
|
316
|
-
}),
|
|
391
|
+
}), { runner: platform.runner, platformId: platform.platformId }),
|
|
317
392
|
]);
|
|
318
393
|
}
|
|
319
394
|
catch (error) {
|
|
320
|
-
// Only bridge
|
|
395
|
+
// Only bridge and the Metro diagnostics listener need cleanup here;
|
|
396
|
+
// leases are released by the outer catch.
|
|
397
|
+
disposeMetroDiagnostics();
|
|
321
398
|
await bridge.dispose();
|
|
322
399
|
throw error;
|
|
323
400
|
}
|
|
@@ -335,7 +412,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
335
412
|
const restartAppSession = async () => {
|
|
336
413
|
await crashMonitor.stop();
|
|
337
414
|
await disposeCurrentAppSession();
|
|
338
|
-
const session = await platformInstance.createAppSession(appLaunchOptions);
|
|
415
|
+
const session = await diagnostics.measure('app.session.create', () => platformInstance.createAppSession(appLaunchOptions));
|
|
339
416
|
currentAppSession = session;
|
|
340
417
|
crashMonitor.setAppSession(session);
|
|
341
418
|
await crashMonitor.start();
|
|
@@ -398,11 +475,13 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
398
475
|
if (runtimeConfig.forwardClientLogs) {
|
|
399
476
|
metroInstance.events.addListener(clientLogListener);
|
|
400
477
|
}
|
|
478
|
+
const disposeBridgeDiagnostics = observeBridgeEvents(bridge, diagnostics);
|
|
401
479
|
sessionLogger.debug('registered runtime, bridge, and Metro listeners');
|
|
402
480
|
// --- Dispose ---
|
|
403
481
|
let disposePromise = null;
|
|
404
482
|
const disposeOnce = async (reason) => {
|
|
405
483
|
sessionLogger.debug('disposing session (reason=%s)', reason);
|
|
484
|
+
const disposeSpan = diagnostics.start('dispose.total', { reason });
|
|
406
485
|
let hookError;
|
|
407
486
|
try {
|
|
408
487
|
await hooks.drain();
|
|
@@ -434,6 +513,8 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
434
513
|
bridge.off('disconnected', onDisconnected);
|
|
435
514
|
bridge.off('event', bridgeEventListener);
|
|
436
515
|
bridge.off('event', onTestRunnerEvent);
|
|
516
|
+
disposeMetroDiagnostics();
|
|
517
|
+
disposeBridgeDiagnostics();
|
|
437
518
|
const nativeCoverageConfig = runtimeConfig.coverage?.native?.ios;
|
|
438
519
|
if (nativeCoverageConfig?.pods?.length && platformInstance.collectNativeCoverage) {
|
|
439
520
|
try {
|
|
@@ -453,9 +534,13 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
453
534
|
}
|
|
454
535
|
let cleanupError;
|
|
455
536
|
try {
|
|
537
|
+
// The app session (and its logcat/device log stream) must be fully
|
|
538
|
+
// torn down before the platform is disposed: platform disposal can
|
|
539
|
+
// shut down the device/emulator, and doing that concurrently with
|
|
540
|
+
// the app session's own cleanup races the device's log stream
|
|
541
|
+
// against the device disappearing out from under it.
|
|
542
|
+
await Promise.all([crashMonitor.dispose(), disposeCurrentAppSession()]);
|
|
456
543
|
await Promise.all([
|
|
457
|
-
crashMonitor.dispose(),
|
|
458
|
-
disposeCurrentAppSession(),
|
|
459
544
|
bridge.dispose(),
|
|
460
545
|
platformInstance.dispose(),
|
|
461
546
|
metroInstance.dispose(),
|
|
@@ -467,13 +552,18 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
467
552
|
}
|
|
468
553
|
finally {
|
|
469
554
|
await resourceLease.release();
|
|
470
|
-
|
|
555
|
+
sessionController.abort();
|
|
471
556
|
}
|
|
472
557
|
sessionLogger.debug('session resources disposed');
|
|
473
|
-
if (hookError)
|
|
558
|
+
if (hookError) {
|
|
559
|
+
disposeSpan.fail(hookError);
|
|
474
560
|
throw hookError;
|
|
475
|
-
|
|
561
|
+
}
|
|
562
|
+
if (cleanupError) {
|
|
563
|
+
disposeSpan.fail(cleanupError);
|
|
476
564
|
throw cleanupError;
|
|
565
|
+
}
|
|
566
|
+
disposeSpan.end();
|
|
477
567
|
};
|
|
478
568
|
const dispose = (reason = 'normal') => {
|
|
479
569
|
disposePromise ??= disposeOnce(reason);
|
|
@@ -483,7 +573,10 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
483
573
|
// that all infrastructure is up.
|
|
484
574
|
process.off('SIGTERM', onEarlySignal);
|
|
485
575
|
process.off('SIGINT', onEarlySignal);
|
|
486
|
-
const onSignal = () =>
|
|
576
|
+
const onSignal = () => {
|
|
577
|
+
const exitCode = getSignalExitCodeForRunState(currentRun);
|
|
578
|
+
void dispose('abort').then(() => process.exit(exitCode), () => process.exit(1));
|
|
579
|
+
};
|
|
487
580
|
process.once('SIGTERM', onSignal);
|
|
488
581
|
process.once('SIGINT', onSignal);
|
|
489
582
|
// --- Startup hooks ---
|
|
@@ -500,28 +593,42 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
500
593
|
throw error;
|
|
501
594
|
}
|
|
502
595
|
logTestEnvironmentReady(platform);
|
|
596
|
+
diagnostics.record({
|
|
597
|
+
label: 'session.setup',
|
|
598
|
+
duration: Date.now() - setupStartedAt,
|
|
599
|
+
});
|
|
503
600
|
sessionLogger.debug('session ready');
|
|
504
601
|
// --- Public API ---
|
|
505
602
|
const ensureAppReady = async (testFilePath) => {
|
|
506
603
|
await hooks.drain();
|
|
507
604
|
sessionLogger.debug('ensuring app is ready for %s', testFilePath);
|
|
605
|
+
const appReadySpan = diagnostics.start('app.ready.total');
|
|
508
606
|
if (crashMonitor.isAlive() && bridge.connection !== null && currentAppSession) {
|
|
509
607
|
const state = await currentAppSession.getState();
|
|
510
608
|
if (state.status === 'running') {
|
|
511
609
|
sessionLogger.debug('reusing existing ready app for %s', testFilePath);
|
|
610
|
+
appReadySpan.end({ file: testFilePath, reused: true });
|
|
512
611
|
return;
|
|
513
612
|
}
|
|
514
613
|
}
|
|
515
614
|
crashMonitor.reset();
|
|
516
615
|
sessionLogger.debug('app not ready, waiting for launch and runtime readiness');
|
|
517
|
-
|
|
616
|
+
try {
|
|
617
|
+
await waitForAppReady(appReadyBaseOptions, testFilePath);
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
appReadySpan.fail(error, { file: testFilePath, reused: false });
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
518
623
|
await hooks.drain();
|
|
519
624
|
sessionLogger.debug('app is ready for %s', testFilePath);
|
|
625
|
+
appReadySpan.end({ file: testFilePath, reused: false });
|
|
520
626
|
};
|
|
521
|
-
|
|
522
|
-
|
|
627
|
+
// Shared by restartApp (crash/timeout recovery) and the 'process' reset
|
|
628
|
+
// strategy: kill the current app session and either wait for a fresh one
|
|
629
|
+
// to become ready for testFilePath, or (no testFilePath) just launch one.
|
|
630
|
+
const performRestart = async (testFilePath) => {
|
|
523
631
|
await crashMonitor.stop();
|
|
524
|
-
sessionLogger.debug('restarting app (testFile=%s mode=%s)', testFilePath ?? 'n/a', testFilePath ? 'stop-and-ensure-ready' : 'direct-restart');
|
|
525
632
|
await disposeCurrentAppSession();
|
|
526
633
|
if (testFilePath) {
|
|
527
634
|
await ensureAppReady(testFilePath);
|
|
@@ -533,8 +640,65 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
533
640
|
crashMonitor.reset();
|
|
534
641
|
await crashMonitor.start();
|
|
535
642
|
}
|
|
643
|
+
};
|
|
644
|
+
const restartApp = async (testFilePath) => {
|
|
645
|
+
await hooks.drain();
|
|
646
|
+
const reason = testFilePath
|
|
647
|
+
? 'stop-and-ensure-ready'
|
|
648
|
+
: 'direct-restart';
|
|
649
|
+
sessionLogger.debug('restarting app (testFile=%s mode=%s)', testFilePath ?? 'n/a', reason);
|
|
650
|
+
const restartSpan = diagnostics.start('app.restart', { reason });
|
|
651
|
+
try {
|
|
652
|
+
await performRestart(testFilePath);
|
|
653
|
+
await hooks.drain();
|
|
654
|
+
sessionLogger.debug('restart completed');
|
|
655
|
+
restartSpan.end();
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
restartSpan.fail(error);
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
const processResetStrategy = createProcessResetStrategy({
|
|
663
|
+
restart: performRestart,
|
|
664
|
+
});
|
|
665
|
+
const runtimeResetStrategy = createRuntimeResetStrategy({
|
|
666
|
+
getConnection: () => bridge.connection,
|
|
667
|
+
expectDisconnect: crashMonitor.expectDisconnect,
|
|
668
|
+
waitForReconnect: (signal) => waitForNextConnected({ bridge, signal }),
|
|
669
|
+
// Pending rpc.invoke calls reject with AppBridgeDisconnectedError when
|
|
670
|
+
// the app connection drops (see bridge server's disconnect()).
|
|
671
|
+
isDisconnectError: (error) => error instanceof AppBridgeDisconnectedError,
|
|
672
|
+
timeoutMs: runtimeConfig.bundleStartTimeout ?? 60000,
|
|
673
|
+
});
|
|
674
|
+
const resetEnvironment = async (testFilePath) => {
|
|
675
|
+
const requestedKind = resolveResetStrategyKind(runtimeConfig.resetEnvironmentBetweenTestFiles);
|
|
676
|
+
if (requestedKind === null) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
536
679
|
await hooks.drain();
|
|
537
|
-
sessionLogger.debug('
|
|
680
|
+
sessionLogger.debug('resetting environment (testFile=%s kind=%s)', testFilePath, requestedKind);
|
|
681
|
+
let escalated = false;
|
|
682
|
+
const strategy = requestedKind === 'runtime'
|
|
683
|
+
? withEscalation(runtimeResetStrategy, processResetStrategy, {
|
|
684
|
+
onEscalate: (error) => {
|
|
685
|
+
escalated = true;
|
|
686
|
+
sessionLogger.debug('runtime reset failed, escalating to process restart: %s', error);
|
|
687
|
+
},
|
|
688
|
+
})
|
|
689
|
+
: processResetStrategy;
|
|
690
|
+
const resetSpan = diagnostics.start('app.reset', { requested: requestedKind });
|
|
691
|
+
const performed = () => (escalated ? 'process' : requestedKind);
|
|
692
|
+
try {
|
|
693
|
+
await strategy.reset({ testFilePath });
|
|
694
|
+
await hooks.drain();
|
|
695
|
+
sessionLogger.debug('environment reset completed');
|
|
696
|
+
resetSpan.end({ performed: performed(), escalated });
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
resetSpan.fail(error, { performed: performed(), escalated });
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
538
702
|
};
|
|
539
703
|
const runTestFile = async (testPath, options) => {
|
|
540
704
|
await hooks.drain();
|
|
@@ -583,6 +747,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
583
747
|
return {
|
|
584
748
|
config: runtimeConfig,
|
|
585
749
|
context,
|
|
750
|
+
diagnostics,
|
|
586
751
|
onTestRunnerEvent: (listener) => {
|
|
587
752
|
testRunnerEventListeners.add(listener);
|
|
588
753
|
return () => {
|
|
@@ -592,26 +757,34 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
592
757
|
runTestFile,
|
|
593
758
|
ensureAppReady,
|
|
594
759
|
restartApp,
|
|
760
|
+
resetEnvironment,
|
|
595
761
|
resetCrashState: () => crashMonitor.reset(),
|
|
596
762
|
flushClientLogs,
|
|
597
763
|
callHook: async (name, payload) => {
|
|
598
764
|
await hooks.drain();
|
|
599
|
-
await pluginManager.callHook(name, payload);
|
|
765
|
+
await diagnostics.measure('plugin.hook', () => pluginManager.callHook(name, payload), { hook: String(name) });
|
|
600
766
|
await hooks.drain();
|
|
601
767
|
},
|
|
602
768
|
setRunState: (state) => {
|
|
603
769
|
currentRun = state;
|
|
604
770
|
},
|
|
605
|
-
dispose: (reason = 'normal') => {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
771
|
+
dispose: async (reason = 'normal') => {
|
|
772
|
+
try {
|
|
773
|
+
await dispose(reason);
|
|
774
|
+
}
|
|
775
|
+
finally {
|
|
776
|
+
process.off('SIGTERM', onSignal);
|
|
777
|
+
process.off('SIGINT', onSignal);
|
|
778
|
+
}
|
|
609
779
|
},
|
|
610
780
|
};
|
|
611
781
|
}
|
|
612
782
|
catch (error) {
|
|
613
783
|
process.off('SIGTERM', onEarlySignal);
|
|
614
784
|
process.off('SIGINT', onEarlySignal);
|
|
785
|
+
// Cancels any inflight init work still holding sessionController.signal
|
|
786
|
+
// (e.g. a platform init call abandoned after a readiness timeout race).
|
|
787
|
+
sessionController.abort();
|
|
615
788
|
await Promise.allSettled([resourceLease.release(), metroPortLease?.release()]);
|
|
616
789
|
throw error;
|
|
617
790
|
}
|
package/dist/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EACV,eAAe,IAAI,sBAAsB,EAE1C,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EACV,eAAe,IAAI,sBAAsB,EAE1C,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAmE3D,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,aAAa,EAAE,UAAU,CAAC,aAAa,CAAC;IACxC,YAAY,EAAE,UAAU,CAAC,YAAY,CAAC;CACvC,KAAK,OAAO,CAAC;IACZ,UAAU,EAAE,cAAc,CAAC;IAC3B,aAAa,EAAE,sBAAsB,CAAC;IACtC,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH,eAAO,MAAM,kBAAkB,EAAE,kBA0FhC,CAAC"}
|
package/dist/run.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { formatResultsErrors } from 'jest-message-util';
|
|
3
3
|
import { toTestResult } from './toTestResult.js';
|
|
4
|
+
import { formatHarnessErrorMessage } from './format-harness-error.js';
|
|
4
5
|
// Helper function to flatten nested test suites into a flat array of tests with hierarchy
|
|
5
6
|
const flattenTests = (suiteResult, ancestorTitles = []) => {
|
|
6
7
|
const tests = [];
|
|
8
|
+
if (suiteResult.error) {
|
|
9
|
+
tests.push({
|
|
10
|
+
name: suiteResult.name,
|
|
11
|
+
status: 'failed',
|
|
12
|
+
duration: suiteResult.duration,
|
|
13
|
+
error: suiteResult.error,
|
|
14
|
+
ancestorTitles: [...ancestorTitles],
|
|
15
|
+
fullName: [...ancestorTitles, suiteResult.name].join(' '),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
7
18
|
// Add tests from current suite with current hierarchy
|
|
8
19
|
for (const test of suiteResult.tests) {
|
|
9
20
|
tests.push({
|
|
@@ -48,10 +59,16 @@ export const runHarnessTestFile = async ({ testPath, globalConfig, projectConfig
|
|
|
48
59
|
// Extract setup files from Jest config and convert to relative paths
|
|
49
60
|
const setupFiles = projectConfig.setupFiles?.map((setupFile) => path.relative(globalConfig.rootDir, setupFile));
|
|
50
61
|
const setupFilesAfterEnv = projectConfig.setupFilesAfterEnv?.map((setupFile) => path.relative(globalConfig.rootDir, setupFile));
|
|
62
|
+
const testTimeout = session.config.testTimeout ??
|
|
63
|
+
projectConfig
|
|
64
|
+
.testTimeout ??
|
|
65
|
+
globalConfig
|
|
66
|
+
.testTimeout;
|
|
51
67
|
const harnessResult = await session.runTestFile(relativeTestPath, {
|
|
52
68
|
testNamePattern: globalConfig.testNamePattern,
|
|
53
69
|
setupFiles,
|
|
54
70
|
setupFilesAfterEnv,
|
|
71
|
+
testTimeout,
|
|
55
72
|
runner: session.context.platform.runner,
|
|
56
73
|
});
|
|
57
74
|
const end = Date.now();
|
|
@@ -59,13 +76,13 @@ export const runHarnessTestFile = async ({ testPath, globalConfig, projectConfig
|
|
|
59
76
|
const stats = calculateStats(allTests);
|
|
60
77
|
// Convert TestResult[] to the format expected by toTestResult
|
|
61
78
|
const tests = allTests.map((test) => {
|
|
62
|
-
const errorMessage = test.error?.message;
|
|
63
79
|
const codeFrame = test.error?.codeFrame;
|
|
80
|
+
const errorMessage = formatHarnessErrorMessage(test.error, {
|
|
81
|
+
testStartedAt: test.startedAt,
|
|
82
|
+
});
|
|
64
83
|
return {
|
|
65
84
|
duration: test.duration,
|
|
66
|
-
errorMessage
|
|
67
|
-
? `${errorMessage}${codeFrame ? `\n\n${codeFrame.content}` : ''}`
|
|
68
|
-
: undefined,
|
|
85
|
+
errorMessage,
|
|
69
86
|
title: test.name,
|
|
70
87
|
fullName: test.fullName,
|
|
71
88
|
status: test.status,
|