@react-native-harness/jest 1.3.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__/harness-session.test.js +63 -239
- 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
|
@@ -8,6 +8,7 @@ const resolveUndefined = async () => undefined;
|
|
|
8
8
|
const mockSession = {
|
|
9
9
|
config: { metroPort: 8081 },
|
|
10
10
|
context: {},
|
|
11
|
+
onTestRunnerEvent: vi.fn(() => () => undefined),
|
|
11
12
|
ensureAppReady: vi.fn(resolveUndefined),
|
|
12
13
|
runTestFile: vi.fn(async () => ({
|
|
13
14
|
name: '',
|
|
@@ -41,40 +42,88 @@ const makeTest = () => ({
|
|
|
41
42
|
context: { config: {} },
|
|
42
43
|
});
|
|
43
44
|
const makeGlobalConfig = (overrides = {}) => ({ rootDir: '/project', watch: false, watchAll: false, collectCoverage: false, ...overrides });
|
|
45
|
+
const typedMockExecuteRun = mockExecuteRun;
|
|
46
|
+
const makeTestCaseStartPayload = () => [
|
|
47
|
+
'/test/example.ts',
|
|
48
|
+
{
|
|
49
|
+
ancestorTitles: [],
|
|
50
|
+
fullName: 'suite case',
|
|
51
|
+
mode: undefined,
|
|
52
|
+
title: 'case',
|
|
53
|
+
startedAt: 1,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const makeTestCaseResultPayload = () => [
|
|
57
|
+
'/test/example.ts',
|
|
58
|
+
{
|
|
59
|
+
ancestorTitles: [],
|
|
60
|
+
duration: 1,
|
|
61
|
+
failureDetails: [],
|
|
62
|
+
failureMessages: [],
|
|
63
|
+
fullName: 'suite case',
|
|
64
|
+
numPassingAsserts: 1,
|
|
65
|
+
startedAt: 1,
|
|
66
|
+
status: 'passed',
|
|
67
|
+
title: 'case',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
44
70
|
// ---------------------------------------------------------------------------
|
|
45
71
|
// Tests
|
|
46
72
|
// ---------------------------------------------------------------------------
|
|
47
73
|
beforeEach(() => {
|
|
48
74
|
vi.clearAllMocks();
|
|
49
75
|
mockSession.dispose.mockResolvedValue(undefined);
|
|
50
|
-
|
|
76
|
+
typedMockExecuteRun.mockResolvedValue(undefined);
|
|
51
77
|
mockCreateHarnessSession.mockResolvedValue(mockSession);
|
|
52
78
|
});
|
|
53
79
|
describe('JestHarness', () => {
|
|
54
80
|
describe('session lifecycle', () => {
|
|
55
81
|
it('creates a session on the first run', async () => {
|
|
56
82
|
const runner = new JestHarness(makeGlobalConfig());
|
|
57
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
83
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
58
84
|
expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
|
|
59
85
|
});
|
|
60
86
|
it('reuses the session across runs in watch mode', async () => {
|
|
61
87
|
const runner = new JestHarness(makeGlobalConfig({ watch: true }));
|
|
62
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
63
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
88
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
89
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
64
90
|
expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
|
|
65
91
|
expect(mockSession.dispose).not.toHaveBeenCalled();
|
|
66
92
|
});
|
|
67
93
|
it('disposes the session after each run in normal mode', async () => {
|
|
68
94
|
const runner = new JestHarness(makeGlobalConfig());
|
|
69
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
95
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
70
96
|
expect(mockSession.dispose).toHaveBeenCalledOnce();
|
|
71
97
|
});
|
|
72
98
|
it('creates a fresh session for each run in normal mode', async () => {
|
|
73
99
|
const runner = new JestHarness(makeGlobalConfig());
|
|
74
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
75
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
100
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
101
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
76
102
|
expect(mockCreateHarnessSession).toHaveBeenCalledTimes(2);
|
|
77
103
|
});
|
|
104
|
+
it('emits registered Jest events', async () => {
|
|
105
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
106
|
+
const listener = vi.fn();
|
|
107
|
+
const eventPayload = makeTestCaseStartPayload();
|
|
108
|
+
runner.on('test-case-start', listener);
|
|
109
|
+
typedMockExecuteRun.mockImplementationOnce(async (_session, _tests, _watcher, emitEvent) => {
|
|
110
|
+
await emitEvent('test-case-start', ...eventPayload);
|
|
111
|
+
});
|
|
112
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
113
|
+
expect(listener).toHaveBeenCalledWith(eventPayload);
|
|
114
|
+
});
|
|
115
|
+
it('stops emitting after listener unsubscribe', async () => {
|
|
116
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
117
|
+
const listener = vi.fn();
|
|
118
|
+
const unsubscribe = runner.on('test-case-result', listener);
|
|
119
|
+
const eventPayload = makeTestCaseResultPayload();
|
|
120
|
+
unsubscribe();
|
|
121
|
+
typedMockExecuteRun.mockImplementationOnce(async (_session, _tests, _watcher, emitEvent) => {
|
|
122
|
+
await emitEvent('test-case-result', ...eventPayload);
|
|
123
|
+
});
|
|
124
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
125
|
+
expect(listener).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
78
127
|
});
|
|
79
128
|
describe('error handling', () => {
|
|
80
129
|
it('converts HarnessError into a formatted string before throwing', async () => {
|
|
@@ -83,17 +132,17 @@ describe('JestHarness', () => {
|
|
|
83
132
|
}
|
|
84
133
|
mockCreateHarnessSession.mockRejectedValue(new TestHarnessError());
|
|
85
134
|
const runner = new JestHarness(makeGlobalConfig());
|
|
86
|
-
await expect(runner.runTests([makeTest()], makeWatcher(),
|
|
135
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), makeOptions())).rejects.toBeTypeOf('string');
|
|
87
136
|
});
|
|
88
137
|
it('propagates non-HarnessError exceptions as-is', async () => {
|
|
89
138
|
const cause = new TypeError('unexpected');
|
|
90
139
|
mockCreateHarnessSession.mockRejectedValue(cause);
|
|
91
140
|
const runner = new JestHarness(makeGlobalConfig());
|
|
92
|
-
await expect(runner.runTests([makeTest()], makeWatcher(),
|
|
141
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), makeOptions())).rejects.toBe(cause);
|
|
93
142
|
});
|
|
94
143
|
it('throws when called without serial flag', async () => {
|
|
95
144
|
const runner = new JestHarness(makeGlobalConfig());
|
|
96
|
-
await expect(runner.runTests([makeTest()], makeWatcher(),
|
|
145
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), { serial: false })).rejects.toThrow('Parallel test running is not supported');
|
|
97
146
|
});
|
|
98
147
|
});
|
|
99
148
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-file-platform-filter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/test-file-platform-filter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createPlatformSkippedTestResult, getHarnessTestFilePlatform, shouldRunHarnessTestFile, } from '../test-file-platform-filter.js';
|
|
3
|
+
const knownPlatformIds = new Set(['android', 'ios', 'web', 'vega']);
|
|
4
|
+
describe('getHarnessTestFilePlatform', () => {
|
|
5
|
+
it('returns null for shared harness test files', () => {
|
|
6
|
+
expect(getHarnessTestFilePlatform('/tests/smoke.harness.ts', knownPlatformIds)).toBeNull();
|
|
7
|
+
expect(getHarnessTestFilePlatform('/tests/smoke.harness.tsx', knownPlatformIds)).toBeNull();
|
|
8
|
+
expect(getHarnessTestFilePlatform('/tests/smoke.harness.mjs', knownPlatformIds)).toBeNull();
|
|
9
|
+
});
|
|
10
|
+
it('returns the platform id for platform-specific harness test files', () => {
|
|
11
|
+
expect(getHarnessTestFilePlatform('/tests/kotlin.android.harness.ts', knownPlatformIds)).toBe('android');
|
|
12
|
+
expect(getHarnessTestFilePlatform('/tests/swift.ios.harness.ts', knownPlatformIds)).toBe('ios');
|
|
13
|
+
expect(getHarnessTestFilePlatform('/tests/browser.web.harness.ts', knownPlatformIds)).toBe('web');
|
|
14
|
+
});
|
|
15
|
+
it('returns null when the segment before .harness is not a known platform id', () => {
|
|
16
|
+
expect(getHarnessTestFilePlatform('/tests/smoke.harness.ts', knownPlatformIds)).toBeNull();
|
|
17
|
+
expect(getHarnessTestFilePlatform('/tests/custom.foo.harness.ts', knownPlatformIds)).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('shouldRunHarnessTestFile', () => {
|
|
21
|
+
it('runs shared harness test files on every platform', () => {
|
|
22
|
+
expect(shouldRunHarnessTestFile('/tests/smoke.harness.ts', 'android', knownPlatformIds)).toBe(true);
|
|
23
|
+
expect(shouldRunHarnessTestFile('/tests/smoke.harness.ts', 'ios', knownPlatformIds)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it('runs platform-specific files only on the matching platform', () => {
|
|
26
|
+
expect(shouldRunHarnessTestFile('/tests/kotlin.android.harness.ts', 'android', knownPlatformIds)).toBe(true);
|
|
27
|
+
expect(shouldRunHarnessTestFile('/tests/kotlin.android.harness.ts', 'ios', knownPlatformIds)).toBe(false);
|
|
28
|
+
expect(shouldRunHarnessTestFile('/tests/swift.ios.harness.ts', 'ios', knownPlatformIds)).toBe(true);
|
|
29
|
+
expect(shouldRunHarnessTestFile('/tests/swift.ios.harness.ts', 'android', knownPlatformIds)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('createPlatformSkippedTestResult', () => {
|
|
33
|
+
it('marks the entire test file as skipped', () => {
|
|
34
|
+
const result = createPlatformSkippedTestResult('/tests/swift.ios.harness.ts');
|
|
35
|
+
expect(result.skipped).toBe(true);
|
|
36
|
+
expect(result.numPassingTests).toBe(0);
|
|
37
|
+
expect(result.numFailingTests).toBe(0);
|
|
38
|
+
expect(result.numPendingTests).toBe(1);
|
|
39
|
+
expect(result.testResults).toHaveLength(1);
|
|
40
|
+
expect(result.testResults[0]).toEqual(expect.objectContaining({
|
|
41
|
+
status: 'skipped',
|
|
42
|
+
title: 'swift.ios.harness.ts',
|
|
43
|
+
fullName: 'swift.ios.harness.ts',
|
|
44
|
+
}));
|
|
45
|
+
expect(result.testFilePath).toBe('/tests/swift.ios.harness.ts');
|
|
46
|
+
});
|
|
47
|
+
});
|
package/dist/crash-monitor.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type AppSession } from '@react-native-harness/platforms';
|
|
2
2
|
import { type NativeCrashPhase } from './errors.js';
|
|
3
3
|
export declare class CrashWatchCancelledError extends Error {
|
|
4
4
|
constructor();
|
|
@@ -13,11 +13,12 @@ export type CrashMonitor = {
|
|
|
13
13
|
stop: () => Promise<void>;
|
|
14
14
|
start: () => Promise<void>;
|
|
15
15
|
reset: () => void;
|
|
16
|
+
setAppSession: (session: AppSession | null) => void;
|
|
17
|
+
handleBridgeDisconnect: () => void;
|
|
16
18
|
dispose: () => Promise<void>;
|
|
17
19
|
};
|
|
18
20
|
export type CrashMonitorOptions = {
|
|
19
|
-
|
|
20
|
-
platformRunner: HarnessPlatformRunner;
|
|
21
|
+
appSession?: AppSession | null;
|
|
21
22
|
};
|
|
22
|
-
export declare const createCrashMonitor: ({
|
|
23
|
+
export declare const createCrashMonitor: ({ appSession: initialAppSession, }?: CrashMonitorOptions) => CrashMonitor;
|
|
23
24
|
//# sourceMappingURL=crash-monitor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAKhB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,aAAa,CAAC;AAOrB,qBAAa,wBAAyB,SAAQ,KAAK;;CAKlD;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,KAAK,UAAU,CAAC;IACrE,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,aAAa,EAAE,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;IACpD,sBAAsB,EAAE,MAAM,IAAI,CAAC;IACnC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC,CAAC;AA0EF,eAAO,MAAM,kBAAkB,GAAI,qCAEhC,mBAAwB,KAAG,YAyL7B,CAAC"}
|
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"}
|