@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,48 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { runHarnessTestFile } from '../run.js';
|
|
3
|
+
vi.mock('jest-message-util', () => ({
|
|
4
|
+
formatResultsErrors: vi.fn(() => ''),
|
|
5
|
+
}));
|
|
6
|
+
const createHarnessResult = () => ({
|
|
7
|
+
name: 'root',
|
|
8
|
+
tests: [],
|
|
9
|
+
suites: [],
|
|
10
|
+
status: 'passed',
|
|
11
|
+
duration: 0,
|
|
12
|
+
});
|
|
13
|
+
const createSession = (testTimeout) => ({
|
|
14
|
+
config: { testTimeout },
|
|
15
|
+
context: {
|
|
16
|
+
platform: {
|
|
17
|
+
runner: './runner.js',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
runTestFile: vi.fn(async () => createHarnessResult()),
|
|
21
|
+
});
|
|
22
|
+
const createGlobalConfig = (overrides = {}) => ({
|
|
23
|
+
rootDir: '/project',
|
|
24
|
+
...overrides,
|
|
25
|
+
});
|
|
26
|
+
const createProjectConfig = (overrides = {}) => overrides;
|
|
27
|
+
describe('runHarnessTestFile', () => {
|
|
28
|
+
it('uses Harness config testTimeout when Jest config does not set one', async () => {
|
|
29
|
+
const session = createSession(15000);
|
|
30
|
+
await runHarnessTestFile({
|
|
31
|
+
testPath: '/project/example.harness.ts',
|
|
32
|
+
session,
|
|
33
|
+
globalConfig: createGlobalConfig(),
|
|
34
|
+
projectConfig: createProjectConfig(),
|
|
35
|
+
});
|
|
36
|
+
expect(session.runTestFile).toHaveBeenCalledWith('example.harness.ts', expect.objectContaining({ testTimeout: 15000 }));
|
|
37
|
+
});
|
|
38
|
+
it('keeps Harness config testTimeout above Jest config testTimeout', async () => {
|
|
39
|
+
const session = createSession(15000);
|
|
40
|
+
await runHarnessTestFile({
|
|
41
|
+
testPath: '/project/example.harness.ts',
|
|
42
|
+
session,
|
|
43
|
+
globalConfig: createGlobalConfig({ testTimeout: 20000 }),
|
|
44
|
+
projectConfig: createProjectConfig({ testTimeout: 30000 }),
|
|
45
|
+
});
|
|
46
|
+
expect(session.runTestFile).toHaveBeenCalledWith('example.harness.ts', expect.objectContaining({ testTimeout: 15000 }));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -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,19 @@ 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;
|
|
18
|
+
/**
|
|
19
|
+
* Opens a window during which handleBridgeDisconnect() is suppressed,
|
|
20
|
+
* for use around operations (like a runtime reset) that intentionally
|
|
21
|
+
* disconnect the bridge without the app process dying. Returns a closer
|
|
22
|
+
* that ends the window; safe to call multiple times or nest.
|
|
23
|
+
*/
|
|
24
|
+
expectDisconnect: () => () => void;
|
|
16
25
|
dispose: () => Promise<void>;
|
|
17
26
|
};
|
|
18
27
|
export type CrashMonitorOptions = {
|
|
19
|
-
|
|
20
|
-
platformRunner: HarnessPlatformRunner;
|
|
28
|
+
appSession?: AppSession | null;
|
|
21
29
|
};
|
|
22
|
-
export declare const createCrashMonitor: ({
|
|
30
|
+
export declare const createCrashMonitor: ({ appSession: initialAppSession, }?: CrashMonitorOptions) => CrashMonitor;
|
|
23
31
|
//# 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;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,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,YA8M7B,CAAC"}
|
package/dist/crash-monitor.js
CHANGED
|
@@ -1,136 +1,144 @@
|
|
|
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';
|
|
58
|
+
let expectedDisconnectDepth = 0;
|
|
32
59
|
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) => {
|
|
60
|
+
const notifyFailure = (err) => {
|
|
43
61
|
const pending = [...watchers];
|
|
44
62
|
watchers.clear();
|
|
45
63
|
for (const fn of pending)
|
|
46
64
|
fn(err);
|
|
47
65
|
};
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
66
|
+
const clearPendingTimer = () => {
|
|
67
|
+
if (pendingTimer) {
|
|
68
|
+
clearTimeout(pendingTimer);
|
|
69
|
+
pendingTimer = null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const classify = async (pending, trigger) => {
|
|
73
|
+
if (disposed || !monitoring || isResolvingCrash) {
|
|
50
74
|
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
75
|
}
|
|
76
|
+
isResolvingCrash = true;
|
|
57
77
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
const session = appSession;
|
|
79
|
+
const state = await session?.getState();
|
|
80
|
+
const logs = session?.getLogs() ?? [];
|
|
81
|
+
const rawLines = getMatchingCrashLines(logs, pending.occurredAt);
|
|
82
|
+
if (state?.status === 'running' && trigger === 'bridge-disconnect') {
|
|
83
|
+
crashLogger.debug('runtime bridge disconnected without confirmed app death');
|
|
84
|
+
notifyFailure(new RuntimeDisconnectError(pending.testFilePath, buildRuntimeDisconnectDetails(pending.phase, rawLines)));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
alive = false;
|
|
88
|
+
crashLogger.debug('native crash detected (phase=%s)', pending.phase);
|
|
89
|
+
for (const line of rawLines) {
|
|
90
|
+
crashLogger.debug('%s', line);
|
|
91
|
+
}
|
|
92
|
+
const fallbackSummary = trigger === 'bridge-disconnect'
|
|
93
|
+
? 'The app process exited after the runtime bridge disconnected, but no crash log lines were found.'
|
|
94
|
+
: 'The app process exited, but no crash log lines were found.';
|
|
95
|
+
const details = buildNativeCrashDetails(pending.phase, rawLines, fallbackSummary);
|
|
96
|
+
const extractedDetails = session?.getCrashDetails
|
|
97
|
+
? await session
|
|
98
|
+
.getCrashDetails({
|
|
99
|
+
occurredAt: pending.occurredAt,
|
|
100
|
+
pid: getStatePid(state),
|
|
101
|
+
processName: details.processName,
|
|
102
|
+
testFilePath: pending.testFilePath,
|
|
103
|
+
})
|
|
104
|
+
.catch((error) => {
|
|
105
|
+
crashLogger.warn('failed to extract native crash details: %s', error);
|
|
106
|
+
return null;
|
|
107
|
+
})
|
|
108
|
+
: null;
|
|
109
|
+
notifyFailure(new NativeCrashError(pending.testFilePath, mergeCrashDetails(details, extractedDetails)));
|
|
74
110
|
}
|
|
75
111
|
finally {
|
|
76
112
|
isResolvingCrash = false;
|
|
113
|
+
pendingTimer = null;
|
|
77
114
|
}
|
|
78
115
|
};
|
|
79
|
-
const
|
|
80
|
-
if (disposed || !monitoring)
|
|
116
|
+
const startCrashResolution = (trigger) => {
|
|
117
|
+
if (disposed || !monitoring || isResolvingCrash) {
|
|
81
118
|
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
119
|
}
|
|
120
|
+
clearPendingTimer();
|
|
121
|
+
const pending = {
|
|
122
|
+
testFilePath: currentTestFilePath,
|
|
123
|
+
phase: currentPhase,
|
|
124
|
+
occurredAt: Date.now(),
|
|
125
|
+
};
|
|
126
|
+
pendingTimer = setTimeout(() => {
|
|
127
|
+
void classify(pending, trigger);
|
|
128
|
+
}, trigger === 'bridge-disconnect' ? CRASH_CLASSIFICATION_SETTLE_MS : 0);
|
|
91
129
|
};
|
|
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
|
-
}
|
|
130
|
+
const appSessionListener = (event) => {
|
|
112
131
|
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
|
-
}
|
|
132
|
+
startCrashResolution('app-exit');
|
|
131
133
|
}
|
|
132
134
|
};
|
|
133
|
-
|
|
135
|
+
const setAppSession = (session) => {
|
|
136
|
+
appSession?.removeListener(appSessionListener);
|
|
137
|
+
appSession = session;
|
|
138
|
+
alive = Boolean(session);
|
|
139
|
+
session?.addListener(appSessionListener);
|
|
140
|
+
};
|
|
141
|
+
setAppSession(initialAppSession);
|
|
134
142
|
const watch = (testFilePath, phase) => {
|
|
135
143
|
currentTestFilePath = testFilePath;
|
|
136
144
|
currentPhase = phase;
|
|
@@ -147,30 +155,55 @@ export const createCrashMonitor = ({ appMonitor, platformRunner, }) => {
|
|
|
147
155
|
};
|
|
148
156
|
return { promise, cancel };
|
|
149
157
|
};
|
|
158
|
+
const expectDisconnect = () => {
|
|
159
|
+
expectedDisconnectDepth += 1;
|
|
160
|
+
let closed = false;
|
|
161
|
+
return () => {
|
|
162
|
+
if (closed) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
closed = true;
|
|
166
|
+
expectedDisconnectDepth = Math.max(0, expectedDisconnectDepth - 1);
|
|
167
|
+
};
|
|
168
|
+
};
|
|
150
169
|
return {
|
|
151
170
|
watch,
|
|
152
171
|
isAlive: () => alive,
|
|
153
172
|
stop: async () => {
|
|
154
173
|
monitoring = false;
|
|
155
|
-
|
|
174
|
+
clearPendingTimer();
|
|
175
|
+
await sleep(0);
|
|
156
176
|
},
|
|
157
177
|
start: async () => {
|
|
158
178
|
monitoring = true;
|
|
159
|
-
await appMonitor.start();
|
|
160
179
|
},
|
|
161
180
|
reset: () => {
|
|
162
|
-
alive =
|
|
181
|
+
alive = Boolean(appSession);
|
|
163
182
|
watchers.clear();
|
|
164
183
|
isResolvingCrash = false;
|
|
165
184
|
currentTestFilePath = '';
|
|
185
|
+
expectedDisconnectDepth = 0;
|
|
186
|
+
clearPendingTimer();
|
|
166
187
|
},
|
|
188
|
+
setAppSession,
|
|
189
|
+
handleBridgeDisconnect: () => {
|
|
190
|
+
if (expectedDisconnectDepth > 0) {
|
|
191
|
+
crashLogger.debug('ignoring expected bridge disconnect');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
startCrashResolution('bridge-disconnect');
|
|
195
|
+
},
|
|
196
|
+
expectDisconnect,
|
|
167
197
|
dispose: async () => {
|
|
168
198
|
disposed = true;
|
|
169
199
|
monitoring = false;
|
|
200
|
+
expectedDisconnectDepth = 0;
|
|
170
201
|
watchers.clear();
|
|
171
202
|
isResolvingCrash = false;
|
|
172
|
-
|
|
173
|
-
|
|
203
|
+
clearPendingTimer();
|
|
204
|
+
appSession?.removeListener(appSessionListener);
|
|
205
|
+
appSession = null;
|
|
206
|
+
alive = false;
|
|
174
207
|
},
|
|
175
208
|
};
|
|
176
209
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derivers.test.d.ts","sourceRoot":"","sources":["../../../src/diagnostics/__tests__/derivers.test.ts"],"names":[],"mappings":""}
|