@react-native-harness/jest 1.0.0 → 1.1.0-rc.1
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__/crash-supervisor.test.d.ts +2 -0
- package/dist/__tests__/crash-supervisor.test.d.ts.map +1 -0
- package/dist/__tests__/crash-supervisor.test.js +151 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.js +54 -0
- package/dist/__tests__/harness.test.d.ts +2 -0
- package/dist/__tests__/harness.test.d.ts.map +1 -0
- package/dist/__tests__/harness.test.js +106 -0
- package/dist/crash-supervisor.d.ts +21 -0
- package/dist/crash-supervisor.d.ts.map +1 -0
- package/dist/crash-supervisor.js +209 -0
- package/dist/errors.d.ts +8 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +42 -8
- package/dist/harness.d.ts +9 -9
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +70 -74
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +0 -4
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/__tests__/crash-supervisor.test.ts +200 -0
- package/src/__tests__/errors.test.ts +70 -0
- package/src/__tests__/harness.test.ts +129 -0
- package/src/crash-supervisor.ts +314 -0
- package/src/errors.ts +63 -8
- package/src/harness.ts +93 -93
- package/src/index.ts +13 -15
- package/src/setup.ts +0 -5
- package/src/crash-monitor.ts +0 -109
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AppMonitor,
|
|
3
|
+
type AppCrashDetails,
|
|
4
|
+
type AppMonitorEvent,
|
|
5
|
+
type AppMonitorListener,
|
|
6
|
+
type HarnessPlatformRunner,
|
|
7
|
+
} from '@react-native-harness/platforms';
|
|
8
|
+
import {
|
|
9
|
+
NativeCrashError,
|
|
10
|
+
type NativeCrashDetails,
|
|
11
|
+
type NativeCrashPhase,
|
|
12
|
+
} from './errors.js';
|
|
13
|
+
import { logger } from '@react-native-harness/tools';
|
|
14
|
+
|
|
15
|
+
export type CrashSupervisorState =
|
|
16
|
+
| 'idle'
|
|
17
|
+
| 'launching'
|
|
18
|
+
| 'ready'
|
|
19
|
+
| 'running'
|
|
20
|
+
| 'disposing';
|
|
21
|
+
|
|
22
|
+
export type CrashSupervisor = {
|
|
23
|
+
setActiveTestFile: (testFilePath: string | null) => void;
|
|
24
|
+
beginLaunch: (testFilePath: string) => void;
|
|
25
|
+
markReady: () => void;
|
|
26
|
+
beginTestRun: (testFilePath: string) => void;
|
|
27
|
+
stop: () => Promise<void>;
|
|
28
|
+
start: () => Promise<void>;
|
|
29
|
+
waitForCrash: (testFilePath: string) => Promise<never>;
|
|
30
|
+
isReady: () => boolean;
|
|
31
|
+
cancelCrashWaiters: () => void;
|
|
32
|
+
reset: () => void;
|
|
33
|
+
dispose: () => Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type CrashSupervisorOptions = {
|
|
37
|
+
appMonitor: AppMonitor;
|
|
38
|
+
platformRunner: HarnessPlatformRunner;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type CrashDetailsProvider = {
|
|
42
|
+
getCrashDetails?: (
|
|
43
|
+
options: {
|
|
44
|
+
processName?: string;
|
|
45
|
+
pid?: number;
|
|
46
|
+
occurredAt: number;
|
|
47
|
+
}
|
|
48
|
+
) => Promise<AppCrashDetails | null>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getCrashPhase = (state: CrashSupervisorState): NativeCrashPhase =>
|
|
52
|
+
state === 'running' ? 'execution' : 'startup';
|
|
53
|
+
|
|
54
|
+
const mergeCrashDetails = (
|
|
55
|
+
phase: NativeCrashPhase,
|
|
56
|
+
initialDetails?: AppCrashDetails,
|
|
57
|
+
enrichedDetails?: AppCrashDetails | null,
|
|
58
|
+
fallbackSummary?: string
|
|
59
|
+
): NativeCrashDetails => ({
|
|
60
|
+
phase,
|
|
61
|
+
source: enrichedDetails?.source ?? initialDetails?.source,
|
|
62
|
+
summary:
|
|
63
|
+
enrichedDetails?.summary ?? initialDetails?.summary ?? fallbackSummary,
|
|
64
|
+
signal: enrichedDetails?.signal ?? initialDetails?.signal,
|
|
65
|
+
exceptionType: enrichedDetails?.exceptionType ?? initialDetails?.exceptionType,
|
|
66
|
+
processName: enrichedDetails?.processName ?? initialDetails?.processName,
|
|
67
|
+
pid: enrichedDetails?.pid ?? initialDetails?.pid,
|
|
68
|
+
stackTrace: enrichedDetails?.stackTrace ?? initialDetails?.stackTrace,
|
|
69
|
+
rawLines: enrichedDetails?.rawLines ?? initialDetails?.rawLines,
|
|
70
|
+
artifactType: enrichedDetails?.artifactType ?? initialDetails?.artifactType,
|
|
71
|
+
artifactPath: enrichedDetails?.artifactPath ?? initialDetails?.artifactPath,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const createCrashSupervisor = ({
|
|
75
|
+
appMonitor,
|
|
76
|
+
platformRunner,
|
|
77
|
+
}: CrashSupervisorOptions): CrashSupervisor => {
|
|
78
|
+
let state: CrashSupervisorState = 'idle';
|
|
79
|
+
let activeTestFilePath: string | null = null;
|
|
80
|
+
let crashRejectors = new Set<(error: NativeCrashError) => void>();
|
|
81
|
+
let disposed = false;
|
|
82
|
+
let monitoring = true;
|
|
83
|
+
let isResolvingCrash = false;
|
|
84
|
+
|
|
85
|
+
const getCrashDetailsProvider = (): CrashDetailsProvider | null => {
|
|
86
|
+
if ('getCrashDetails' in appMonitor) {
|
|
87
|
+
return appMonitor as AppMonitor & CrashDetailsProvider;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (platformRunner.getCrashDetails) {
|
|
91
|
+
return platformRunner;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const rejectCrashWaiters = (testFilePath: string, details: NativeCrashDetails) => {
|
|
98
|
+
const error = new NativeCrashError(testFilePath, details);
|
|
99
|
+
|
|
100
|
+
for (const reject of crashRejectors) {
|
|
101
|
+
reject(error);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
crashRejectors = new Set();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleCrash = async (reason: string, details?: AppCrashDetails) => {
|
|
108
|
+
if (
|
|
109
|
+
isResolvingCrash ||
|
|
110
|
+
(state !== 'launching' && state !== 'running' && state !== 'ready')
|
|
111
|
+
) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!activeTestFilePath) {
|
|
116
|
+
logger.debug(`Ignoring crash signal without active test: ${reason}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
isResolvingCrash = true;
|
|
121
|
+
logger.debug(`Native crash detected during ${activeTestFilePath} (state: ${state}, reason: ${reason || '(none)'})`);
|
|
122
|
+
|
|
123
|
+
for (const line of details?.rawLines ?? []) {
|
|
124
|
+
logger.debug(line);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const phase = getCrashPhase(state);
|
|
128
|
+
const testFilePath = activeTestFilePath;
|
|
129
|
+
state = 'idle';
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const enrichedDetails = await getCrashDetailsProvider()?.getCrashDetails?.({
|
|
133
|
+
processName: details?.processName,
|
|
134
|
+
pid: details?.pid,
|
|
135
|
+
occurredAt: Date.now(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const mergedDetails = mergeCrashDetails(phase, details, enrichedDetails, reason);
|
|
139
|
+
logger.debug('Crash details:', {
|
|
140
|
+
phase: mergedDetails.phase,
|
|
141
|
+
source: mergedDetails.source,
|
|
142
|
+
summary: mergedDetails.summary,
|
|
143
|
+
signal: mergedDetails.signal,
|
|
144
|
+
exceptionType: mergedDetails.exceptionType,
|
|
145
|
+
processName: mergedDetails.processName,
|
|
146
|
+
pid: mergedDetails.pid,
|
|
147
|
+
});
|
|
148
|
+
rejectCrashWaiters(testFilePath, mergedDetails);
|
|
149
|
+
} finally {
|
|
150
|
+
isResolvingCrash = false;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const confirmCrash = async (reason: string, details?: AppCrashDetails) => {
|
|
155
|
+
if (disposed || !monitoring || state === 'disposing') {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const isRunning = await platformRunner.isAppRunning();
|
|
161
|
+
|
|
162
|
+
if (!isRunning) {
|
|
163
|
+
handleCrash(reason, details);
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
logger.debug('Crash confirmation failed', error);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const appMonitorListener: AppMonitorListener = (event: AppMonitorEvent) => {
|
|
171
|
+
if (disposed || !monitoring) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (event.type === 'app_started') {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (event.type === 'app_exited') {
|
|
180
|
+
const details = {
|
|
181
|
+
source: event.crashDetails?.source ?? event.source,
|
|
182
|
+
summary: event.crashDetails?.summary,
|
|
183
|
+
signal: event.crashDetails?.signal,
|
|
184
|
+
exceptionType: event.crashDetails?.exceptionType,
|
|
185
|
+
processName: event.crashDetails?.processName,
|
|
186
|
+
pid: event.crashDetails?.pid ?? event.pid,
|
|
187
|
+
stackTrace: event.crashDetails?.stackTrace,
|
|
188
|
+
rawLines:
|
|
189
|
+
event.crashDetails?.rawLines ??
|
|
190
|
+
(event.line ? [event.line] : undefined),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (event.isConfirmed ?? event.source === 'polling') {
|
|
194
|
+
void handleCrash('', details);
|
|
195
|
+
} else {
|
|
196
|
+
void confirmCrash('', details);
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (event.type === 'possible_crash') {
|
|
202
|
+
const details = {
|
|
203
|
+
source: event.crashDetails?.source ?? event.source,
|
|
204
|
+
summary: event.crashDetails?.summary,
|
|
205
|
+
signal: event.crashDetails?.signal,
|
|
206
|
+
exceptionType: event.crashDetails?.exceptionType,
|
|
207
|
+
processName: event.crashDetails?.processName,
|
|
208
|
+
pid: event.crashDetails?.pid ?? event.pid,
|
|
209
|
+
stackTrace: event.crashDetails?.stackTrace,
|
|
210
|
+
rawLines:
|
|
211
|
+
event.crashDetails?.rawLines ??
|
|
212
|
+
(event.line ? [event.line] : undefined),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (event.isConfirmed) {
|
|
216
|
+
void handleCrash(
|
|
217
|
+
`possible crash signal (${event.source ?? 'unknown'})`,
|
|
218
|
+
details
|
|
219
|
+
);
|
|
220
|
+
} else {
|
|
221
|
+
void confirmCrash(
|
|
222
|
+
`possible crash signal (${event.source ?? 'unknown'})`,
|
|
223
|
+
details
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
appMonitor.addListener(appMonitorListener);
|
|
230
|
+
|
|
231
|
+
const setActiveTestFile = (testFilePath: string | null) => {
|
|
232
|
+
activeTestFilePath = testFilePath;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const beginLaunch = (testFilePath: string) => {
|
|
236
|
+
activeTestFilePath = testFilePath;
|
|
237
|
+
state = 'launching';
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const markReady = () => {
|
|
241
|
+
if (state !== 'disposing') {
|
|
242
|
+
state = 'ready';
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const beginTestRun = (testFilePath: string) => {
|
|
247
|
+
activeTestFilePath = testFilePath;
|
|
248
|
+
state = 'running';
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const stop = async () => {
|
|
252
|
+
monitoring = false;
|
|
253
|
+
await appMonitor.stop();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const start = async () => {
|
|
257
|
+
monitoring = true;
|
|
258
|
+
await appMonitor.start();
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const waitForCrash = (testFilePath: string): Promise<never> =>
|
|
262
|
+
new Promise<never>((_, reject) => {
|
|
263
|
+
if (disposed) {
|
|
264
|
+
reject(
|
|
265
|
+
new NativeCrashError(testFilePath, {
|
|
266
|
+
phase: 'startup',
|
|
267
|
+
source: 'polling',
|
|
268
|
+
summary: 'Crash supervisor disposed while waiting for app startup.',
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
crashRejectors.add(reject);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const isReady = () => state === 'ready' || state === 'running';
|
|
278
|
+
|
|
279
|
+
const cancelCrashWaiters = () => {
|
|
280
|
+
crashRejectors = new Set();
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const reset = () => {
|
|
284
|
+
cancelCrashWaiters();
|
|
285
|
+
isResolvingCrash = false;
|
|
286
|
+
if (state !== 'disposing') {
|
|
287
|
+
state = 'idle';
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const dispose = async () => {
|
|
292
|
+
disposed = true;
|
|
293
|
+
monitoring = false;
|
|
294
|
+
state = 'disposing';
|
|
295
|
+
crashRejectors = new Set();
|
|
296
|
+
isResolvingCrash = false;
|
|
297
|
+
appMonitor.removeListener(appMonitorListener);
|
|
298
|
+
await appMonitor.dispose();
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
setActiveTestFile,
|
|
303
|
+
beginLaunch,
|
|
304
|
+
markReady,
|
|
305
|
+
beginTestRun,
|
|
306
|
+
stop,
|
|
307
|
+
start,
|
|
308
|
+
waitForCrash,
|
|
309
|
+
isReady,
|
|
310
|
+
cancelCrashWaiters,
|
|
311
|
+
reset,
|
|
312
|
+
dispose,
|
|
313
|
+
};
|
|
314
|
+
};
|
package/src/errors.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HarnessError } from '@react-native-harness/tools';
|
|
2
|
+
import type { AppCrashDetails } from '@react-native-harness/platforms';
|
|
2
3
|
|
|
3
4
|
export class NoRunnerSpecifiedError extends HarnessError {
|
|
4
5
|
constructor() {
|
|
@@ -21,22 +22,76 @@ export class InitializationTimeoutError extends HarnessError {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
export type NativeCrashPhase = 'startup' | 'execution';
|
|
26
|
+
|
|
27
|
+
export type NativeCrashDetails = AppCrashDetails & {
|
|
28
|
+
phase: NativeCrashPhase;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const buildNativeCrashMessage = ({
|
|
32
|
+
phase,
|
|
33
|
+
summary,
|
|
34
|
+
signal,
|
|
35
|
+
exceptionType,
|
|
36
|
+
processName,
|
|
37
|
+
pid,
|
|
38
|
+
stackTrace,
|
|
39
|
+
artifactType,
|
|
40
|
+
}: NativeCrashDetails) => {
|
|
41
|
+
const lines = [
|
|
42
|
+
phase === 'startup'
|
|
43
|
+
? 'The native app crashed while preparing to run this test file.'
|
|
44
|
+
: 'The native app crashed during test execution.',
|
|
45
|
+
];
|
|
46
|
+
const hasCrashBlock = summary?.includes('\n') ?? false;
|
|
47
|
+
const shouldRenderSummary =
|
|
48
|
+
Boolean(summary) &&
|
|
49
|
+
!(
|
|
50
|
+
!hasCrashBlock &&
|
|
51
|
+
artifactType === 'ios-crash-report'
|
|
29
52
|
);
|
|
30
|
-
|
|
53
|
+
|
|
54
|
+
if (shouldRenderSummary && summary) {
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push(summary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!hasCrashBlock && signal) {
|
|
60
|
+
lines.push(`Signal: ${signal}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!hasCrashBlock && exceptionType) {
|
|
64
|
+
lines.push(`Exception: ${exceptionType}`);
|
|
31
65
|
}
|
|
32
|
-
|
|
66
|
+
|
|
67
|
+
if (!hasCrashBlock && processName && pid !== undefined) {
|
|
68
|
+
lines.push(`Process: ${processName} (pid ${pid})`);
|
|
69
|
+
} else if (!hasCrashBlock && processName) {
|
|
70
|
+
lines.push(`Process: ${processName}`);
|
|
71
|
+
} else if (!hasCrashBlock && pid !== undefined) {
|
|
72
|
+
lines.push(`PID: ${pid}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!hasCrashBlock && stackTrace && stackTrace.length > 0) {
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(...stackTrace.map((line) => ` ${line}`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
};
|
|
33
82
|
|
|
34
83
|
export class NativeCrashError extends HarnessError {
|
|
35
84
|
constructor(
|
|
36
85
|
public readonly testFilePath: string,
|
|
86
|
+
public readonly details: NativeCrashDetails,
|
|
37
87
|
public readonly lastKnownTest?: string
|
|
38
88
|
) {
|
|
39
|
-
super(
|
|
89
|
+
super(buildNativeCrashMessage(details));
|
|
40
90
|
this.name = 'NativeCrashError';
|
|
91
|
+
this.stack = `${this.name}: ${this.message.split('\n')[0]}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get phase() {
|
|
95
|
+
return this.details.phase;
|
|
41
96
|
}
|
|
42
97
|
}
|
package/src/harness.ts
CHANGED
|
@@ -8,18 +8,21 @@ import {
|
|
|
8
8
|
TestSuiteResult,
|
|
9
9
|
} from '@react-native-harness/bridge';
|
|
10
10
|
import {
|
|
11
|
+
type AppLaunchOptions,
|
|
11
12
|
HarnessPlatform,
|
|
12
13
|
HarnessPlatformRunner,
|
|
13
14
|
} from '@react-native-harness/platforms';
|
|
14
15
|
import {
|
|
15
16
|
getMetroInstance,
|
|
16
17
|
prewarmMetroBundle,
|
|
17
|
-
Reporter,
|
|
18
|
-
ReportableEvent,
|
|
19
18
|
} from '@react-native-harness/bundler-metro';
|
|
20
|
-
import {
|
|
19
|
+
import { createCrashArtifactWriter } from '@react-native-harness/tools';
|
|
20
|
+
import { InitializationTimeoutError } from './errors.js';
|
|
21
21
|
import { Config as HarnessConfig } from '@react-native-harness/config';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
createCrashSupervisor,
|
|
24
|
+
type CrashSupervisor,
|
|
25
|
+
} from './crash-supervisor.js';
|
|
23
26
|
import { createClientLogListener } from './client-log-handler.js';
|
|
24
27
|
import { logMetroPrewarmCompleted } from './logs.js';
|
|
25
28
|
|
|
@@ -31,97 +34,65 @@ export type Harness = {
|
|
|
31
34
|
path: string,
|
|
32
35
|
options: HarnessRunTestsOptions
|
|
33
36
|
) => Promise<TestSuiteResult>;
|
|
34
|
-
|
|
37
|
+
ensureAppReady: (testFilePath: string) => Promise<void>;
|
|
38
|
+
restart: (testFilePath?: string) => Promise<void>;
|
|
35
39
|
dispose: () => Promise<void>;
|
|
36
|
-
|
|
40
|
+
crashSupervisor: CrashSupervisor;
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
export const waitForAppReady = async (options: {
|
|
40
|
-
metroEvents: Reporter;
|
|
41
44
|
serverBridge: BridgeServer;
|
|
42
45
|
platformInstance: HarnessPlatformRunner;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
bridgeTimeout: number;
|
|
47
|
+
testFilePath: string;
|
|
48
|
+
crashSupervisor: CrashSupervisor;
|
|
49
|
+
appLaunchOptions?: AppLaunchOptions;
|
|
46
50
|
}): Promise<void> => {
|
|
47
51
|
const {
|
|
48
|
-
metroEvents,
|
|
49
52
|
serverBridge,
|
|
50
53
|
platformInstance,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
bridgeTimeout,
|
|
55
|
+
testFilePath,
|
|
56
|
+
crashSupervisor,
|
|
57
|
+
appLaunchOptions,
|
|
54
58
|
} = options;
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
let isBundling = false;
|
|
58
|
-
let bundleTimeoutId: NodeJS.Timeout | null = null;
|
|
59
|
-
|
|
60
|
-
const clearBundleTimeout = () => {
|
|
61
|
-
if (bundleTimeoutId) {
|
|
62
|
-
clearTimeout(bundleTimeoutId);
|
|
63
|
-
bundleTimeoutId = null;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
60
|
+
const signal = AbortSignal.timeout(bridgeTimeout);
|
|
66
61
|
|
|
67
62
|
return new Promise<void>((resolve, reject) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
reject(new DOMException('The operation was aborted', 'AbortError'));
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Start/restart the bundle timeout
|
|
75
|
-
const startBundleTimeout = () => {
|
|
76
|
-
clearBundleTimeout();
|
|
77
|
-
bundleTimeoutId = setTimeout(() => {
|
|
78
|
-
if (isBundling) return; // Don't restart while bundling
|
|
79
|
-
|
|
80
|
-
if (restartCount >= maxRestarts) {
|
|
81
|
-
cleanup();
|
|
82
|
-
reject(new MaxAppRestartsError(restartCount + 1));
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
restartCount++;
|
|
87
|
-
platformInstance.restartApp().catch(reject);
|
|
88
|
-
startBundleTimeout(); // Reset timer for next attempt
|
|
89
|
-
}, bundleStartTimeout);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Metro event listener
|
|
93
|
-
const onMetroEvent = (event: ReportableEvent) => {
|
|
94
|
-
if (event.type === 'bundle_build_started') {
|
|
95
|
-
isBundling = true;
|
|
96
|
-
clearBundleTimeout(); // Cancel restart timer while bundling
|
|
97
|
-
} else if (
|
|
98
|
-
event.type === 'bundle_build_done' ||
|
|
99
|
-
event.type === 'bundle_build_failed'
|
|
100
|
-
) {
|
|
101
|
-
isBundling = false;
|
|
102
|
-
startBundleTimeout(); // Reset timer after bundle completes
|
|
103
|
-
}
|
|
63
|
+
const launchApp = async () => {
|
|
64
|
+
crashSupervisor.beginLaunch(testFilePath);
|
|
65
|
+
await platformInstance.restartApp(appLaunchOptions);
|
|
104
66
|
};
|
|
105
67
|
|
|
106
|
-
// Bridge ready listener
|
|
107
68
|
const onReady = () => {
|
|
69
|
+
crashSupervisor.markReady();
|
|
108
70
|
cleanup();
|
|
109
71
|
resolve();
|
|
110
72
|
};
|
|
111
73
|
|
|
74
|
+
const onAbort = () => {
|
|
75
|
+
cleanup();
|
|
76
|
+
reject(new DOMException('The operation was aborted', 'AbortError'));
|
|
77
|
+
};
|
|
78
|
+
|
|
112
79
|
const cleanup = () => {
|
|
113
|
-
clearBundleTimeout();
|
|
114
|
-
metroEvents.removeListener(onMetroEvent);
|
|
115
80
|
serverBridge.off('ready', onReady);
|
|
81
|
+
signal.removeEventListener('abort', onAbort);
|
|
82
|
+
crashSupervisor.cancelCrashWaiters();
|
|
116
83
|
};
|
|
117
84
|
|
|
118
|
-
|
|
119
|
-
metroEvents.addListener(onMetroEvent);
|
|
85
|
+
signal.addEventListener('abort', onAbort);
|
|
120
86
|
serverBridge.once('ready', onReady);
|
|
87
|
+
void crashSupervisor.waitForCrash(testFilePath).catch((error) => {
|
|
88
|
+
cleanup();
|
|
89
|
+
reject(error);
|
|
90
|
+
});
|
|
121
91
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
92
|
+
void launchApp().catch((error) => {
|
|
93
|
+
cleanup();
|
|
94
|
+
reject(error);
|
|
95
|
+
});
|
|
125
96
|
});
|
|
126
97
|
};
|
|
127
98
|
|
|
@@ -146,9 +117,24 @@ const getHarnessInternal = async (
|
|
|
146
117
|
context,
|
|
147
118
|
}),
|
|
148
119
|
]);
|
|
120
|
+
const crashArtifactWriter = createCrashArtifactWriter({
|
|
121
|
+
runnerName: platform.name,
|
|
122
|
+
platformId: platform.platformId,
|
|
123
|
+
});
|
|
124
|
+
const appMonitor = platformInstance.createAppMonitor({
|
|
125
|
+
crashArtifactWriter,
|
|
126
|
+
});
|
|
127
|
+
const appLaunchOptions = (
|
|
128
|
+
platform.config as { appLaunchOptions?: AppLaunchOptions }
|
|
129
|
+
).appLaunchOptions;
|
|
149
130
|
|
|
150
|
-
// Forward client logs to console if enabled
|
|
151
131
|
const clientLogListener = createClientLogListener();
|
|
132
|
+
const crashSupervisor = createCrashSupervisor({
|
|
133
|
+
appMonitor,
|
|
134
|
+
platformRunner: platformInstance,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
serverBridge.on('ready', crashSupervisor.markReady);
|
|
152
138
|
|
|
153
139
|
if (config.forwardClientLogs) {
|
|
154
140
|
metroInstance.events.addListener(clientLogListener);
|
|
@@ -158,19 +144,15 @@ const getHarnessInternal = async (
|
|
|
158
144
|
if (config.forwardClientLogs) {
|
|
159
145
|
metroInstance.events.removeListener(clientLogListener);
|
|
160
146
|
}
|
|
147
|
+
serverBridge.off('ready', crashSupervisor.markReady);
|
|
161
148
|
await Promise.all([
|
|
149
|
+
crashSupervisor.dispose(),
|
|
162
150
|
serverBridge.dispose(),
|
|
163
151
|
platformInstance.dispose(),
|
|
164
152
|
metroInstance.dispose(),
|
|
165
153
|
]);
|
|
166
154
|
};
|
|
167
155
|
|
|
168
|
-
const crashMonitor = createCrashMonitor({
|
|
169
|
-
interval: config.crashDetectionInterval,
|
|
170
|
-
platformRunner: platformInstance,
|
|
171
|
-
bridgeServer: serverBridge,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
156
|
if (signal.aborted) {
|
|
175
157
|
await dispose();
|
|
176
158
|
|
|
@@ -187,29 +169,46 @@ const getHarnessInternal = async (
|
|
|
187
169
|
signal,
|
|
188
170
|
});
|
|
189
171
|
logMetroPrewarmCompleted(platform);
|
|
190
|
-
|
|
191
|
-
await waitForAppReady({
|
|
192
|
-
metroEvents: metroInstance.events,
|
|
193
|
-
serverBridge,
|
|
194
|
-
platformInstance: platformInstance as HarnessPlatformRunner,
|
|
195
|
-
bundleStartTimeout: config.bundleStartTimeout,
|
|
196
|
-
maxRestarts: config.maxAppRestarts,
|
|
197
|
-
signal,
|
|
198
|
-
});
|
|
172
|
+
await appMonitor.start();
|
|
199
173
|
} catch (error) {
|
|
200
174
|
await dispose();
|
|
201
175
|
throw error;
|
|
202
176
|
}
|
|
203
177
|
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
178
|
+
const ensureAppReady = async (testFilePath: string) => {
|
|
179
|
+
crashSupervisor.setActiveTestFile(testFilePath);
|
|
180
|
+
|
|
181
|
+
if (crashSupervisor.isReady() && (await platformInstance.isAppRunning())) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
crashSupervisor.reset();
|
|
186
|
+
await waitForAppReady({
|
|
187
|
+
serverBridge,
|
|
188
|
+
platformInstance: platformInstance as HarnessPlatformRunner,
|
|
189
|
+
bridgeTimeout: config.bridgeTimeout,
|
|
190
|
+
testFilePath,
|
|
191
|
+
crashSupervisor,
|
|
192
|
+
appLaunchOptions,
|
|
212
193
|
});
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const restart = async (testFilePath?: string) => {
|
|
197
|
+
await crashSupervisor.stop();
|
|
198
|
+
|
|
199
|
+
if (testFilePath) {
|
|
200
|
+
await platformInstance.stopApp();
|
|
201
|
+
} else {
|
|
202
|
+
await platformInstance.restartApp(appLaunchOptions);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
crashSupervisor.reset();
|
|
206
|
+
await crashSupervisor.start();
|
|
207
|
+
|
|
208
|
+
if (testFilePath) {
|
|
209
|
+
await ensureAppReady(testFilePath);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
213
212
|
|
|
214
213
|
return {
|
|
215
214
|
context,
|
|
@@ -225,9 +224,10 @@ const getHarnessInternal = async (
|
|
|
225
224
|
runner: platform.runner,
|
|
226
225
|
});
|
|
227
226
|
},
|
|
227
|
+
ensureAppReady,
|
|
228
228
|
restart,
|
|
229
229
|
dispose,
|
|
230
|
-
|
|
230
|
+
crashSupervisor,
|
|
231
231
|
};
|
|
232
232
|
};
|
|
233
233
|
|