@react-native-harness/jest 1.1.0-rc.2 → 1.1.0-rc.4
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__/errors.test.js +6 -1
- package/dist/__tests__/harness-cache.test.js +1 -1
- package/dist/__tests__/harness.test.js +205 -3
- package/dist/__tests__/metro-port.test.d.ts +2 -0
- package/dist/__tests__/metro-port.test.d.ts.map +1 -0
- package/dist/__tests__/metro-port.test.js +52 -0
- package/dist/__tests__/resource-lock.test.d.ts +2 -0
- package/dist/__tests__/resource-lock.test.d.ts.map +1 -0
- package/dist/__tests__/resource-lock.test.js +138 -0
- package/dist/action-hooks.d.ts +9 -0
- package/dist/action-hooks.d.ts.map +1 -0
- package/dist/action-hooks.js +36 -0
- package/dist/cli-args.test.d.ts +2 -0
- package/dist/cli-args.test.d.ts.map +1 -0
- package/dist/cli-args.test.js +1 -0
- package/dist/crash-monitor.d.ts +16 -0
- package/dist/crash-monitor.d.ts.map +1 -0
- package/dist/crash-monitor.js +75 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -2
- package/dist/event-mapper.d.ts +16 -0
- package/dist/event-mapper.d.ts.map +1 -0
- package/dist/event-mapper.js +120 -0
- package/dist/global-setup.d.ts +3 -0
- package/dist/global-setup.d.ts.map +1 -0
- package/dist/global-setup.js +60 -0
- package/dist/global-teardown.d.ts +3 -0
- package/dist/global-teardown.d.ts.map +1 -0
- package/dist/global-teardown.js +11 -0
- package/dist/harness-reporter.d.ts +8 -0
- package/dist/harness-reporter.d.ts.map +1 -0
- package/dist/harness-reporter.js +12 -0
- package/dist/harness.d.ts +1 -0
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +491 -386
- package/dist/jest-options.d.ts +2 -0
- package/dist/jest-options.d.ts.map +1 -0
- package/dist/jest-options.js +1 -0
- package/dist/logs.d.ts +4 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +12 -0
- package/dist/metro-port.d.ts +16 -0
- package/dist/metro-port.d.ts.map +1 -0
- package/dist/metro-port.js +48 -0
- package/dist/resource-lock.d.ts +25 -0
- package/dist/resource-lock.d.ts.map +1 -0
- package/dist/resource-lock.js +299 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +12 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +7 -7
- package/src/__tests__/errors.test.ts +18 -6
- package/src/__tests__/harness-cache.test.ts +2 -2
- package/src/__tests__/harness.test.ts +342 -19
- package/src/__tests__/metro-port.test.ts +61 -0
- package/src/__tests__/resource-lock.test.ts +192 -0
- package/src/action-hooks.ts +66 -0
- package/src/errors.ts +23 -4
- package/src/harness.ts +609 -447
- package/src/logs.ts +24 -3
- package/src/metro-port.ts +76 -0
- package/src/resource-lock.ts +472 -0
- package/src/setup.ts +22 -2
package/src/harness.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
BridgeServer,
|
|
4
4
|
} from '@react-native-harness/bridge/server';
|
|
5
5
|
import {
|
|
6
|
+
HARNESS_BRIDGE_PATH,
|
|
6
7
|
HarnessContext,
|
|
7
8
|
type BridgeEvents,
|
|
8
9
|
type DeviceDescriptor,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
type AppMonitorEvent,
|
|
14
15
|
type AppLaunchOptions,
|
|
15
16
|
HarnessPlatform,
|
|
17
|
+
type HarnessPlatformInitOptions,
|
|
16
18
|
HarnessPlatformRunner,
|
|
17
19
|
} from '@react-native-harness/platforms';
|
|
18
20
|
import {
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
isMetroCacheReusable,
|
|
21
23
|
waitForMetroBackedAppReady,
|
|
22
24
|
type MetroInstance,
|
|
25
|
+
type MetroWebSocketEndpoint,
|
|
23
26
|
type ReportableEvent,
|
|
24
27
|
} from '@react-native-harness/bundler-metro';
|
|
25
28
|
import {
|
|
@@ -30,8 +33,13 @@ import {
|
|
|
30
33
|
type HarnessRunStatus,
|
|
31
34
|
type HarnessRunSummary,
|
|
32
35
|
} from '@react-native-harness/plugins';
|
|
33
|
-
import {
|
|
34
|
-
|
|
36
|
+
import {
|
|
37
|
+
logger,
|
|
38
|
+
createCrashArtifactWriter,
|
|
39
|
+
getTimeoutSignal,
|
|
40
|
+
raceAbortSignals,
|
|
41
|
+
} from '@react-native-harness/tools';
|
|
42
|
+
import { PlatformReadyTimeoutError } from './errors.js';
|
|
35
43
|
import { Config as HarnessConfig } from '@react-native-harness/config';
|
|
36
44
|
import {
|
|
37
45
|
createCrashSupervisor,
|
|
@@ -39,9 +47,18 @@ import {
|
|
|
39
47
|
} from './crash-supervisor.js';
|
|
40
48
|
import { createClientLogListener } from './client-log-handler.js';
|
|
41
49
|
import path from 'node:path';
|
|
42
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
logMetroCacheReused,
|
|
52
|
+
logMetroPortFallback,
|
|
53
|
+
logRunnerStarting,
|
|
54
|
+
logRunnerStillWaitingInQueue,
|
|
55
|
+
logRunnerWaitingInQueue,
|
|
56
|
+
} from './logs.js';
|
|
57
|
+
import { createResourceLockManager } from './resource-lock.js';
|
|
58
|
+
import { resolveHarnessMetroPort } from './metro-port.js';
|
|
43
59
|
|
|
44
60
|
const harnessLogger = logger.child('runtime');
|
|
61
|
+
const resourceLockManager = createResourceLockManager();
|
|
45
62
|
|
|
46
63
|
export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
|
|
47
64
|
|
|
@@ -58,6 +75,7 @@ export type HarnessRunState = {
|
|
|
58
75
|
|
|
59
76
|
export type Harness = {
|
|
60
77
|
context: HarnessContext;
|
|
78
|
+
config: HarnessConfig;
|
|
61
79
|
runTests: (
|
|
62
80
|
path: string,
|
|
63
81
|
options: HarnessRunTestsOptions
|
|
@@ -76,10 +94,7 @@ export const maybeLogMetroCacheReuse = (
|
|
|
76
94
|
platform: HarnessPlatform,
|
|
77
95
|
projectRoot: string
|
|
78
96
|
): void => {
|
|
79
|
-
if (
|
|
80
|
-
config.unstable__enableMetroCache &&
|
|
81
|
-
isMetroCacheReusable(projectRoot)
|
|
82
|
-
) {
|
|
97
|
+
if (config.unstable__enableMetroCache && isMetroCacheReusable(projectRoot)) {
|
|
83
98
|
logMetroCacheReused(platform);
|
|
84
99
|
}
|
|
85
100
|
};
|
|
@@ -87,6 +102,9 @@ export const maybeLogMetroCacheReuse = (
|
|
|
87
102
|
const createAbortError = () =>
|
|
88
103
|
new DOMException('The operation was aborted', 'AbortError');
|
|
89
104
|
|
|
105
|
+
const getDefaultResourceLockKey = (platform: HarnessPlatform): string =>
|
|
106
|
+
`${platform.platformId}:${platform.name}`;
|
|
107
|
+
|
|
90
108
|
const waitForAbort = (signal: AbortSignal): Promise<never> => {
|
|
91
109
|
if (signal.aborted) {
|
|
92
110
|
return Promise.reject(signal.reason ?? createAbortError());
|
|
@@ -103,6 +121,30 @@ const waitForAbort = (signal: AbortSignal): Promise<never> => {
|
|
|
103
121
|
});
|
|
104
122
|
};
|
|
105
123
|
|
|
124
|
+
const withPlatformReadyTimeout = async <T>(options: {
|
|
125
|
+
timeout: number;
|
|
126
|
+
signal: AbortSignal;
|
|
127
|
+
work: (signal: AbortSignal) => Promise<T>;
|
|
128
|
+
}): Promise<T> => {
|
|
129
|
+
const timeoutSignal = getTimeoutSignal(options.timeout);
|
|
130
|
+
const combinedSignal = raceAbortSignals([options.signal, timeoutSignal]);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
return await options.work(combinedSignal);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (
|
|
136
|
+
error instanceof DOMException &&
|
|
137
|
+
error.name === 'AbortError' &&
|
|
138
|
+
timeoutSignal.aborted &&
|
|
139
|
+
!options.signal.aborted
|
|
140
|
+
) {
|
|
141
|
+
throw new PlatformReadyTimeoutError(options.timeout);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
106
148
|
export const waitForAppReady = async (options: {
|
|
107
149
|
metroInstance: MetroInstance;
|
|
108
150
|
serverBridge: BridgeServer;
|
|
@@ -207,470 +249,600 @@ const getHarnessInternal = async (
|
|
|
207
249
|
platform.name,
|
|
208
250
|
platform.platformId
|
|
209
251
|
);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
testFilePath == null ? undefined : path.relative(projectRoot, testFilePath);
|
|
229
|
-
const setActiveTestFilePath = (testFilePath?: string) => {
|
|
230
|
-
activeTestFilePath = toRelativeTestFilePath(testFilePath);
|
|
231
|
-
};
|
|
232
|
-
const flushPendingHooks = async () => {
|
|
233
|
-
if (pendingHookPromises.size > 0) {
|
|
234
|
-
await Promise.allSettled([...pendingHookPromises]);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (pendingHookError !== undefined) {
|
|
238
|
-
const error = pendingHookError;
|
|
239
|
-
pendingHookError = undefined;
|
|
240
|
-
throw error;
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
const trackHook = (promise: Promise<void>) => {
|
|
244
|
-
const trackedPromise = promise
|
|
245
|
-
.catch((error) => {
|
|
246
|
-
pendingHookError ??= error;
|
|
247
|
-
})
|
|
248
|
-
.finally(() => {
|
|
249
|
-
pendingHookPromises.delete(trackedPromise);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
pendingHookPromises.add(trackedPromise);
|
|
253
|
-
};
|
|
254
|
-
const scheduleHook = <
|
|
255
|
-
TName extends keyof FlatHarnessHookContexts<object, HarnessConfig, HarnessPlatform>,
|
|
256
|
-
>(
|
|
257
|
-
name: TName,
|
|
258
|
-
payload: Omit<
|
|
259
|
-
FlatHarnessHookContexts<object, HarnessConfig, HarnessPlatform>[TName],
|
|
260
|
-
| 'plugin'
|
|
261
|
-
| 'logger'
|
|
262
|
-
| 'projectRoot'
|
|
263
|
-
| 'config'
|
|
264
|
-
| 'runner'
|
|
265
|
-
| 'platform'
|
|
266
|
-
| 'state'
|
|
267
|
-
| 'timestamp'
|
|
268
|
-
| 'abortSignal'
|
|
269
|
-
| 'meta'
|
|
270
|
-
>
|
|
271
|
-
) => {
|
|
272
|
-
trackHook(pluginManager.callHook(name, payload));
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
harnessLogger.debug(
|
|
276
|
-
'starting Metro, platform runner, and bridge initialization'
|
|
277
|
-
);
|
|
278
|
-
const [metroInstance, platformInstance, serverBridge] = await Promise.all([
|
|
279
|
-
getMetroInstance({ projectRoot, harnessConfig: config }, signal).then(
|
|
280
|
-
(instance) => {
|
|
281
|
-
harnessLogger.debug('Metro initialized');
|
|
282
|
-
return instance;
|
|
252
|
+
const resourceLockKey = await (platform.getResourceLockKey?.() ??
|
|
253
|
+
getDefaultResourceLockKey(platform));
|
|
254
|
+
let didWaitForResourceLock = false;
|
|
255
|
+
let lastStillWaitingLogAt = 0;
|
|
256
|
+
const resourceLease = await resourceLockManager.acquire(resourceLockKey, {
|
|
257
|
+
signal,
|
|
258
|
+
onWait: () => {
|
|
259
|
+
didWaitForResourceLock = true;
|
|
260
|
+
logRunnerWaitingInQueue(platform);
|
|
261
|
+
harnessLogger.debug(
|
|
262
|
+
'waiting in queue for runner=%s key=%s',
|
|
263
|
+
platform.name,
|
|
264
|
+
resourceLockKey
|
|
265
|
+
);
|
|
266
|
+
},
|
|
267
|
+
onStillWaiting: (elapsedMs) => {
|
|
268
|
+
if (elapsedMs - lastStillWaitingLogAt < 5000) {
|
|
269
|
+
return;
|
|
283
270
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
.then((instance) => {
|
|
288
|
-
harnessLogger.debug('platform runner initialized');
|
|
289
|
-
return instance;
|
|
290
|
-
}),
|
|
291
|
-
getBridgeServer({
|
|
292
|
-
port: config.webSocketPort,
|
|
293
|
-
timeout: config.bridgeTimeout,
|
|
294
|
-
context,
|
|
295
|
-
}).then((bridge) => {
|
|
271
|
+
|
|
272
|
+
lastStillWaitingLogAt = elapsedMs;
|
|
273
|
+
logRunnerStillWaitingInQueue(platform);
|
|
296
274
|
harnessLogger.debug(
|
|
297
|
-
'
|
|
298
|
-
|
|
275
|
+
'still waiting in queue for runner=%s key=%s elapsedMs=%d',
|
|
276
|
+
platform.name,
|
|
277
|
+
resourceLockKey,
|
|
278
|
+
elapsedMs
|
|
299
279
|
);
|
|
300
|
-
|
|
301
|
-
}),
|
|
302
|
-
]);
|
|
303
|
-
const crashArtifactWriter = createCrashArtifactWriter({
|
|
304
|
-
runnerName: platform.name,
|
|
305
|
-
platformId: platform.platformId,
|
|
306
|
-
});
|
|
307
|
-
const appMonitor = platformInstance.createAppMonitor({
|
|
308
|
-
crashArtifactWriter,
|
|
280
|
+
},
|
|
309
281
|
});
|
|
310
|
-
|
|
311
|
-
platform
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
runId,
|
|
331
|
-
file: event.file,
|
|
332
|
-
duration: event.duration,
|
|
333
|
-
totalTests: event.totalTests,
|
|
334
|
-
});
|
|
335
|
-
break;
|
|
336
|
-
case 'suite-started':
|
|
337
|
-
scheduleHook('suite:started', {
|
|
338
|
-
runId,
|
|
339
|
-
file: event.file,
|
|
340
|
-
name: event.name,
|
|
341
|
-
});
|
|
342
|
-
break;
|
|
343
|
-
case 'suite-finished':
|
|
344
|
-
scheduleHook('suite:finished', {
|
|
345
|
-
runId,
|
|
346
|
-
file: event.file,
|
|
347
|
-
name: event.name,
|
|
348
|
-
duration: event.duration,
|
|
349
|
-
status: event.status,
|
|
350
|
-
error: event.error,
|
|
351
|
-
});
|
|
352
|
-
break;
|
|
353
|
-
case 'test-started':
|
|
354
|
-
scheduleHook('test:started', {
|
|
355
|
-
runId,
|
|
356
|
-
file: event.file,
|
|
357
|
-
suite: event.suite,
|
|
358
|
-
name: event.name,
|
|
359
|
-
});
|
|
360
|
-
break;
|
|
361
|
-
case 'test-finished':
|
|
362
|
-
scheduleHook('test:finished', {
|
|
363
|
-
runId,
|
|
364
|
-
file: event.file,
|
|
365
|
-
suite: event.suite,
|
|
366
|
-
name: event.name,
|
|
367
|
-
duration: event.duration,
|
|
368
|
-
status: event.status,
|
|
369
|
-
error: event.error,
|
|
370
|
-
});
|
|
371
|
-
break;
|
|
372
|
-
case 'module-bundling-started':
|
|
373
|
-
scheduleHook('metro:bundle-started', {
|
|
374
|
-
runId,
|
|
375
|
-
target: 'module',
|
|
376
|
-
file: event.file,
|
|
377
|
-
});
|
|
378
|
-
break;
|
|
379
|
-
case 'module-bundling-finished':
|
|
380
|
-
scheduleHook('metro:bundle-finished', {
|
|
381
|
-
runId,
|
|
382
|
-
target: 'module',
|
|
383
|
-
file: event.file,
|
|
384
|
-
duration: event.duration,
|
|
385
|
-
});
|
|
386
|
-
break;
|
|
387
|
-
case 'module-bundling-failed':
|
|
388
|
-
scheduleHook('metro:bundle-failed', {
|
|
389
|
-
runId,
|
|
390
|
-
target: 'module',
|
|
391
|
-
file: event.file,
|
|
392
|
-
duration: event.duration,
|
|
393
|
-
error: event.error,
|
|
394
|
-
});
|
|
395
|
-
break;
|
|
396
|
-
case 'setup-file-bundling-started':
|
|
397
|
-
scheduleHook('metro:bundle-started', {
|
|
398
|
-
runId,
|
|
399
|
-
target: 'setupFile',
|
|
400
|
-
file: event.file,
|
|
401
|
-
setupType: event.setupType,
|
|
402
|
-
});
|
|
403
|
-
break;
|
|
404
|
-
case 'setup-file-bundling-finished':
|
|
405
|
-
scheduleHook('metro:bundle-finished', {
|
|
406
|
-
runId,
|
|
407
|
-
target: 'setupFile',
|
|
408
|
-
file: event.file,
|
|
409
|
-
setupType: event.setupType,
|
|
410
|
-
duration: event.duration,
|
|
411
|
-
});
|
|
412
|
-
break;
|
|
413
|
-
case 'setup-file-bundling-failed':
|
|
414
|
-
scheduleHook('metro:bundle-failed', {
|
|
415
|
-
runId,
|
|
416
|
-
target: 'setupFile',
|
|
417
|
-
file: event.file,
|
|
418
|
-
setupType: event.setupType,
|
|
419
|
-
duration: event.duration,
|
|
420
|
-
error: event.error,
|
|
421
|
-
});
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
const onMetroEvent = (event: ReportableEvent) => {
|
|
426
|
-
const runId = getCurrentRunId();
|
|
282
|
+
if (didWaitForResourceLock) {
|
|
283
|
+
logRunnerStarting(platform);
|
|
284
|
+
}
|
|
285
|
+
harnessLogger.debug(
|
|
286
|
+
'resource lock acquired for runner=%s key=%s',
|
|
287
|
+
platform.name,
|
|
288
|
+
resourceLockKey
|
|
289
|
+
);
|
|
290
|
+
try {
|
|
291
|
+
const {
|
|
292
|
+
config: runtimeConfig,
|
|
293
|
+
metroPortLease,
|
|
294
|
+
initialMetroPort,
|
|
295
|
+
didFallback,
|
|
296
|
+
} = await resolveHarnessMetroPort({
|
|
297
|
+
config,
|
|
298
|
+
platform,
|
|
299
|
+
resourceLockManager,
|
|
300
|
+
signal,
|
|
301
|
+
});
|
|
427
302
|
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
runId,
|
|
431
|
-
level: event.level,
|
|
432
|
-
data: event.data,
|
|
433
|
-
});
|
|
303
|
+
if (didFallback) {
|
|
304
|
+
logMetroPortFallback(initialMetroPort, runtimeConfig.metroPort);
|
|
434
305
|
}
|
|
435
|
-
};
|
|
436
|
-
const crashSupervisor = createCrashSupervisor({
|
|
437
|
-
appMonitor,
|
|
438
|
-
platformRunner: platformInstance,
|
|
439
|
-
});
|
|
440
306
|
|
|
441
|
-
|
|
442
|
-
|
|
307
|
+
maybeLogMetroCacheReuse(runtimeConfig, platform, projectRoot);
|
|
308
|
+
const pluginAbortController = new AbortController();
|
|
309
|
+
const pluginManager = createHarnessPluginManager<
|
|
310
|
+
HarnessConfig,
|
|
311
|
+
HarnessPlatform
|
|
312
|
+
>({
|
|
313
|
+
plugins: (runtimeConfig.plugins ?? []) as Array<
|
|
314
|
+
HarnessPlugin<object, HarnessConfig, HarnessPlatform>
|
|
315
|
+
>,
|
|
316
|
+
projectRoot,
|
|
317
|
+
config: runtimeConfig,
|
|
318
|
+
runner: platform,
|
|
319
|
+
abortSignal: pluginAbortController.signal,
|
|
320
|
+
});
|
|
321
|
+
let currentRun: HarnessRunState | null = null;
|
|
322
|
+
let activeTestFilePath: string | undefined;
|
|
323
|
+
const pendingHookPromises = new Set<Promise<void>>();
|
|
324
|
+
let pendingHookError: unknown;
|
|
325
|
+
|
|
326
|
+
const getCurrentRunId = () => currentRun?.runId;
|
|
327
|
+
const toRelativeTestFilePath = (testFilePath?: string) =>
|
|
328
|
+
testFilePath == null
|
|
329
|
+
? undefined
|
|
330
|
+
: path.relative(projectRoot, testFilePath);
|
|
331
|
+
const setActiveTestFilePath = (testFilePath?: string) => {
|
|
332
|
+
activeTestFilePath = toRelativeTestFilePath(testFilePath);
|
|
333
|
+
};
|
|
334
|
+
const flushPendingHooks = async () => {
|
|
335
|
+
if (pendingHookPromises.size > 0) {
|
|
336
|
+
await Promise.allSettled([...pendingHookPromises]);
|
|
337
|
+
}
|
|
443
338
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
339
|
+
if (pendingHookError !== undefined) {
|
|
340
|
+
const error = pendingHookError;
|
|
341
|
+
pendingHookError = undefined;
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const trackHook = (promise: Promise<void>) => {
|
|
346
|
+
const trackedPromise = promise
|
|
347
|
+
.catch((error) => {
|
|
348
|
+
pendingHookError ??= error;
|
|
349
|
+
})
|
|
350
|
+
.finally(() => {
|
|
351
|
+
pendingHookPromises.delete(trackedPromise);
|
|
352
|
+
});
|
|
448
353
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
354
|
+
pendingHookPromises.add(trackedPromise);
|
|
355
|
+
};
|
|
356
|
+
const scheduleHook = <
|
|
357
|
+
TName extends keyof FlatHarnessHookContexts<
|
|
358
|
+
object,
|
|
359
|
+
HarnessConfig,
|
|
360
|
+
HarnessPlatform
|
|
361
|
+
>
|
|
362
|
+
>(
|
|
363
|
+
name: TName,
|
|
364
|
+
payload: Omit<
|
|
365
|
+
FlatHarnessHookContexts<object, HarnessConfig, HarnessPlatform>[TName],
|
|
366
|
+
| 'plugin'
|
|
367
|
+
| 'logger'
|
|
368
|
+
| 'projectRoot'
|
|
369
|
+
| 'config'
|
|
370
|
+
| 'runner'
|
|
371
|
+
| 'platform'
|
|
372
|
+
| 'state'
|
|
373
|
+
| 'timestamp'
|
|
374
|
+
| 'abortSignal'
|
|
375
|
+
| 'meta'
|
|
376
|
+
>
|
|
377
|
+
) => {
|
|
378
|
+
trackHook(pluginManager.callHook(name, payload));
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const serverBridge = await getBridgeServer({
|
|
382
|
+
noServer: true,
|
|
383
|
+
timeout: runtimeConfig.bridgeTimeout,
|
|
384
|
+
context,
|
|
452
385
|
});
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
386
|
+
harnessLogger.debug(
|
|
387
|
+
'starting Metro, platform runner, and bridge initialization'
|
|
388
|
+
);
|
|
389
|
+
harnessLogger.debug(
|
|
390
|
+
'bridge server initialized on Metro websocket path %s',
|
|
391
|
+
HARNESS_BRIDGE_PATH
|
|
392
|
+
);
|
|
393
|
+
const [metroInstance, platformInstance] = await (async () => {
|
|
394
|
+
try {
|
|
395
|
+
return await Promise.all([
|
|
396
|
+
getMetroInstance(
|
|
397
|
+
{
|
|
398
|
+
projectRoot,
|
|
399
|
+
harnessConfig: runtimeConfig,
|
|
400
|
+
websocketEndpoints: {
|
|
401
|
+
[HARNESS_BRIDGE_PATH]:
|
|
402
|
+
serverBridge.ws as unknown as MetroWebSocketEndpoint,
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
signal
|
|
406
|
+
).then((instance) => {
|
|
407
|
+
harnessLogger.debug('Metro initialized');
|
|
408
|
+
return instance;
|
|
409
|
+
}),
|
|
410
|
+
withPlatformReadyTimeout({
|
|
411
|
+
timeout: runtimeConfig.platformReadyTimeout,
|
|
412
|
+
signal,
|
|
413
|
+
work: async () => {
|
|
414
|
+
return await import(platform.runner)
|
|
415
|
+
.then((module) =>
|
|
416
|
+
module.default(platform.config, runtimeConfig, {
|
|
417
|
+
signal,
|
|
418
|
+
} satisfies HarnessPlatformInitOptions)
|
|
419
|
+
)
|
|
420
|
+
.then((instance) => {
|
|
421
|
+
harnessLogger.debug('platform runner initialized');
|
|
422
|
+
return instance;
|
|
423
|
+
});
|
|
424
|
+
},
|
|
425
|
+
}),
|
|
426
|
+
]);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
await Promise.allSettled([
|
|
429
|
+
resourceLease.release(),
|
|
430
|
+
metroPortLease?.release(),
|
|
431
|
+
serverBridge.dispose(),
|
|
432
|
+
]);
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
})();
|
|
436
|
+
const crashArtifactWriter = createCrashArtifactWriter({
|
|
437
|
+
runnerName: platform.name,
|
|
438
|
+
platformId: platform.platformId,
|
|
439
|
+
});
|
|
440
|
+
const appMonitor = platformInstance.createAppMonitor({
|
|
441
|
+
crashArtifactWriter,
|
|
442
|
+
});
|
|
443
|
+
const appLaunchOptions = (
|
|
444
|
+
platform.config as { appLaunchOptions?: AppLaunchOptions }
|
|
445
|
+
).appLaunchOptions;
|
|
446
|
+
|
|
447
|
+
const clientLogListener = createClientLogListener();
|
|
448
|
+
const bridgeEventListener = (event: BridgeEvents) => {
|
|
449
|
+
const runId = getCurrentRunId();
|
|
450
|
+
if (!runId) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
459
453
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
454
|
+
switch (event.type) {
|
|
455
|
+
case 'collection-started':
|
|
456
|
+
scheduleHook('collection:started', {
|
|
457
|
+
runId,
|
|
458
|
+
file: event.file,
|
|
459
|
+
});
|
|
460
|
+
break;
|
|
461
|
+
case 'collection-finished':
|
|
462
|
+
scheduleHook('collection:finished', {
|
|
463
|
+
runId,
|
|
464
|
+
file: event.file,
|
|
465
|
+
duration: event.duration,
|
|
466
|
+
totalTests: event.totalTests,
|
|
467
|
+
});
|
|
468
|
+
break;
|
|
469
|
+
case 'suite-started':
|
|
470
|
+
scheduleHook('suite:started', {
|
|
471
|
+
runId,
|
|
472
|
+
file: event.file,
|
|
473
|
+
name: event.name,
|
|
474
|
+
});
|
|
475
|
+
break;
|
|
476
|
+
case 'suite-finished':
|
|
477
|
+
scheduleHook('suite:finished', {
|
|
478
|
+
runId,
|
|
479
|
+
file: event.file,
|
|
480
|
+
name: event.name,
|
|
481
|
+
duration: event.duration,
|
|
482
|
+
status: event.status,
|
|
483
|
+
error: event.error,
|
|
484
|
+
});
|
|
485
|
+
break;
|
|
486
|
+
case 'test-started':
|
|
487
|
+
scheduleHook('test:started', {
|
|
488
|
+
runId,
|
|
489
|
+
file: event.file,
|
|
490
|
+
suite: event.suite,
|
|
491
|
+
name: event.name,
|
|
492
|
+
});
|
|
493
|
+
break;
|
|
494
|
+
case 'test-finished':
|
|
495
|
+
scheduleHook('test:finished', {
|
|
496
|
+
runId,
|
|
497
|
+
file: event.file,
|
|
498
|
+
suite: event.suite,
|
|
499
|
+
name: event.name,
|
|
500
|
+
duration: event.duration,
|
|
501
|
+
status: event.status,
|
|
502
|
+
error: event.error,
|
|
503
|
+
});
|
|
504
|
+
break;
|
|
505
|
+
case 'module-bundling-started':
|
|
506
|
+
scheduleHook('metro:bundle-started', {
|
|
507
|
+
runId,
|
|
508
|
+
target: 'module',
|
|
509
|
+
file: event.file,
|
|
510
|
+
});
|
|
511
|
+
break;
|
|
512
|
+
case 'module-bundling-finished':
|
|
513
|
+
scheduleHook('metro:bundle-finished', {
|
|
514
|
+
runId,
|
|
515
|
+
target: 'module',
|
|
516
|
+
file: event.file,
|
|
517
|
+
duration: event.duration,
|
|
518
|
+
});
|
|
519
|
+
break;
|
|
520
|
+
case 'module-bundling-failed':
|
|
521
|
+
scheduleHook('metro:bundle-failed', {
|
|
522
|
+
runId,
|
|
523
|
+
target: 'module',
|
|
524
|
+
file: event.file,
|
|
525
|
+
duration: event.duration,
|
|
526
|
+
error: event.error,
|
|
527
|
+
});
|
|
528
|
+
break;
|
|
529
|
+
case 'setup-file-bundling-started':
|
|
530
|
+
scheduleHook('metro:bundle-started', {
|
|
531
|
+
runId,
|
|
532
|
+
target: 'setupFile',
|
|
533
|
+
file: event.file,
|
|
534
|
+
setupType: event.setupType,
|
|
535
|
+
});
|
|
536
|
+
break;
|
|
537
|
+
case 'setup-file-bundling-finished':
|
|
538
|
+
scheduleHook('metro:bundle-finished', {
|
|
539
|
+
runId,
|
|
540
|
+
target: 'setupFile',
|
|
541
|
+
file: event.file,
|
|
542
|
+
setupType: event.setupType,
|
|
543
|
+
duration: event.duration,
|
|
544
|
+
});
|
|
545
|
+
break;
|
|
546
|
+
case 'setup-file-bundling-failed':
|
|
547
|
+
scheduleHook('metro:bundle-failed', {
|
|
548
|
+
runId,
|
|
549
|
+
target: 'setupFile',
|
|
550
|
+
file: event.file,
|
|
551
|
+
setupType: event.setupType,
|
|
552
|
+
duration: event.duration,
|
|
553
|
+
error: event.error,
|
|
554
|
+
});
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const onMetroEvent = (event: ReportableEvent) => {
|
|
559
|
+
const runId = getCurrentRunId();
|
|
560
|
+
|
|
561
|
+
if (runId && event.type === 'client_log') {
|
|
562
|
+
scheduleHook('metro:client-log', {
|
|
563
|
+
runId,
|
|
564
|
+
level: event.level,
|
|
565
|
+
data: event.data,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
const crashSupervisor = createCrashSupervisor({
|
|
570
|
+
appMonitor,
|
|
571
|
+
platformRunner: platformInstance,
|
|
463
572
|
});
|
|
464
|
-
};
|
|
465
|
-
const onAppMonitorEvent = (event: AppMonitorEvent) => {
|
|
466
|
-
const runId = getCurrentRunId();
|
|
467
|
-
if (!runId) {
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
573
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
runId,
|
|
474
|
-
testFile: activeTestFilePath,
|
|
475
|
-
pid: event.pid,
|
|
476
|
-
source: event.source,
|
|
477
|
-
line: event.line,
|
|
478
|
-
});
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
574
|
+
const onReady = (device: DeviceDescriptor) => {
|
|
575
|
+
crashSupervisor.markReady();
|
|
481
576
|
|
|
482
|
-
|
|
483
|
-
|
|
577
|
+
const runId = getCurrentRunId();
|
|
578
|
+
if (!runId) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
scheduleHook('runtime:ready', {
|
|
484
583
|
runId,
|
|
485
|
-
|
|
486
|
-
pid: event.pid,
|
|
487
|
-
source: event.source,
|
|
488
|
-
line: event.line,
|
|
489
|
-
isConfirmed: event.isConfirmed,
|
|
490
|
-
crashDetails: event.crashDetails,
|
|
584
|
+
device,
|
|
491
585
|
});
|
|
492
|
-
|
|
493
|
-
|
|
586
|
+
};
|
|
587
|
+
const onDisconnect = () => {
|
|
588
|
+
const runId = getCurrentRunId();
|
|
589
|
+
if (!runId) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
494
592
|
|
|
495
|
-
|
|
496
|
-
scheduleHook('app:possible-crash', {
|
|
593
|
+
scheduleHook('runtime:disconnected', {
|
|
497
594
|
runId,
|
|
498
|
-
|
|
499
|
-
pid: event.pid,
|
|
500
|
-
source: event.source,
|
|
501
|
-
line: event.line,
|
|
502
|
-
isConfirmed: event.isConfirmed,
|
|
503
|
-
crashDetails: event.crashDetails,
|
|
595
|
+
reason: 'bridge-disconnected',
|
|
504
596
|
});
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
metroInstance.events.addListener(onMetroEvent);
|
|
512
|
-
appMonitor.addListener(onAppMonitorEvent);
|
|
513
|
-
harnessLogger.debug('registered runtime, bridge, and Metro listeners');
|
|
597
|
+
};
|
|
598
|
+
const onAppMonitorEvent = (event: AppMonitorEvent) => {
|
|
599
|
+
const runId = getCurrentRunId();
|
|
600
|
+
if (!runId) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
514
603
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
604
|
+
if (event.type === 'app_started') {
|
|
605
|
+
scheduleHook('app:started', {
|
|
606
|
+
runId,
|
|
607
|
+
testFile: activeTestFilePath,
|
|
608
|
+
pid: event.pid,
|
|
609
|
+
source: event.source,
|
|
610
|
+
line: event.line,
|
|
611
|
+
});
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
519
614
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
615
|
+
if (event.type === 'app_exited') {
|
|
616
|
+
scheduleHook('app:exited', {
|
|
617
|
+
runId,
|
|
618
|
+
testFile: activeTestFilePath,
|
|
619
|
+
pid: event.pid,
|
|
620
|
+
source: event.source,
|
|
621
|
+
line: event.line,
|
|
622
|
+
isConfirmed: event.isConfirmed,
|
|
623
|
+
crashDetails: event.crashDetails,
|
|
624
|
+
});
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
523
627
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
628
|
+
if (event.type === 'possible_crash') {
|
|
629
|
+
scheduleHook('app:possible-crash', {
|
|
630
|
+
runId,
|
|
631
|
+
testFile: activeTestFilePath,
|
|
632
|
+
pid: event.pid,
|
|
633
|
+
source: event.source,
|
|
634
|
+
line: event.line,
|
|
635
|
+
isConfirmed: event.isConfirmed,
|
|
636
|
+
crashDetails: event.crashDetails,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
serverBridge.on('ready', onReady);
|
|
642
|
+
serverBridge.on('disconnect', onDisconnect);
|
|
643
|
+
serverBridge.on('event', bridgeEventListener);
|
|
644
|
+
metroInstance.events.addListener(onMetroEvent);
|
|
645
|
+
appMonitor.addListener(onAppMonitorEvent);
|
|
646
|
+
harnessLogger.debug('registered runtime, bridge, and Metro listeners');
|
|
647
|
+
|
|
648
|
+
if (runtimeConfig.forwardClientLogs) {
|
|
649
|
+
metroInstance.events.addListener(clientLogListener);
|
|
650
|
+
harnessLogger.debug('client log forwarding enabled');
|
|
536
651
|
}
|
|
537
652
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
metroInstance.events.removeListener(onMetroEvent);
|
|
542
|
-
appMonitor.removeListener(onAppMonitorEvent);
|
|
543
|
-
serverBridge.off('ready', onReady);
|
|
544
|
-
serverBridge.off('disconnect', onDisconnect);
|
|
545
|
-
serverBridge.off('event', bridgeEventListener);
|
|
546
|
-
await Promise.all([
|
|
547
|
-
crashSupervisor.dispose(),
|
|
548
|
-
serverBridge.dispose(),
|
|
549
|
-
platformInstance.dispose(),
|
|
550
|
-
metroInstance.dispose(),
|
|
551
|
-
]);
|
|
552
|
-
pluginAbortController.abort();
|
|
553
|
-
harnessLogger.debug('Harness resources disposed');
|
|
554
|
-
|
|
555
|
-
if (hookError) {
|
|
556
|
-
throw hookError;
|
|
557
|
-
}
|
|
558
|
-
};
|
|
653
|
+
const dispose = async (reason: 'normal' | 'abort' | 'error' = 'normal') => {
|
|
654
|
+
harnessLogger.debug('disposing Harness (reason=%s)', reason);
|
|
655
|
+
let hookError: unknown;
|
|
559
656
|
|
|
560
|
-
|
|
561
|
-
|
|
657
|
+
try {
|
|
658
|
+
await flushPendingHooks();
|
|
659
|
+
await pluginManager.callHook('harness:after-run', {
|
|
660
|
+
runId: currentRun?.runId,
|
|
661
|
+
reason,
|
|
662
|
+
summary: currentRun?.summary,
|
|
663
|
+
status: currentRun?.status,
|
|
664
|
+
error: currentRun?.error,
|
|
665
|
+
});
|
|
666
|
+
await flushPendingHooks();
|
|
667
|
+
await pluginManager.callHook('harness:before-dispose', {
|
|
668
|
+
runId: currentRun?.runId,
|
|
669
|
+
reason,
|
|
670
|
+
summary: currentRun?.summary,
|
|
671
|
+
status: currentRun?.status,
|
|
672
|
+
error: currentRun?.error,
|
|
673
|
+
});
|
|
674
|
+
await flushPendingHooks();
|
|
675
|
+
} catch (error) {
|
|
676
|
+
hookError = error;
|
|
677
|
+
}
|
|
562
678
|
|
|
563
|
-
|
|
564
|
-
|
|
679
|
+
if (runtimeConfig.forwardClientLogs) {
|
|
680
|
+
metroInstance.events.removeListener(clientLogListener);
|
|
681
|
+
}
|
|
682
|
+
metroInstance.events.removeListener(onMetroEvent);
|
|
683
|
+
appMonitor.removeListener(onAppMonitorEvent);
|
|
684
|
+
serverBridge.off('ready', onReady);
|
|
685
|
+
serverBridge.off('disconnect', onDisconnect);
|
|
686
|
+
serverBridge.off('event', bridgeEventListener);
|
|
687
|
+
let cleanupError: unknown;
|
|
688
|
+
try {
|
|
689
|
+
await Promise.all([
|
|
690
|
+
crashSupervisor.dispose(),
|
|
691
|
+
serverBridge.dispose(),
|
|
692
|
+
platformInstance.dispose(),
|
|
693
|
+
metroInstance.dispose(),
|
|
694
|
+
metroPortLease?.release(),
|
|
695
|
+
]);
|
|
696
|
+
} catch (error) {
|
|
697
|
+
cleanupError = error;
|
|
698
|
+
} finally {
|
|
699
|
+
await resourceLease.release();
|
|
700
|
+
pluginAbortController.abort();
|
|
701
|
+
}
|
|
702
|
+
harnessLogger.debug('Harness resources disposed');
|
|
565
703
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
});
|
|
570
|
-
await appMonitor.start();
|
|
571
|
-
harnessLogger.debug('app monitor started');
|
|
572
|
-
} catch (error) {
|
|
573
|
-
const runState = currentRun as HarnessRunState | null;
|
|
704
|
+
if (hookError) {
|
|
705
|
+
throw hookError;
|
|
706
|
+
}
|
|
574
707
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
await dispose(error instanceof DOMException && error.name === 'AbortError' ? 'abort' : 'error');
|
|
580
|
-
throw error;
|
|
581
|
-
}
|
|
708
|
+
if (cleanupError) {
|
|
709
|
+
throw cleanupError;
|
|
710
|
+
}
|
|
711
|
+
};
|
|
582
712
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
setActiveTestFilePath(testFilePath);
|
|
586
|
-
crashSupervisor.setActiveTestFile(testFilePath);
|
|
587
|
-
harnessLogger.debug('ensuring app is ready for %s', testFilePath);
|
|
713
|
+
if (signal.aborted) {
|
|
714
|
+
await dispose('abort');
|
|
588
715
|
|
|
589
|
-
|
|
590
|
-
harnessLogger.debug('reusing existing ready app for %s', testFilePath);
|
|
591
|
-
return;
|
|
716
|
+
throw new DOMException('The operation was aborted', 'AbortError');
|
|
592
717
|
}
|
|
593
718
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
});
|
|
608
|
-
await flushPendingHooks();
|
|
609
|
-
harnessLogger.debug('app is ready for %s', testFilePath);
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
const restart = async (testFilePath?: string) => {
|
|
613
|
-
await flushPendingHooks();
|
|
614
|
-
await crashSupervisor.stop();
|
|
615
|
-
setActiveTestFilePath(testFilePath);
|
|
616
|
-
harnessLogger.debug(
|
|
617
|
-
'restarting app (testFile=%s mode=%s)',
|
|
618
|
-
testFilePath ?? 'n/a',
|
|
619
|
-
testFilePath ? 'stop-and-ensure-ready' : 'direct-restart'
|
|
620
|
-
);
|
|
621
|
-
|
|
622
|
-
if (testFilePath) {
|
|
623
|
-
harnessLogger.debug('stopping app before restart');
|
|
624
|
-
await platformInstance.stopApp();
|
|
625
|
-
} else {
|
|
626
|
-
harnessLogger.debug('requesting direct app restart');
|
|
627
|
-
await platformInstance.restartApp(appLaunchOptions);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
crashSupervisor.reset();
|
|
631
|
-
await crashSupervisor.start();
|
|
719
|
+
try {
|
|
720
|
+
await pluginManager.callHook('harness:before-creation', {
|
|
721
|
+
appLaunchOptions,
|
|
722
|
+
});
|
|
723
|
+
await flushPendingHooks();
|
|
724
|
+
await appMonitor.start();
|
|
725
|
+
harnessLogger.debug('app monitor started');
|
|
726
|
+
await pluginManager.callHook('harness:before-run', {
|
|
727
|
+
appLaunchOptions,
|
|
728
|
+
});
|
|
729
|
+
await flushPendingHooks();
|
|
730
|
+
} catch (error) {
|
|
731
|
+
const runState = currentRun as HarnessRunState | null;
|
|
632
732
|
|
|
633
|
-
|
|
634
|
-
|
|
733
|
+
if (runState) {
|
|
734
|
+
runState.error = error;
|
|
735
|
+
currentRun = runState;
|
|
736
|
+
}
|
|
737
|
+
await dispose(
|
|
738
|
+
error instanceof DOMException && error.name === 'AbortError'
|
|
739
|
+
? 'abort'
|
|
740
|
+
: 'error'
|
|
741
|
+
);
|
|
742
|
+
throw error;
|
|
635
743
|
}
|
|
636
744
|
|
|
637
|
-
|
|
638
|
-
harnessLogger.debug('restart completed');
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
return {
|
|
642
|
-
context,
|
|
643
|
-
runTests: async (path, options) => {
|
|
745
|
+
const ensureAppReady = async (testFilePath: string) => {
|
|
644
746
|
await flushPendingHooks();
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
747
|
+
setActiveTestFilePath(testFilePath);
|
|
748
|
+
crashSupervisor.setActiveTestFile(testFilePath);
|
|
749
|
+
harnessLogger.debug('ensuring app is ready for %s', testFilePath);
|
|
750
|
+
|
|
751
|
+
if (
|
|
752
|
+
crashSupervisor.isReady() &&
|
|
753
|
+
(await platformInstance.isAppRunning())
|
|
754
|
+
) {
|
|
755
|
+
harnessLogger.debug('reusing existing ready app for %s', testFilePath);
|
|
756
|
+
return;
|
|
650
757
|
}
|
|
651
758
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
759
|
+
crashSupervisor.reset();
|
|
760
|
+
harnessLogger.debug(
|
|
761
|
+
'app not ready, waiting for launch and runtime readiness'
|
|
762
|
+
);
|
|
763
|
+
await waitForAppReady({
|
|
764
|
+
metroInstance,
|
|
765
|
+
serverBridge,
|
|
766
|
+
platformInstance: platformInstance as HarnessPlatformRunner,
|
|
767
|
+
platformId: platform.platformId,
|
|
768
|
+
bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
|
|
769
|
+
readyTimeout: runtimeConfig.bridgeTimeout,
|
|
770
|
+
maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
|
|
771
|
+
testFilePath,
|
|
772
|
+
crashSupervisor,
|
|
773
|
+
appLaunchOptions,
|
|
656
774
|
});
|
|
657
775
|
await flushPendingHooks();
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
restart
|
|
662
|
-
dispose: () => dispose('normal'),
|
|
663
|
-
crashSupervisor,
|
|
664
|
-
callHook: async (name, payload) => {
|
|
776
|
+
harnessLogger.debug('app is ready for %s', testFilePath);
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const restart = async (testFilePath?: string) => {
|
|
665
780
|
await flushPendingHooks();
|
|
666
|
-
await
|
|
781
|
+
await crashSupervisor.stop();
|
|
782
|
+
setActiveTestFilePath(testFilePath);
|
|
783
|
+
harnessLogger.debug(
|
|
784
|
+
'restarting app (testFile=%s mode=%s)',
|
|
785
|
+
testFilePath ?? 'n/a',
|
|
786
|
+
testFilePath ? 'stop-and-ensure-ready' : 'direct-restart'
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
if (testFilePath) {
|
|
790
|
+
harnessLogger.debug('stopping app before restart');
|
|
791
|
+
await platformInstance.stopApp();
|
|
792
|
+
} else {
|
|
793
|
+
harnessLogger.debug('requesting direct app restart');
|
|
794
|
+
await platformInstance.restartApp(appLaunchOptions);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
crashSupervisor.reset();
|
|
798
|
+
await crashSupervisor.start();
|
|
799
|
+
|
|
800
|
+
if (testFilePath) {
|
|
801
|
+
await ensureAppReady(testFilePath);
|
|
802
|
+
}
|
|
803
|
+
|
|
667
804
|
await flushPendingHooks();
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
805
|
+
harnessLogger.debug('restart completed');
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
return {
|
|
809
|
+
context,
|
|
810
|
+
config: runtimeConfig,
|
|
811
|
+
runTests: async (path, options) => {
|
|
812
|
+
await flushPendingHooks();
|
|
813
|
+
activeTestFilePath = path;
|
|
814
|
+
const client = serverBridge.rpc.clients.at(-1);
|
|
815
|
+
|
|
816
|
+
if (!client) {
|
|
817
|
+
throw new Error('No client found');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
harnessLogger.debug('running test file on client: %s', path);
|
|
821
|
+
const result = await client.runTests(path, {
|
|
822
|
+
...options,
|
|
823
|
+
runner: platform.runner,
|
|
824
|
+
});
|
|
825
|
+
await flushPendingHooks();
|
|
826
|
+
return result;
|
|
827
|
+
},
|
|
828
|
+
ensureAppReady,
|
|
829
|
+
restart,
|
|
830
|
+
dispose: () => dispose('normal'),
|
|
831
|
+
crashSupervisor,
|
|
832
|
+
callHook: async (name, payload) => {
|
|
833
|
+
await flushPendingHooks();
|
|
834
|
+
await pluginManager.callHook(name, payload);
|
|
835
|
+
await flushPendingHooks();
|
|
836
|
+
},
|
|
837
|
+
setRunState: (runState) => {
|
|
838
|
+
currentRun = runState;
|
|
839
|
+
},
|
|
840
|
+
getRunState: () => currentRun,
|
|
841
|
+
};
|
|
842
|
+
} catch (error) {
|
|
843
|
+
await resourceLease.release();
|
|
844
|
+
throw error;
|
|
845
|
+
}
|
|
674
846
|
};
|
|
675
847
|
|
|
676
848
|
export const getHarness = async (
|
|
@@ -678,25 +850,15 @@ export const getHarness = async (
|
|
|
678
850
|
platform: HarnessPlatform,
|
|
679
851
|
projectRoot: string
|
|
680
852
|
): Promise<Harness> => {
|
|
681
|
-
const abortSignal = AbortSignal.timeout(config.bridgeTimeout);
|
|
682
853
|
harnessLogger.debug(
|
|
683
|
-
'creating Harness with
|
|
684
|
-
config.
|
|
854
|
+
'creating Harness with platform ready timeout %dms',
|
|
855
|
+
config.platformReadyTimeout
|
|
685
856
|
);
|
|
686
857
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
);
|
|
694
|
-
return harness;
|
|
695
|
-
} catch (error) {
|
|
696
|
-
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
697
|
-
throw new InitializationTimeoutError();
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
throw error;
|
|
701
|
-
}
|
|
858
|
+
return await getHarnessInternal(
|
|
859
|
+
config,
|
|
860
|
+
platform,
|
|
861
|
+
projectRoot,
|
|
862
|
+
new AbortController().signal
|
|
863
|
+
);
|
|
702
864
|
};
|