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