@react-native-harness/jest 1.2.0 → 1.4.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__/bridge.test.js +23 -2
- package/dist/__tests__/crash-monitor.test.js +146 -192
- package/dist/__tests__/errors.test.js +39 -0
- package/dist/__tests__/execute-run.test.js +190 -26
- package/dist/__tests__/fixtures/mock-platform-runner.d.ts +7 -0
- package/dist/__tests__/fixtures/mock-platform-runner.d.ts.map +1 -0
- package/dist/__tests__/fixtures/mock-platform-runner.js +6 -0
- package/dist/__tests__/harness-session.test.d.ts +2 -0
- package/dist/__tests__/harness-session.test.d.ts.map +1 -0
- package/dist/__tests__/harness-session.test.js +74 -0
- package/dist/__tests__/index.test.js +59 -10
- package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
- package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
- package/dist/__tests__/test-file-platform-filter.test.js +47 -0
- package/dist/crash-monitor.d.ts +5 -4
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +116 -102
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -2
- package/dist/execute-run.d.ts +4 -2
- package/dist/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +85 -7
- package/dist/harness-session.d.ts +21 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +134 -39
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -2
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +1 -0
- package/dist/test-file-platform-filter.d.ts +5 -0
- package/dist/test-file-platform-filter.d.ts.map +1 -0
- package/dist/test-file-platform-filter.js +40 -0
- package/dist/toTestResult.d.ts +1 -0
- package/dist/toTestResult.d.ts.map +1 -1
- package/dist/toTestResult.js +2 -2
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/__tests__/bridge.test.ts +37 -2
- package/src/__tests__/crash-monitor.test.ts +182 -231
- package/src/__tests__/errors.test.ts +75 -0
- package/src/__tests__/execute-run.test.ts +235 -34
- package/src/__tests__/harness-session.test.ts +93 -0
- package/src/__tests__/index.test.ts +89 -11
- package/src/__tests__/test-file-platform-filter.test.ts +83 -0
- package/src/crash-monitor.ts +192 -131
- package/src/errors.ts +62 -1
- package/src/execute-run.ts +125 -11
- package/src/harness-session.ts +177 -42
- package/src/index.ts +40 -11
- package/src/run.ts +1 -0
- package/src/test-file-platform-filter.ts +57 -0
- package/src/toTestResult.ts +3 -2
package/dist/crash-monitor.js
CHANGED
|
@@ -1,136 +1,143 @@
|
|
|
1
|
-
import { NativeCrashError, } from './errors.js';
|
|
1
|
+
import { NativeCrashError, RuntimeDisconnectError, } from './errors.js';
|
|
2
2
|
import { logger } from '@react-native-harness/tools';
|
|
3
3
|
const crashLogger = logger.child('crash');
|
|
4
|
+
const CRASH_CLASSIFICATION_SETTLE_MS = 1500;
|
|
5
|
+
const CRASH_LOG_WINDOW_MS = 3000;
|
|
4
6
|
export class CrashWatchCancelledError extends Error {
|
|
5
7
|
constructor() {
|
|
6
8
|
super('Crash watch was cancelled');
|
|
7
9
|
this.name = 'CrashWatchCancelledError';
|
|
8
10
|
}
|
|
9
11
|
}
|
|
10
|
-
const
|
|
12
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
const isCrashIndicator = (line) => /uncaught exception|terminating app due to|fatal error|EXC_[A-Z_]+|termination reason|crash|abort/i.test(line) || /\bSIG[A-Z]{2,}\b/.test(line);
|
|
14
|
+
const getMatchingCrashLines = (logs, occurredAt) => logs
|
|
15
|
+
.filter((log) => Math.abs(log.occurredAt - occurredAt) <= CRASH_LOG_WINDOW_MS)
|
|
16
|
+
.map((log) => log.line)
|
|
17
|
+
.filter(isCrashIndicator);
|
|
18
|
+
const buildNativeCrashDetails = (phase, rawLines, summary) => ({
|
|
11
19
|
phase,
|
|
12
|
-
source:
|
|
13
|
-
summary:
|
|
14
|
-
|
|
15
|
-
exceptionType: enriched?.exceptionType ?? initial?.exceptionType,
|
|
16
|
-
processName: enriched?.processName ?? initial?.processName,
|
|
17
|
-
pid: enriched?.pid ?? initial?.pid,
|
|
18
|
-
stackTrace: enriched?.stackTrace ?? initial?.stackTrace,
|
|
19
|
-
rawLines: enriched?.rawLines ?? initial?.rawLines,
|
|
20
|
-
artifactType: enriched?.artifactType ?? initial?.artifactType,
|
|
21
|
-
artifactPath: enriched?.artifactPath ?? initial?.artifactPath,
|
|
20
|
+
source: rawLines.length > 0 ? 'logs' : 'bridge',
|
|
21
|
+
summary: rawLines.length > 0 ? rawLines.join('\n') : summary,
|
|
22
|
+
rawLines: rawLines.length > 0 ? rawLines : undefined,
|
|
22
23
|
});
|
|
23
|
-
|
|
24
|
+
const getStatePid = (state) => {
|
|
25
|
+
if (state && 'pid' in state) {
|
|
26
|
+
return state.pid;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
const mergeCrashDetails = (fallback, extracted) => {
|
|
31
|
+
if (!extracted) {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
...fallback,
|
|
36
|
+
...extracted,
|
|
37
|
+
phase: fallback.phase,
|
|
38
|
+
source: extracted.source ?? fallback.source,
|
|
39
|
+
summary: extracted.summary ?? fallback.summary,
|
|
40
|
+
rawLines: extracted.rawLines ?? fallback.rawLines,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const buildRuntimeDisconnectDetails = (phase, rawLines) => ({
|
|
44
|
+
phase,
|
|
45
|
+
source: 'bridge',
|
|
46
|
+
summary: 'The runtime bridge disconnected, but the app session still appears to be running.',
|
|
47
|
+
rawLines: rawLines.length > 0 ? rawLines : undefined,
|
|
48
|
+
});
|
|
49
|
+
export const createCrashMonitor = ({ appSession: initialAppSession = null, } = {}) => {
|
|
24
50
|
let alive = false;
|
|
25
51
|
let monitoring = true;
|
|
26
52
|
let isResolvingCrash = false;
|
|
27
53
|
let disposed = false;
|
|
28
|
-
|
|
29
|
-
|
|
54
|
+
let appSession = null;
|
|
55
|
+
let pendingTimer = null;
|
|
30
56
|
let currentTestFilePath = '';
|
|
31
57
|
let currentPhase = 'startup';
|
|
32
58
|
const watchers = new Set();
|
|
33
|
-
const
|
|
34
|
-
if ('getCrashDetails' in appMonitor) {
|
|
35
|
-
return appMonitor;
|
|
36
|
-
}
|
|
37
|
-
if (platformRunner.getCrashDetails) {
|
|
38
|
-
return platformRunner;
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
};
|
|
42
|
-
const notifyCrash = (err) => {
|
|
59
|
+
const notifyFailure = (err) => {
|
|
43
60
|
const pending = [...watchers];
|
|
44
61
|
watchers.clear();
|
|
45
62
|
for (const fn of pending)
|
|
46
63
|
fn(err);
|
|
47
64
|
};
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
65
|
+
const clearPendingTimer = () => {
|
|
66
|
+
if (pendingTimer) {
|
|
67
|
+
clearTimeout(pendingTimer);
|
|
68
|
+
pendingTimer = null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const classify = async (pending, trigger) => {
|
|
72
|
+
if (disposed || !monitoring || isResolvingCrash) {
|
|
50
73
|
return;
|
|
51
|
-
isResolvingCrash = true;
|
|
52
|
-
alive = false;
|
|
53
|
-
crashLogger.debug('native crash detected (phase=%s)', phase);
|
|
54
|
-
for (const line of details?.rawLines ?? []) {
|
|
55
|
-
crashLogger.debug('%s', line);
|
|
56
74
|
}
|
|
75
|
+
isResolvingCrash = true;
|
|
57
76
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
const session = appSession;
|
|
78
|
+
const state = await session?.getState();
|
|
79
|
+
const logs = session?.getLogs() ?? [];
|
|
80
|
+
const rawLines = getMatchingCrashLines(logs, pending.occurredAt);
|
|
81
|
+
if (state?.status === 'running' && trigger === 'bridge-disconnect') {
|
|
82
|
+
crashLogger.debug('runtime bridge disconnected without confirmed app death');
|
|
83
|
+
notifyFailure(new RuntimeDisconnectError(pending.testFilePath, buildRuntimeDisconnectDetails(pending.phase, rawLines)));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
alive = false;
|
|
87
|
+
crashLogger.debug('native crash detected (phase=%s)', pending.phase);
|
|
88
|
+
for (const line of rawLines) {
|
|
89
|
+
crashLogger.debug('%s', line);
|
|
90
|
+
}
|
|
91
|
+
const fallbackSummary = trigger === 'bridge-disconnect'
|
|
92
|
+
? 'The app process exited after the runtime bridge disconnected, but no crash log lines were found.'
|
|
93
|
+
: 'The app process exited, but no crash log lines were found.';
|
|
94
|
+
const details = buildNativeCrashDetails(pending.phase, rawLines, fallbackSummary);
|
|
95
|
+
const extractedDetails = session?.getCrashDetails
|
|
96
|
+
? await session
|
|
97
|
+
.getCrashDetails({
|
|
98
|
+
occurredAt: pending.occurredAt,
|
|
99
|
+
pid: getStatePid(state),
|
|
100
|
+
processName: details.processName,
|
|
101
|
+
testFilePath: pending.testFilePath,
|
|
102
|
+
})
|
|
103
|
+
.catch((error) => {
|
|
104
|
+
crashLogger.warn('failed to extract native crash details: %s', error);
|
|
105
|
+
return null;
|
|
106
|
+
})
|
|
107
|
+
: null;
|
|
108
|
+
notifyFailure(new NativeCrashError(pending.testFilePath, mergeCrashDetails(details, extractedDetails)));
|
|
74
109
|
}
|
|
75
110
|
finally {
|
|
76
111
|
isResolvingCrash = false;
|
|
112
|
+
pendingTimer = null;
|
|
77
113
|
}
|
|
78
114
|
};
|
|
79
|
-
const
|
|
80
|
-
if (disposed || !monitoring)
|
|
115
|
+
const startCrashResolution = (trigger) => {
|
|
116
|
+
if (disposed || !monitoring || isResolvingCrash) {
|
|
81
117
|
return;
|
|
82
|
-
try {
|
|
83
|
-
const isRunning = await platformRunner.isAppRunning();
|
|
84
|
-
if (!isRunning) {
|
|
85
|
-
void handleCrash(phase, details, fallbackSummary);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
crashLogger.debug('crash confirmation failed', error);
|
|
90
118
|
}
|
|
119
|
+
clearPendingTimer();
|
|
120
|
+
const pending = {
|
|
121
|
+
testFilePath: currentTestFilePath,
|
|
122
|
+
phase: currentPhase,
|
|
123
|
+
occurredAt: Date.now(),
|
|
124
|
+
};
|
|
125
|
+
pendingTimer = setTimeout(() => {
|
|
126
|
+
void classify(pending, trigger);
|
|
127
|
+
}, trigger === 'bridge-disconnect' ? CRASH_CLASSIFICATION_SETTLE_MS : 0);
|
|
91
128
|
};
|
|
92
|
-
const
|
|
93
|
-
? {
|
|
94
|
-
source: event.crashDetails.source ?? event.source,
|
|
95
|
-
summary: event.crashDetails.summary,
|
|
96
|
-
signal: event.crashDetails.signal,
|
|
97
|
-
exceptionType: event.crashDetails.exceptionType,
|
|
98
|
-
processName: event.crashDetails.processName,
|
|
99
|
-
pid: event.crashDetails.pid ?? event.pid,
|
|
100
|
-
stackTrace: event.crashDetails.stackTrace,
|
|
101
|
-
rawLines: event.crashDetails.rawLines ??
|
|
102
|
-
(event.line ? [event.line] : undefined),
|
|
103
|
-
}
|
|
104
|
-
: undefined;
|
|
105
|
-
const appMonitorListener = (event) => {
|
|
106
|
-
if (disposed || !monitoring)
|
|
107
|
-
return;
|
|
108
|
-
if (event.type === 'app_started') {
|
|
109
|
-
alive = true;
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
129
|
+
const appSessionListener = (event) => {
|
|
112
130
|
if (event.type === 'app_exited') {
|
|
113
|
-
|
|
114
|
-
if (event.isConfirmed ?? event.source === 'polling') {
|
|
115
|
-
void handleCrash(currentPhase, details);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
void confirmAndHandleCrash(currentPhase, details);
|
|
119
|
-
}
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (event.type === 'possible_crash') {
|
|
123
|
-
const details = extractCrashDetails(event);
|
|
124
|
-
const fallback = `possible crash signal (${event.source ?? 'unknown'})`;
|
|
125
|
-
if (event.isConfirmed) {
|
|
126
|
-
void handleCrash(currentPhase, details, fallback);
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
void confirmAndHandleCrash(currentPhase, details, fallback);
|
|
130
|
-
}
|
|
131
|
+
startCrashResolution('app-exit');
|
|
131
132
|
}
|
|
132
133
|
};
|
|
133
|
-
|
|
134
|
+
const setAppSession = (session) => {
|
|
135
|
+
appSession?.removeListener(appSessionListener);
|
|
136
|
+
appSession = session;
|
|
137
|
+
alive = Boolean(session);
|
|
138
|
+
session?.addListener(appSessionListener);
|
|
139
|
+
};
|
|
140
|
+
setAppSession(initialAppSession);
|
|
134
141
|
const watch = (testFilePath, phase) => {
|
|
135
142
|
currentTestFilePath = testFilePath;
|
|
136
143
|
currentPhase = phase;
|
|
@@ -152,25 +159,32 @@ export const createCrashMonitor = ({ appMonitor, platformRunner, }) => {
|
|
|
152
159
|
isAlive: () => alive,
|
|
153
160
|
stop: async () => {
|
|
154
161
|
monitoring = false;
|
|
155
|
-
|
|
162
|
+
clearPendingTimer();
|
|
163
|
+
await sleep(0);
|
|
156
164
|
},
|
|
157
165
|
start: async () => {
|
|
158
166
|
monitoring = true;
|
|
159
|
-
await appMonitor.start();
|
|
160
167
|
},
|
|
161
168
|
reset: () => {
|
|
162
|
-
alive =
|
|
169
|
+
alive = Boolean(appSession);
|
|
163
170
|
watchers.clear();
|
|
164
171
|
isResolvingCrash = false;
|
|
165
172
|
currentTestFilePath = '';
|
|
173
|
+
clearPendingTimer();
|
|
174
|
+
},
|
|
175
|
+
setAppSession,
|
|
176
|
+
handleBridgeDisconnect: () => {
|
|
177
|
+
startCrashResolution('bridge-disconnect');
|
|
166
178
|
},
|
|
167
179
|
dispose: async () => {
|
|
168
180
|
disposed = true;
|
|
169
181
|
monitoring = false;
|
|
170
182
|
watchers.clear();
|
|
171
183
|
isResolvingCrash = false;
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
clearPendingTimer();
|
|
185
|
+
appSession?.removeListener(appSessionListener);
|
|
186
|
+
appSession = null;
|
|
187
|
+
alive = false;
|
|
174
188
|
},
|
|
175
189
|
};
|
|
176
190
|
};
|
package/dist/errors.d.ts
CHANGED
|
@@ -24,6 +24,9 @@ export type NativeCrashPhase = 'startup' | 'execution';
|
|
|
24
24
|
export type NativeCrashDetails = AppCrashDetails & {
|
|
25
25
|
phase: NativeCrashPhase;
|
|
26
26
|
};
|
|
27
|
+
export type RuntimeDisconnectDetails = AppCrashDetails & {
|
|
28
|
+
phase: NativeCrashPhase;
|
|
29
|
+
};
|
|
27
30
|
export declare class NativeCrashError extends HarnessError {
|
|
28
31
|
readonly testFilePath: string;
|
|
29
32
|
readonly details: NativeCrashDetails;
|
|
@@ -31,4 +34,11 @@ export declare class NativeCrashError extends HarnessError {
|
|
|
31
34
|
constructor(testFilePath: string, details: NativeCrashDetails, lastKnownTest?: string | undefined);
|
|
32
35
|
get phase(): NativeCrashPhase;
|
|
33
36
|
}
|
|
37
|
+
export declare class RuntimeDisconnectError extends HarnessError {
|
|
38
|
+
readonly testFilePath: string;
|
|
39
|
+
readonly details: RuntimeDisconnectDetails;
|
|
40
|
+
constructor(testFilePath: string, details: RuntimeDisconnectDetails);
|
|
41
|
+
get phase(): NativeCrashPhase;
|
|
42
|
+
}
|
|
43
|
+
export type HarnessRuntimeFailure = NativeCrashError | RuntimeDisconnectError;
|
|
34
44
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,YAAY,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EACL,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,qCAAqC,CAAC;AAE7C,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D;AAED,qBAAa,yBAA0B,SAAQ,YAAY;aAC7B,OAAO,EAAE,MAAM;gBAAf,OAAO,EAAE,MAAM;CAM5C;AAED,qBAAa,4BAA6B,SAAQ,YAAY;aAE1C,WAAW,EAAE,MAAM;aACnB,QAAQ,EAAE,MAAM;gBADhB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM;CAQnC;AAED,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG;IACjD,KAAK,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,eAAe,GAAG;IACvD,KAAK,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAmEF,qBAAa,gBAAiB,SAAQ,YAAY;aAE9B,YAAY,EAAE,MAAM;aACpB,OAAO,EAAE,kBAAkB;aAC3B,aAAa,CAAC,EAAE,MAAM;gBAFtB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,kBAAkB,EAC3B,aAAa,CAAC,EAAE,MAAM,YAAA;IAOxC,IAAI,KAAK,qBAER;CACF;AA0BD,qBAAa,sBAAuB,SAAQ,YAAY;aAEpC,YAAY,EAAE,MAAM;aACpB,OAAO,EAAE,wBAAwB;gBADjC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,wBAAwB;IAOnD,IAAI,KAAK,qBAER;CACF;AAED,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,GAAG,sBAAsB,CAAC"}
|
package/dist/errors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HarnessError } from '@react-native-harness/tools';
|
|
1
|
+
import { formatPath, HarnessError } from '@react-native-harness/tools';
|
|
2
2
|
export { StartupStallError, } from '@react-native-harness/bundler-metro';
|
|
3
3
|
export class NoRunnerSpecifiedError extends HarnessError {
|
|
4
4
|
constructor() {
|
|
@@ -39,12 +39,21 @@ export class MetroPortRangeExhaustedError extends HarnessError {
|
|
|
39
39
|
this.name = 'MetroPortRangeExhaustedError';
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
const buildNativeCrashMessage = ({ phase, summary, signal, exceptionType, processName, pid, stackTrace, artifactType, }) => {
|
|
42
|
+
const buildNativeCrashMessage = ({ phase, summary, signal, exceptionType, processName, pid, stackTrace, artifactType, artifactPath, enrichmentArtifacts, }) => {
|
|
43
43
|
const lines = [
|
|
44
44
|
phase === 'startup'
|
|
45
45
|
? 'The native app crashed while preparing to run this test file.'
|
|
46
46
|
: 'The native app crashed during test execution.',
|
|
47
47
|
];
|
|
48
|
+
lines.push(artifactPath
|
|
49
|
+
? `Harness extracted the crash log: ${formatPath(artifactPath)}`
|
|
50
|
+
: "Harness couldn't extract the crash log.");
|
|
51
|
+
if (enrichmentArtifacts && enrichmentArtifacts.length > 0) {
|
|
52
|
+
lines.push('Additional crash artifacts:');
|
|
53
|
+
for (const artifact of enrichmentArtifacts) {
|
|
54
|
+
lines.push(` - ${formatPath(artifact.artifactPath)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
48
57
|
const hasCrashBlock = summary?.includes('\n') ?? false;
|
|
49
58
|
const shouldRenderSummary = Boolean(summary) &&
|
|
50
59
|
!(!hasCrashBlock && artifactType === 'ios-crash-report');
|
|
@@ -89,3 +98,33 @@ export class NativeCrashError extends HarnessError {
|
|
|
89
98
|
return this.details.phase;
|
|
90
99
|
}
|
|
91
100
|
}
|
|
101
|
+
const buildRuntimeDisconnectMessage = ({ phase, summary, rawLines, }) => {
|
|
102
|
+
const lines = [
|
|
103
|
+
phase === 'startup'
|
|
104
|
+
? 'The native runtime disconnected while preparing to run this test file.'
|
|
105
|
+
: 'The native runtime disconnected during test execution.',
|
|
106
|
+
];
|
|
107
|
+
if (summary) {
|
|
108
|
+
lines.push('');
|
|
109
|
+
lines.push(summary);
|
|
110
|
+
}
|
|
111
|
+
if (rawLines && rawLines.length > 0 && summary !== rawLines.join('\n')) {
|
|
112
|
+
lines.push('');
|
|
113
|
+
lines.push(...rawLines);
|
|
114
|
+
}
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
};
|
|
117
|
+
export class RuntimeDisconnectError extends HarnessError {
|
|
118
|
+
testFilePath;
|
|
119
|
+
details;
|
|
120
|
+
constructor(testFilePath, details) {
|
|
121
|
+
super(buildRuntimeDisconnectMessage(details));
|
|
122
|
+
this.testFilePath = testFilePath;
|
|
123
|
+
this.details = details;
|
|
124
|
+
this.name = 'RuntimeDisconnectError';
|
|
125
|
+
this.stack = `${this.name}: ${this.message.split('\n')[0]}`;
|
|
126
|
+
}
|
|
127
|
+
get phase() {
|
|
128
|
+
return this.details.phase;
|
|
129
|
+
}
|
|
130
|
+
}
|
package/dist/execute-run.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Test, TestWatcher, Config, TestEvents } from 'jest-runner';
|
|
2
2
|
import { type HarnessSession } from './harness-session.js';
|
|
3
|
-
|
|
3
|
+
type EmitTestEvent = <Name extends keyof TestEvents>(eventName: Name, ...eventData: TestEvents[Name]) => Promise<void>;
|
|
4
|
+
export declare const executeRun: (session: HarnessSession, tests: Array<Test>, watcher: TestWatcher, emitEvent: EmitTestEvent, globalConfig: Config.GlobalConfig) => Promise<void>;
|
|
5
|
+
export {};
|
|
4
6
|
//# sourceMappingURL=execute-run.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute-run.d.ts","sourceRoot":"","sources":["../src/execute-run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"execute-run.d.ts","sourceRoot":"","sources":["../src/execute-run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,WAAW,EACX,MAAM,EACN,UAAU,EACX,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,KAAK,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AAsBjF,KAAK,aAAa,GAAG,CAAC,IAAI,SAAS,MAAM,UAAU,EACjD,SAAS,EAAE,IAAI,EACf,GAAG,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,KAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;AAmFnB,eAAO,MAAM,UAAU,GACrB,SAAS,cAAc,EACvB,OAAO,KAAK,CAAC,IAAI,CAAC,EAClB,SAAS,WAAW,EACpB,WAAW,aAAa,EACxB,cAAc,MAAM,CAAC,YAAY,KAChC,OAAO,CAAC,IAAI,CA8Ld,CAAC"}
|
package/dist/execute-run.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { runHarnessTestFile } from './run.js';
|
|
4
|
-
import { NativeCrashError, StartupStallError, } from './errors.js';
|
|
5
|
-
import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
|
|
4
|
+
import { NativeCrashError, RuntimeDisconnectError, StartupStallError, } from './errors.js';
|
|
5
|
+
import { AppBridgeDisconnectedError, DeviceNotRespondingError, } from '@react-native-harness/bridge/server';
|
|
6
|
+
import { createPlatformSkippedTestResult, shouldRunHarnessTestFile, } from './test-file-platform-filter.js';
|
|
6
7
|
const createRunSummary = () => ({ passed: 0, failed: 0, skipped: 0, todo: 0 });
|
|
7
8
|
const applyJestResultToSummary = (summary, result) => {
|
|
8
9
|
summary.passed += result.numPassingTests;
|
|
@@ -18,19 +19,62 @@ class CancelRun extends Error {
|
|
|
18
19
|
}
|
|
19
20
|
const buildTestFailure = (err) => {
|
|
20
21
|
if (err instanceof NativeCrashError ||
|
|
22
|
+
err instanceof RuntimeDisconnectError ||
|
|
21
23
|
err instanceof StartupStallError ||
|
|
24
|
+
err instanceof AppBridgeDisconnectedError ||
|
|
22
25
|
err instanceof DeviceNotRespondingError) {
|
|
23
26
|
return { message: err.message, stack: '' };
|
|
24
27
|
}
|
|
25
28
|
return err;
|
|
26
29
|
};
|
|
27
|
-
|
|
30
|
+
const toJestMode = (declarationMode) => declarationMode;
|
|
31
|
+
const emitHarnessTestStarted = async (emitEvent, event) => {
|
|
32
|
+
await emitEvent('test-case-start', event.file, {
|
|
33
|
+
ancestorTitles: event.ancestorTitles,
|
|
34
|
+
fullName: event.fullName,
|
|
35
|
+
mode: toJestMode(event.declarationMode),
|
|
36
|
+
title: event.name,
|
|
37
|
+
startedAt: event.startedAt,
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
const emitHarnessTestFinished = async (emitEvent, event) => {
|
|
41
|
+
const failureMessage = event.error?.message;
|
|
42
|
+
const codeFrame = event.error?.codeFrame;
|
|
43
|
+
const location = codeFrame?.location
|
|
44
|
+
? { column: codeFrame.location.column, line: codeFrame.location.row }
|
|
45
|
+
: null;
|
|
46
|
+
const testCaseResult = {
|
|
47
|
+
ancestorTitles: event.ancestorTitles,
|
|
48
|
+
duration: event.duration,
|
|
49
|
+
failureDetails: [],
|
|
50
|
+
failureMessages: failureMessage
|
|
51
|
+
? [`${failureMessage}${codeFrame ? `\n\n${codeFrame.content}` : ''}`]
|
|
52
|
+
: [],
|
|
53
|
+
fullName: event.fullName,
|
|
54
|
+
location,
|
|
55
|
+
numPassingAsserts: event.status === 'passed' ? 1 : 0,
|
|
56
|
+
startedAt: event.startedAt,
|
|
57
|
+
status: event.status,
|
|
58
|
+
title: event.name,
|
|
59
|
+
};
|
|
60
|
+
await emitEvent('test-case-result', event.file, testCaseResult);
|
|
61
|
+
};
|
|
62
|
+
const isHarnessCaseEvent = (event) => event.type === 'test-started' || event.type === 'test-finished';
|
|
63
|
+
export const executeRun = async (session, tests, watcher, emitEvent, globalConfig) => {
|
|
28
64
|
const runId = randomUUID();
|
|
29
65
|
const startTime = Date.now();
|
|
30
66
|
const watchMode = globalConfig.watch || globalConfig.watchAll;
|
|
31
67
|
const rootDir = globalConfig.rootDir ?? process.cwd();
|
|
32
68
|
const testFiles = tests.map((t) => path.relative(rootDir, t.path));
|
|
33
69
|
const summary = createRunSummary();
|
|
70
|
+
let caseEventChain = Promise.resolve();
|
|
71
|
+
const unsubscribe = session.onTestRunnerEvent((event) => {
|
|
72
|
+
if (isHarnessCaseEvent(event)) {
|
|
73
|
+
caseEventChain = caseEventChain.then(() => event.type === 'test-started'
|
|
74
|
+
? emitHarnessTestStarted(emitEvent, event)
|
|
75
|
+
: emitHarnessTestFinished(emitEvent, event));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
34
78
|
const updateRunState = (overrides = {}) => {
|
|
35
79
|
const state = {
|
|
36
80
|
runId,
|
|
@@ -59,6 +103,8 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
|
|
|
59
103
|
host: session.config.host?.trim() || undefined,
|
|
60
104
|
});
|
|
61
105
|
const shouldResetEnv = session.config.resetEnvironmentBetweenTestFiles;
|
|
106
|
+
const platformId = session.context.platform.platformId;
|
|
107
|
+
const knownPlatformIds = new Set(session.config.runners.map((runner) => runner.platformId));
|
|
62
108
|
let isFirstTest = true;
|
|
63
109
|
let runError;
|
|
64
110
|
try {
|
|
@@ -79,13 +125,39 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
|
|
|
79
125
|
});
|
|
80
126
|
};
|
|
81
127
|
await session.callHook('test-file:started', { runId, file: relativeTestPath });
|
|
128
|
+
if (!shouldRunHarnessTestFile(test.path, platformId, knownPlatformIds)) {
|
|
129
|
+
try {
|
|
130
|
+
await emitEvent('test-file-start', test);
|
|
131
|
+
const skippedResult = createPlatformSkippedTestResult(test.path);
|
|
132
|
+
applyJestResultToSummary(summary, skippedResult);
|
|
133
|
+
updateRunState();
|
|
134
|
+
await emitTestFileFinished({
|
|
135
|
+
status: 'skipped',
|
|
136
|
+
duration: Date.now() - fileStartedAt,
|
|
137
|
+
result: null,
|
|
138
|
+
});
|
|
139
|
+
await emitEvent('test-file-success', test, skippedResult);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (!emittedTestFileFinished) {
|
|
143
|
+
await emitTestFileFinished({
|
|
144
|
+
status: 'failed',
|
|
145
|
+
duration: Date.now() - fileStartedAt,
|
|
146
|
+
result: null,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
updateRunState({ error: err });
|
|
150
|
+
await emitEvent('test-file-failure', test, buildTestFailure(err));
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
82
154
|
try {
|
|
83
155
|
if (shouldResetEnv && !isFirstTest) {
|
|
84
156
|
await session.restartApp(test.path);
|
|
85
157
|
}
|
|
86
158
|
isFirstTest = false;
|
|
87
159
|
session.flushClientLogs();
|
|
88
|
-
await
|
|
160
|
+
await emitEvent('test-file-start', test);
|
|
89
161
|
await session.ensureAppReady(test.path);
|
|
90
162
|
// Crash detection is handled inside session.runTestFile; NativeCrashError
|
|
91
163
|
// propagates here if a crash wins the race.
|
|
@@ -106,7 +178,8 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
|
|
|
106
178
|
duration: result.duration,
|
|
107
179
|
result: result.harnessResult,
|
|
108
180
|
});
|
|
109
|
-
await
|
|
181
|
+
await caseEventChain;
|
|
182
|
+
await emitEvent('test-file-success', test, result.jestResult);
|
|
110
183
|
}
|
|
111
184
|
catch (err) {
|
|
112
185
|
if (!emittedTestFileFinished) {
|
|
@@ -117,17 +190,20 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
|
|
|
117
190
|
});
|
|
118
191
|
}
|
|
119
192
|
const isRuntimeFailure = err instanceof NativeCrashError ||
|
|
193
|
+
err instanceof RuntimeDisconnectError ||
|
|
120
194
|
err instanceof StartupStallError ||
|
|
195
|
+
err instanceof AppBridgeDisconnectedError ||
|
|
121
196
|
err instanceof DeviceNotRespondingError;
|
|
122
197
|
if (isRuntimeFailure) {
|
|
123
198
|
summary.failed += 1;
|
|
124
199
|
updateRunState();
|
|
125
200
|
}
|
|
126
|
-
if (err instanceof NativeCrashError) {
|
|
201
|
+
if (err instanceof NativeCrashError || err instanceof RuntimeDisconnectError) {
|
|
127
202
|
session.resetCrashState();
|
|
128
203
|
}
|
|
129
204
|
updateRunState({ error: isRuntimeFailure ? undefined : err });
|
|
130
|
-
|
|
205
|
+
await caseEventChain;
|
|
206
|
+
await emitEvent('test-file-failure', test, buildTestFailure(err));
|
|
131
207
|
}
|
|
132
208
|
}
|
|
133
209
|
}
|
|
@@ -147,5 +223,7 @@ export const executeRun = async (session, tests, watcher, onStart, onResult, onF
|
|
|
147
223
|
status: runState.status ?? (runError != null ? 'failed' : 'passed'),
|
|
148
224
|
...(runError != null ? { error: runError } : {}),
|
|
149
225
|
});
|
|
226
|
+
await caseEventChain;
|
|
227
|
+
unsubscribe();
|
|
150
228
|
}
|
|
151
229
|
};
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type AppConnection } from '@react-native-harness/bridge/server';
|
|
2
|
+
import { type HarnessContext, type TestRunnerEvents, type TestExecutionOptions, type TestSuiteResult } from '@react-native-harness/bridge';
|
|
2
3
|
import { type HarnessPlatform } from '@react-native-harness/platforms';
|
|
3
4
|
import { type HarnessPluginManager, type HarnessRunStatus, type HarnessRunSummary } from '@react-native-harness/plugins';
|
|
4
5
|
import { type Config as HarnessConfig } from '@react-native-harness/config';
|
|
5
6
|
import type { Config as JestConfig } from 'jest-runner';
|
|
7
|
+
import { type CrashMonitor } from './crash-monitor.js';
|
|
6
8
|
import { type ClientLogBuffer } from './client-log-handler.js';
|
|
7
9
|
import { type ResourceLockManager } from './resource-lock.js';
|
|
10
|
+
type DisconnectObservableBridge = {
|
|
11
|
+
readonly connection: AppConnection | null;
|
|
12
|
+
on: (event: 'disconnected', listener: () => void) => void;
|
|
13
|
+
off: (event: 'disconnected', listener: () => void) => void;
|
|
14
|
+
};
|
|
15
|
+
export declare const waitForBridgeDisconnectOrTimeout: ({ bridge, connection, timeoutMs, }: {
|
|
16
|
+
bridge: DisconnectObservableBridge;
|
|
17
|
+
connection: AppConnection;
|
|
18
|
+
timeoutMs: number;
|
|
19
|
+
}) => Promise<boolean>;
|
|
8
20
|
export type HarnessRunState = {
|
|
9
21
|
readonly runId: string;
|
|
10
22
|
readonly startTime: number;
|
|
@@ -19,6 +31,7 @@ export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
|
|
|
19
31
|
export type HarnessSession = {
|
|
20
32
|
readonly config: HarnessConfig;
|
|
21
33
|
readonly context: HarnessContext;
|
|
34
|
+
onTestRunnerEvent: (listener: (event: TestRunnerEvents) => void) => () => void;
|
|
22
35
|
runTestFile: (path: string, options: HarnessRunTestsOptions) => Promise<TestSuiteResult>;
|
|
23
36
|
ensureAppReady: (testFilePath: string) => Promise<void>;
|
|
24
37
|
restartApp: (testFilePath?: string) => Promise<void>;
|
|
@@ -28,7 +41,14 @@ export type HarnessSession = {
|
|
|
28
41
|
setRunState: (state: HarnessRunState | null) => void;
|
|
29
42
|
dispose: (reason?: 'normal' | 'abort' | 'error') => Promise<void>;
|
|
30
43
|
};
|
|
44
|
+
export declare const waitForStartupCrash: ({ crashMonitor, detectNativeCrashes, testFilePath, signal, }: {
|
|
45
|
+
crashMonitor: CrashMonitor;
|
|
46
|
+
detectNativeCrashes: boolean;
|
|
47
|
+
testFilePath: string;
|
|
48
|
+
signal: AbortSignal;
|
|
49
|
+
}) => Promise<never>;
|
|
31
50
|
export declare const createHarnessSession: (globalConfig: JestConfig.GlobalConfig, { lockManager }?: {
|
|
32
51
|
lockManager?: ResourceLockManager;
|
|
33
52
|
}) => Promise<HarnessSession>;
|
|
53
|
+
export {};
|
|
34
54
|
//# sourceMappingURL=harness-session.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness-session.d.ts","sourceRoot":"","sources":["../src/harness-session.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"harness-session.d.ts","sourceRoot":"","sources":["../src/harness-session.ts"],"names":[],"mappings":"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"}
|