@react-native-harness/jest 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/bridge.test.js +29 -4
- package/dist/__tests__/crash-monitor.test.js +194 -176
- package/dist/__tests__/environment-reset.test.d.ts +2 -0
- package/dist/__tests__/environment-reset.test.d.ts.map +1 -0
- package/dist/__tests__/environment-reset.test.js +238 -0
- package/dist/__tests__/errors.test.js +39 -0
- package/dist/__tests__/execute-run.test.js +439 -29
- package/dist/__tests__/format-harness-error.test.d.ts +2 -0
- package/dist/__tests__/format-harness-error.test.d.ts.map +1 -0
- package/dist/__tests__/format-harness-error.test.js +27 -0
- package/dist/__tests__/harness-cache.test.js +62 -41
- package/dist/__tests__/harness-session-signal.test.d.ts +2 -0
- package/dist/__tests__/harness-session-signal.test.d.ts.map +1 -0
- package/dist/__tests__/harness-session-signal.test.js +298 -0
- package/dist/__tests__/harness-session.test.js +217 -234
- package/dist/__tests__/index.test.js +62 -11
- package/dist/__tests__/metro-port.test.js +2 -0
- package/dist/__tests__/run.test.d.ts +2 -0
- package/dist/__tests__/run.test.d.ts.map +1 -0
- package/dist/__tests__/run.test.js +48 -0
- package/dist/__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 +12 -4
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +135 -102
- package/dist/diagnostics/__tests__/derivers.test.d.ts +2 -0
- package/dist/diagnostics/__tests__/derivers.test.d.ts.map +1 -0
- package/dist/diagnostics/__tests__/derivers.test.js +248 -0
- package/dist/diagnostics/__tests__/report.test.d.ts +2 -0
- package/dist/diagnostics/__tests__/report.test.d.ts.map +1 -0
- package/dist/diagnostics/__tests__/report.test.js +97 -0
- package/dist/diagnostics/derivers.d.ts +24 -0
- package/dist/diagnostics/derivers.d.ts.map +1 -0
- package/dist/diagnostics/derivers.js +146 -0
- package/dist/diagnostics/index.d.ts +3 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +2 -0
- package/dist/diagnostics/report.d.ts +7 -0
- package/dist/diagnostics/report.d.ts.map +1 -0
- package/dist/diagnostics/report.js +84 -0
- package/dist/environment-reset.d.ts +51 -0
- package/dist/environment-reset.d.ts.map +1 -0
- package/dist/environment-reset.js +83 -0
- package/dist/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 +179 -49
- package/dist/format-harness-error.d.ts +5 -0
- package/dist/format-harness-error.d.ts.map +1 -0
- package/dist/format-harness-error.js +43 -0
- package/dist/harness-session.d.ts +46 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +386 -118
- 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 +22 -4
- 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 +8 -7
- package/src/__tests__/bridge.test.ts +45 -5
- package/src/__tests__/crash-monitor.test.ts +234 -199
- package/src/__tests__/environment-reset.test.ts +308 -0
- package/src/__tests__/errors.test.ts +75 -0
- package/src/__tests__/execute-run.test.ts +561 -37
- package/src/__tests__/format-harness-error.test.ts +32 -0
- package/src/__tests__/harness-cache.test.ts +91 -0
- package/src/__tests__/harness-session.test.ts +303 -0
- package/src/__tests__/index.test.ts +92 -12
- package/src/__tests__/metro-port.test.ts +2 -0
- package/src/__tests__/run.test.ts +74 -0
- package/src/__tests__/test-file-platform-filter.test.ts +83 -0
- package/src/crash-monitor.ts +220 -131
- package/src/diagnostics/__tests__/derivers.test.ts +300 -0
- package/src/diagnostics/__tests__/report.test.ts +128 -0
- package/src/diagnostics/derivers.ts +170 -0
- package/src/diagnostics/index.ts +2 -0
- package/src/diagnostics/report.ts +124 -0
- package/src/environment-reset.ts +138 -0
- package/src/errors.ts +62 -1
- package/src/execute-run.ts +230 -57
- package/src/format-harness-error.ts +73 -0
- package/src/harness-session.ts +535 -162
- package/src/index.ts +40 -11
- package/src/run.ts +24 -4
- package/src/test-file-platform-filter.ts +57 -0
- package/src/toTestResult.ts +3 -2
- package/tsconfig.json +3 -0
- package/tsconfig.lib.json +3 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createPlatformSkippedTestResult,
|
|
4
|
+
getHarnessTestFilePlatform,
|
|
5
|
+
shouldRunHarnessTestFile,
|
|
6
|
+
} from '../test-file-platform-filter.js';
|
|
7
|
+
|
|
8
|
+
const knownPlatformIds = new Set(['android', 'ios', 'web', 'vega']);
|
|
9
|
+
|
|
10
|
+
describe('getHarnessTestFilePlatform', () => {
|
|
11
|
+
it('returns null for shared harness test files', () => {
|
|
12
|
+
expect(getHarnessTestFilePlatform('/tests/smoke.harness.ts', knownPlatformIds)).toBeNull();
|
|
13
|
+
expect(getHarnessTestFilePlatform('/tests/smoke.harness.tsx', knownPlatformIds)).toBeNull();
|
|
14
|
+
expect(getHarnessTestFilePlatform('/tests/smoke.harness.mjs', knownPlatformIds)).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns the platform id for platform-specific harness test files', () => {
|
|
18
|
+
expect(
|
|
19
|
+
getHarnessTestFilePlatform('/tests/kotlin.android.harness.ts', knownPlatformIds),
|
|
20
|
+
).toBe('android');
|
|
21
|
+
expect(
|
|
22
|
+
getHarnessTestFilePlatform('/tests/swift.ios.harness.ts', knownPlatformIds),
|
|
23
|
+
).toBe('ios');
|
|
24
|
+
expect(
|
|
25
|
+
getHarnessTestFilePlatform('/tests/browser.web.harness.ts', knownPlatformIds),
|
|
26
|
+
).toBe('web');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns null when the segment before .harness is not a known platform id', () => {
|
|
30
|
+
expect(
|
|
31
|
+
getHarnessTestFilePlatform('/tests/smoke.harness.ts', knownPlatformIds),
|
|
32
|
+
).toBeNull();
|
|
33
|
+
expect(
|
|
34
|
+
getHarnessTestFilePlatform('/tests/custom.foo.harness.ts', knownPlatformIds),
|
|
35
|
+
).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('shouldRunHarnessTestFile', () => {
|
|
40
|
+
it('runs shared harness test files on every platform', () => {
|
|
41
|
+
expect(
|
|
42
|
+
shouldRunHarnessTestFile('/tests/smoke.harness.ts', 'android', knownPlatformIds),
|
|
43
|
+
).toBe(true);
|
|
44
|
+
expect(
|
|
45
|
+
shouldRunHarnessTestFile('/tests/smoke.harness.ts', 'ios', knownPlatformIds),
|
|
46
|
+
).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('runs platform-specific files only on the matching platform', () => {
|
|
50
|
+
expect(
|
|
51
|
+
shouldRunHarnessTestFile('/tests/kotlin.android.harness.ts', 'android', knownPlatformIds),
|
|
52
|
+
).toBe(true);
|
|
53
|
+
expect(
|
|
54
|
+
shouldRunHarnessTestFile('/tests/kotlin.android.harness.ts', 'ios', knownPlatformIds),
|
|
55
|
+
).toBe(false);
|
|
56
|
+
expect(
|
|
57
|
+
shouldRunHarnessTestFile('/tests/swift.ios.harness.ts', 'ios', knownPlatformIds),
|
|
58
|
+
).toBe(true);
|
|
59
|
+
expect(
|
|
60
|
+
shouldRunHarnessTestFile('/tests/swift.ios.harness.ts', 'android', knownPlatformIds),
|
|
61
|
+
).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('createPlatformSkippedTestResult', () => {
|
|
66
|
+
it('marks the entire test file as skipped', () => {
|
|
67
|
+
const result = createPlatformSkippedTestResult('/tests/swift.ios.harness.ts');
|
|
68
|
+
|
|
69
|
+
expect(result.skipped).toBe(true);
|
|
70
|
+
expect(result.numPassingTests).toBe(0);
|
|
71
|
+
expect(result.numFailingTests).toBe(0);
|
|
72
|
+
expect(result.numPendingTests).toBe(1);
|
|
73
|
+
expect(result.testResults).toHaveLength(1);
|
|
74
|
+
expect(result.testResults[0]).toEqual(
|
|
75
|
+
expect.objectContaining({
|
|
76
|
+
status: 'skipped',
|
|
77
|
+
title: 'swift.ios.harness.ts',
|
|
78
|
+
fullName: 'swift.ios.harness.ts',
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
expect(result.testFilePath).toBe('/tests/swift.ios.harness.ts');
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/crash-monitor.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type AppMonitor,
|
|
3
2
|
type AppCrashDetails,
|
|
4
|
-
type
|
|
5
|
-
type
|
|
6
|
-
type
|
|
3
|
+
type AppSession,
|
|
4
|
+
type AppSessionEvent,
|
|
5
|
+
type AppSessionListener,
|
|
6
|
+
type AppSessionLog,
|
|
7
|
+
type AppSessionState,
|
|
7
8
|
} from '@react-native-harness/platforms';
|
|
8
9
|
import {
|
|
9
10
|
NativeCrashError,
|
|
11
|
+
RuntimeDisconnectError,
|
|
12
|
+
type HarnessRuntimeFailure,
|
|
10
13
|
type NativeCrashDetails,
|
|
11
14
|
type NativeCrashPhase,
|
|
15
|
+
type RuntimeDisconnectDetails,
|
|
12
16
|
} from './errors.js';
|
|
13
17
|
import { logger } from '@react-native-harness/tools';
|
|
14
18
|
|
|
15
19
|
const crashLogger = logger.child('crash');
|
|
20
|
+
const CRASH_CLASSIFICATION_SETTLE_MS = 1500;
|
|
21
|
+
const CRASH_LOG_WINDOW_MS = 3000;
|
|
16
22
|
|
|
17
23
|
export class CrashWatchCancelledError extends Error {
|
|
18
24
|
constructor() {
|
|
@@ -32,172 +38,228 @@ export type CrashMonitor = {
|
|
|
32
38
|
stop: () => Promise<void>;
|
|
33
39
|
start: () => Promise<void>;
|
|
34
40
|
reset: () => void;
|
|
41
|
+
setAppSession: (session: AppSession | null) => void;
|
|
42
|
+
handleBridgeDisconnect: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* Opens a window during which handleBridgeDisconnect() is suppressed,
|
|
45
|
+
* for use around operations (like a runtime reset) that intentionally
|
|
46
|
+
* disconnect the bridge without the app process dying. Returns a closer
|
|
47
|
+
* that ends the window; safe to call multiple times or nest.
|
|
48
|
+
*/
|
|
49
|
+
expectDisconnect: () => () => void;
|
|
35
50
|
dispose: () => Promise<void>;
|
|
36
51
|
};
|
|
37
52
|
|
|
38
53
|
export type CrashMonitorOptions = {
|
|
39
|
-
|
|
40
|
-
platformRunner: HarnessPlatformRunner;
|
|
54
|
+
appSession?: AppSession | null;
|
|
41
55
|
};
|
|
42
56
|
|
|
43
|
-
type
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
occurredAt: number;
|
|
48
|
-
}) => Promise<AppCrashDetails | null>;
|
|
57
|
+
type PendingCrash = {
|
|
58
|
+
testFilePath: string;
|
|
59
|
+
phase: NativeCrashPhase;
|
|
60
|
+
occurredAt: number;
|
|
49
61
|
};
|
|
50
62
|
|
|
51
|
-
const
|
|
63
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
64
|
+
|
|
65
|
+
const isCrashIndicator = (line: string) =>
|
|
66
|
+
/uncaught exception|terminating app due to|fatal error|EXC_[A-Z_]+|termination reason|crash|abort/i.test(
|
|
67
|
+
line
|
|
68
|
+
) || /\bSIG[A-Z]{2,}\b/.test(line);
|
|
69
|
+
|
|
70
|
+
const getMatchingCrashLines = (
|
|
71
|
+
logs: AppSessionLog[],
|
|
72
|
+
occurredAt: number
|
|
73
|
+
): string[] =>
|
|
74
|
+
logs
|
|
75
|
+
.filter(
|
|
76
|
+
(log) => Math.abs(log.occurredAt - occurredAt) <= CRASH_LOG_WINDOW_MS
|
|
77
|
+
)
|
|
78
|
+
.map((log) => log.line)
|
|
79
|
+
.filter(isCrashIndicator);
|
|
80
|
+
|
|
81
|
+
const buildNativeCrashDetails = (
|
|
52
82
|
phase: NativeCrashPhase,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
fallbackSummary?: string,
|
|
83
|
+
rawLines: string[],
|
|
84
|
+
summary: string
|
|
56
85
|
): NativeCrashDetails => ({
|
|
57
86
|
phase,
|
|
58
|
-
source:
|
|
59
|
-
summary:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
source: rawLines.length > 0 ? 'logs' : 'bridge',
|
|
88
|
+
summary: rawLines.length > 0 ? rawLines.join('\n') : summary,
|
|
89
|
+
rawLines: rawLines.length > 0 ? rawLines : undefined,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const getStatePid = (state: AppSessionState | undefined) => {
|
|
93
|
+
if (state && 'pid' in state) {
|
|
94
|
+
return state.pid;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return undefined;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const mergeCrashDetails = (
|
|
101
|
+
fallback: NativeCrashDetails,
|
|
102
|
+
extracted: AppCrashDetails | null
|
|
103
|
+
): NativeCrashDetails => {
|
|
104
|
+
if (!extracted) {
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
...fallback,
|
|
110
|
+
...extracted,
|
|
111
|
+
phase: fallback.phase,
|
|
112
|
+
source: extracted.source ?? fallback.source,
|
|
113
|
+
summary: extracted.summary ?? fallback.summary,
|
|
114
|
+
rawLines: extracted.rawLines ?? fallback.rawLines,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const buildRuntimeDisconnectDetails = (
|
|
119
|
+
phase: NativeCrashPhase,
|
|
120
|
+
rawLines: string[]
|
|
121
|
+
): RuntimeDisconnectDetails => ({
|
|
122
|
+
phase,
|
|
123
|
+
source: 'bridge',
|
|
124
|
+
summary:
|
|
125
|
+
'The runtime bridge disconnected, but the app session still appears to be running.',
|
|
126
|
+
rawLines: rawLines.length > 0 ? rawLines : undefined,
|
|
68
127
|
});
|
|
69
128
|
|
|
70
129
|
export const createCrashMonitor = ({
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}: CrashMonitorOptions): CrashMonitor => {
|
|
130
|
+
appSession: initialAppSession = null,
|
|
131
|
+
}: CrashMonitorOptions = {}): CrashMonitor => {
|
|
74
132
|
let alive = false;
|
|
75
133
|
let monitoring = true;
|
|
76
134
|
let isResolvingCrash = false;
|
|
77
135
|
let disposed = false;
|
|
136
|
+
let appSession: AppSession | null = null;
|
|
137
|
+
let pendingTimer: NodeJS.Timeout | null = null;
|
|
78
138
|
|
|
79
|
-
// Both updated when watch() is called so crashes are attributed to the
|
|
80
|
-
// correct test file and lifecycle phase.
|
|
81
139
|
let currentTestFilePath = '';
|
|
82
140
|
let currentPhase: NativeCrashPhase = 'startup';
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const getCrashDetailsProvider = (): CrashDetailsProvider | null => {
|
|
86
|
-
if ('getCrashDetails' in appMonitor) {
|
|
87
|
-
return appMonitor as AppMonitor & CrashDetailsProvider;
|
|
88
|
-
}
|
|
89
|
-
if (platformRunner.getCrashDetails) {
|
|
90
|
-
return platformRunner;
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
};
|
|
141
|
+
let expectedDisconnectDepth = 0;
|
|
142
|
+
const watchers = new Set<(err: HarnessRuntimeFailure) => void>();
|
|
94
143
|
|
|
95
|
-
const
|
|
144
|
+
const notifyFailure = (err: HarnessRuntimeFailure) => {
|
|
96
145
|
const pending = [...watchers];
|
|
97
146
|
watchers.clear();
|
|
98
147
|
for (const fn of pending) fn(err);
|
|
99
148
|
};
|
|
100
149
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
) => {
|
|
106
|
-
if (isResolvingCrash) return;
|
|
107
|
-
isResolvingCrash = true;
|
|
108
|
-
alive = false;
|
|
109
|
-
|
|
110
|
-
crashLogger.debug('native crash detected (phase=%s)', phase);
|
|
111
|
-
for (const line of details?.rawLines ?? []) {
|
|
112
|
-
crashLogger.debug('%s', line);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
const enriched = await getCrashDetailsProvider()?.getCrashDetails?.({
|
|
117
|
-
processName: details?.processName,
|
|
118
|
-
pid: details?.pid,
|
|
119
|
-
occurredAt: Date.now(),
|
|
120
|
-
});
|
|
121
|
-
const merged = mergeCrashDetails(phase, details, enriched, fallbackSummary);
|
|
122
|
-
crashLogger.debug('crash details: %o', {
|
|
123
|
-
phase: merged.phase,
|
|
124
|
-
source: merged.source,
|
|
125
|
-
summary: merged.summary,
|
|
126
|
-
signal: merged.signal,
|
|
127
|
-
exceptionType: merged.exceptionType,
|
|
128
|
-
processName: merged.processName,
|
|
129
|
-
pid: merged.pid,
|
|
130
|
-
});
|
|
131
|
-
notifyCrash(new NativeCrashError(currentTestFilePath, merged));
|
|
132
|
-
} finally {
|
|
133
|
-
isResolvingCrash = false;
|
|
150
|
+
const clearPendingTimer = () => {
|
|
151
|
+
if (pendingTimer) {
|
|
152
|
+
clearTimeout(pendingTimer);
|
|
153
|
+
pendingTimer = null;
|
|
134
154
|
}
|
|
135
155
|
};
|
|
136
156
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
fallbackSummary?: string,
|
|
157
|
+
const classify = async (
|
|
158
|
+
pending: PendingCrash,
|
|
159
|
+
trigger: 'bridge-disconnect' | 'app-exit'
|
|
141
160
|
) => {
|
|
142
|
-
if (disposed || !monitoring)
|
|
161
|
+
if (disposed || !monitoring || isResolvingCrash) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
isResolvingCrash = true;
|
|
166
|
+
|
|
143
167
|
try {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
const session = appSession;
|
|
169
|
+
const state = await session?.getState();
|
|
170
|
+
const logs = session?.getLogs() ?? [];
|
|
171
|
+
const rawLines = getMatchingCrashLines(logs, pending.occurredAt);
|
|
172
|
+
|
|
173
|
+
if (state?.status === 'running' && trigger === 'bridge-disconnect') {
|
|
174
|
+
crashLogger.debug(
|
|
175
|
+
'runtime bridge disconnected without confirmed app death'
|
|
176
|
+
);
|
|
177
|
+
notifyFailure(
|
|
178
|
+
new RuntimeDisconnectError(
|
|
179
|
+
pending.testFilePath,
|
|
180
|
+
buildRuntimeDisconnectDetails(pending.phase, rawLines)
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
return;
|
|
147
184
|
}
|
|
148
|
-
|
|
149
|
-
|
|
185
|
+
|
|
186
|
+
alive = false;
|
|
187
|
+
crashLogger.debug('native crash detected (phase=%s)', pending.phase);
|
|
188
|
+
for (const line of rawLines) {
|
|
189
|
+
crashLogger.debug('%s', line);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const fallbackSummary =
|
|
193
|
+
trigger === 'bridge-disconnect'
|
|
194
|
+
? 'The app process exited after the runtime bridge disconnected, but no crash log lines were found.'
|
|
195
|
+
: 'The app process exited, but no crash log lines were found.';
|
|
196
|
+
const details = buildNativeCrashDetails(
|
|
197
|
+
pending.phase,
|
|
198
|
+
rawLines,
|
|
199
|
+
fallbackSummary
|
|
200
|
+
);
|
|
201
|
+
const extractedDetails = session?.getCrashDetails
|
|
202
|
+
? await session
|
|
203
|
+
.getCrashDetails({
|
|
204
|
+
occurredAt: pending.occurredAt,
|
|
205
|
+
pid: getStatePid(state),
|
|
206
|
+
processName: details.processName,
|
|
207
|
+
testFilePath: pending.testFilePath,
|
|
208
|
+
})
|
|
209
|
+
.catch((error) => {
|
|
210
|
+
crashLogger.warn(
|
|
211
|
+
'failed to extract native crash details: %s',
|
|
212
|
+
error
|
|
213
|
+
);
|
|
214
|
+
return null;
|
|
215
|
+
})
|
|
216
|
+
: null;
|
|
217
|
+
notifyFailure(
|
|
218
|
+
new NativeCrashError(
|
|
219
|
+
pending.testFilePath,
|
|
220
|
+
mergeCrashDetails(details, extractedDetails)
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
} finally {
|
|
224
|
+
isResolvingCrash = false;
|
|
225
|
+
pendingTimer = null;
|
|
150
226
|
}
|
|
151
227
|
};
|
|
152
228
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
): AppCrashDetails | undefined =>
|
|
156
|
-
event.crashDetails
|
|
157
|
-
? {
|
|
158
|
-
source: event.crashDetails.source ?? event.source,
|
|
159
|
-
summary: event.crashDetails.summary,
|
|
160
|
-
signal: event.crashDetails.signal,
|
|
161
|
-
exceptionType: event.crashDetails.exceptionType,
|
|
162
|
-
processName: event.crashDetails.processName,
|
|
163
|
-
pid: event.crashDetails.pid ?? event.pid,
|
|
164
|
-
stackTrace: event.crashDetails.stackTrace,
|
|
165
|
-
rawLines:
|
|
166
|
-
event.crashDetails.rawLines ??
|
|
167
|
-
(event.line ? [event.line] : undefined),
|
|
168
|
-
}
|
|
169
|
-
: undefined;
|
|
170
|
-
|
|
171
|
-
const appMonitorListener: AppMonitorListener = (event: AppMonitorEvent) => {
|
|
172
|
-
if (disposed || !monitoring) return;
|
|
173
|
-
|
|
174
|
-
if (event.type === 'app_started') {
|
|
175
|
-
alive = true;
|
|
229
|
+
const startCrashResolution = (trigger: 'bridge-disconnect' | 'app-exit') => {
|
|
230
|
+
if (disposed || !monitoring || isResolvingCrash) {
|
|
176
231
|
return;
|
|
177
232
|
}
|
|
178
233
|
|
|
234
|
+
clearPendingTimer();
|
|
235
|
+
const pending: PendingCrash = {
|
|
236
|
+
testFilePath: currentTestFilePath,
|
|
237
|
+
phase: currentPhase,
|
|
238
|
+
occurredAt: Date.now(),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
pendingTimer = setTimeout(
|
|
242
|
+
() => {
|
|
243
|
+
void classify(pending, trigger);
|
|
244
|
+
},
|
|
245
|
+
trigger === 'bridge-disconnect' ? CRASH_CLASSIFICATION_SETTLE_MS : 0
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const appSessionListener: AppSessionListener = (event: AppSessionEvent) => {
|
|
179
250
|
if (event.type === 'app_exited') {
|
|
180
|
-
|
|
181
|
-
if (event.isConfirmed ?? event.source === 'polling') {
|
|
182
|
-
void handleCrash(currentPhase, details);
|
|
183
|
-
} else {
|
|
184
|
-
void confirmAndHandleCrash(currentPhase, details);
|
|
185
|
-
}
|
|
186
|
-
return;
|
|
251
|
+
startCrashResolution('app-exit');
|
|
187
252
|
}
|
|
253
|
+
};
|
|
188
254
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
} else {
|
|
195
|
-
void confirmAndHandleCrash(currentPhase, details, fallback);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
255
|
+
const setAppSession = (session: AppSession | null) => {
|
|
256
|
+
appSession?.removeListener(appSessionListener);
|
|
257
|
+
appSession = session;
|
|
258
|
+
alive = Boolean(session);
|
|
259
|
+
session?.addListener(appSessionListener);
|
|
198
260
|
};
|
|
199
261
|
|
|
200
|
-
|
|
262
|
+
setAppSession(initialAppSession);
|
|
201
263
|
|
|
202
264
|
const watch = (testFilePath: string, phase: NativeCrashPhase): CrashWatch => {
|
|
203
265
|
currentTestFilePath = testFilePath;
|
|
@@ -219,30 +281,57 @@ export const createCrashMonitor = ({
|
|
|
219
281
|
return { promise, cancel };
|
|
220
282
|
};
|
|
221
283
|
|
|
284
|
+
const expectDisconnect = (): (() => void) => {
|
|
285
|
+
expectedDisconnectDepth += 1;
|
|
286
|
+
let closed = false;
|
|
287
|
+
|
|
288
|
+
return () => {
|
|
289
|
+
if (closed) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
closed = true;
|
|
293
|
+
expectedDisconnectDepth = Math.max(0, expectedDisconnectDepth - 1);
|
|
294
|
+
};
|
|
295
|
+
};
|
|
296
|
+
|
|
222
297
|
return {
|
|
223
298
|
watch,
|
|
224
299
|
isAlive: () => alive,
|
|
225
300
|
stop: async () => {
|
|
226
301
|
monitoring = false;
|
|
227
|
-
|
|
302
|
+
clearPendingTimer();
|
|
303
|
+
await sleep(0);
|
|
228
304
|
},
|
|
229
305
|
start: async () => {
|
|
230
306
|
monitoring = true;
|
|
231
|
-
await appMonitor.start();
|
|
232
307
|
},
|
|
233
308
|
reset: () => {
|
|
234
|
-
alive =
|
|
309
|
+
alive = Boolean(appSession);
|
|
235
310
|
watchers.clear();
|
|
236
311
|
isResolvingCrash = false;
|
|
237
312
|
currentTestFilePath = '';
|
|
313
|
+
expectedDisconnectDepth = 0;
|
|
314
|
+
clearPendingTimer();
|
|
238
315
|
},
|
|
316
|
+
setAppSession,
|
|
317
|
+
handleBridgeDisconnect: () => {
|
|
318
|
+
if (expectedDisconnectDepth > 0) {
|
|
319
|
+
crashLogger.debug('ignoring expected bridge disconnect');
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
startCrashResolution('bridge-disconnect');
|
|
323
|
+
},
|
|
324
|
+
expectDisconnect,
|
|
239
325
|
dispose: async () => {
|
|
240
326
|
disposed = true;
|
|
241
327
|
monitoring = false;
|
|
328
|
+
expectedDisconnectDepth = 0;
|
|
242
329
|
watchers.clear();
|
|
243
330
|
isResolvingCrash = false;
|
|
244
|
-
|
|
245
|
-
|
|
331
|
+
clearPendingTimer();
|
|
332
|
+
appSession?.removeListener(appSessionListener);
|
|
333
|
+
appSession = null;
|
|
334
|
+
alive = false;
|
|
246
335
|
},
|
|
247
336
|
};
|
|
248
337
|
};
|