@react-native-harness/jest 1.6.0 → 1.7.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/cli-args.d.ts +3 -4
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +3 -8
- package/dist/diagnostics/index.d.ts +4 -1
- package/dist/diagnostics/index.d.ts.map +1 -1
- package/dist/diagnostics/index.js +2 -1
- package/dist/diagnostics/report.d.ts +81 -1
- package/dist/diagnostics/report.d.ts.map +1 -1
- package/dist/diagnostics/report.js +225 -3
- package/dist/diagnostics/telemetry.d.ts +42 -0
- package/dist/diagnostics/telemetry.d.ts.map +1 -0
- package/dist/diagnostics/telemetry.js +120 -0
- package/dist/errors.d.ts +8 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +29 -3
- package/dist/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +73 -21
- package/dist/format-harness-error.d.ts.map +1 -1
- package/dist/format-harness-error.js +3 -0
- package/dist/harness-session.d.ts +8 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +106 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/jest-preset.d.ts +3 -0
- package/dist/jest-preset.d.ts.map +1 -1
- package/dist/jest-preset.js +18 -0
- package/dist/relative-path.d.ts +8 -0
- package/dist/relative-path.d.ts.map +1 -0
- package/dist/relative-path.js +8 -0
- package/dist/resource-lock.d.ts.map +1 -1
- package/dist/resource-lock.js +47 -27
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +4 -4
- package/dist/watch-session.d.ts +6 -0
- package/dist/watch-session.d.ts.map +1 -0
- package/dist/watch-session.js +37 -0
- package/package.json +8 -8
- package/src/cli-args.ts +5 -13
- package/src/diagnostics/index.ts +12 -1
- package/src/diagnostics/report.ts +326 -5
- package/src/diagnostics/telemetry.ts +186 -0
- package/src/errors.ts +42 -5
- package/src/execute-run.ts +93 -23
- package/src/format-harness-error.ts +5 -0
- package/src/harness-session.ts +139 -21
- package/src/index.ts +8 -7
- package/src/jest-preset.ts +18 -0
- package/src/relative-path.ts +10 -0
- package/src/resource-lock.ts +64 -33
- package/src/run.ts +4 -4
- package/src/watch-session.ts +65 -0
- package/dist/crash-supervisor.d.ts +0 -21
- package/dist/crash-supervisor.d.ts.map +0 -1
- package/dist/crash-supervisor.js +0 -210
- package/dist/event-mapper.d.ts +0 -16
- package/dist/event-mapper.d.ts.map +0 -1
- package/dist/event-mapper.js +0 -120
- package/dist/global-setup.d.ts +0 -3
- package/dist/global-setup.d.ts.map +0 -1
- package/dist/global-setup.js +0 -60
- package/dist/global-teardown.d.ts +0 -3
- package/dist/global-teardown.d.ts.map +0 -1
- package/dist/global-teardown.js +0 -11
- package/dist/harness-reporter.d.ts +0 -8
- package/dist/harness-reporter.d.ts.map +0 -1
- package/dist/harness-reporter.js +0 -12
- package/dist/harness.d.ts +0 -47
- package/dist/harness.d.ts.map +0 -1
- package/dist/harness.js +0 -619
- package/dist/jest-options.d.ts +0 -2
- package/dist/jest-options.d.ts.map +0 -1
- package/dist/jest-options.js +0 -1
- package/dist/setup.d.ts +0 -3
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js +0 -80
- package/dist/teardown.d.ts +0 -3
- package/dist/teardown.d.ts.map +0 -1
- package/dist/teardown.js +0 -10
- package/dist/types.d.ts +0 -17
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
package/src/resource-lock.ts
CHANGED
|
@@ -89,6 +89,37 @@ const isMissingFileError = (error: unknown): boolean => {
|
|
|
89
89
|
);
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
// On Windows, a file being deleted by another process can briefly fail
|
|
93
|
+
// reads and deletes with EPERM/EBUSY instead of ENOENT.
|
|
94
|
+
const isTransientFsError = (error: unknown): boolean => {
|
|
95
|
+
return (
|
|
96
|
+
error instanceof Error &&
|
|
97
|
+
'code' in error &&
|
|
98
|
+
(error.code === 'EPERM' || error.code === 'EBUSY')
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const TRANSIENT_FS_RETRIES = 5;
|
|
103
|
+
const TRANSIENT_FS_RETRY_DELAY_MS = 20;
|
|
104
|
+
|
|
105
|
+
const retryTransientFsError = async <T>(
|
|
106
|
+
operation: () => Promise<T>,
|
|
107
|
+
): Promise<T> => {
|
|
108
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
109
|
+
try {
|
|
110
|
+
return await operation();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (!isTransientFsError(error) || attempt >= TRANSIENT_FS_RETRIES) {
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await new Promise((resolve) =>
|
|
117
|
+
setTimeout(resolve, TRANSIENT_FS_RETRY_DELAY_MS),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
92
123
|
const isExclusiveCreateError = (error: unknown): boolean => {
|
|
93
124
|
return (
|
|
94
125
|
error instanceof Error &&
|
|
@@ -104,7 +135,9 @@ const ensureLockDirectories = async (paths: LockPaths): Promise<void> => {
|
|
|
104
135
|
|
|
105
136
|
const readJsonFile = async <T>(filePath: string): Promise<T | null> => {
|
|
106
137
|
try {
|
|
107
|
-
const content = await
|
|
138
|
+
const content = await retryTransientFsError(() =>
|
|
139
|
+
fs.readFile(filePath, 'utf8'),
|
|
140
|
+
);
|
|
108
141
|
return JSON.parse(content) as T;
|
|
109
142
|
} catch (error) {
|
|
110
143
|
if (isMissingFileError(error)) {
|
|
@@ -132,7 +165,7 @@ const writeJsonFileAtomic = async <T>(
|
|
|
132
165
|
|
|
133
166
|
const removeFileIfPresent = async (filePath: string): Promise<void> => {
|
|
134
167
|
try {
|
|
135
|
-
await fs.rm(filePath, { force: true });
|
|
168
|
+
await retryTransientFsError(() => fs.rm(filePath, { force: true }));
|
|
136
169
|
} catch (error) {
|
|
137
170
|
if (!isMissingFileError(error)) {
|
|
138
171
|
throw error;
|
|
@@ -338,7 +371,7 @@ export const createResourceLockManager = (
|
|
|
338
371
|
scopedLogger.debug('queued ticket %s for key %s', ticketId, key);
|
|
339
372
|
|
|
340
373
|
let heartbeatTimer: NodeJS.Timeout | null = null;
|
|
341
|
-
let heartbeatInFlight =
|
|
374
|
+
let heartbeatInFlight: Promise<void> | null = null;
|
|
342
375
|
let released = false;
|
|
343
376
|
let didNotifyWait = false;
|
|
344
377
|
const waitStartedAt = Date.now();
|
|
@@ -354,6 +387,10 @@ export const createResourceLockManager = (
|
|
|
354
387
|
heartbeatTimer = null;
|
|
355
388
|
}
|
|
356
389
|
|
|
390
|
+
// Let an in-flight refresh finish so it cannot recreate owner.json
|
|
391
|
+
// after we remove it.
|
|
392
|
+
await heartbeatInFlight;
|
|
393
|
+
|
|
357
394
|
const owner = await readJsonFile<ResourceLockMetadata>(
|
|
358
395
|
paths.ownerFilePath,
|
|
359
396
|
);
|
|
@@ -366,13 +403,7 @@ export const createResourceLockManager = (
|
|
|
366
403
|
};
|
|
367
404
|
|
|
368
405
|
const startHeartbeat = () => {
|
|
369
|
-
|
|
370
|
-
if (heartbeatInFlight) {
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
heartbeatInFlight = true;
|
|
375
|
-
|
|
406
|
+
const refreshHeartbeat = async () => {
|
|
376
407
|
try {
|
|
377
408
|
const nextHeartbeatAt = Date.now();
|
|
378
409
|
const owner = await readJsonFile<ResourceLockMetadata>(
|
|
@@ -383,33 +414,33 @@ export const createResourceLockManager = (
|
|
|
383
414
|
return;
|
|
384
415
|
}
|
|
385
416
|
|
|
386
|
-
|
|
417
|
+
await writeJsonFileAtomic(paths.ownerFilePath, {
|
|
387
418
|
...owner,
|
|
388
419
|
heartbeatAt: nextHeartbeatAt,
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
420
|
+
});
|
|
421
|
+
scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
// A failed refresh is not fatal by design: the lock goes stale
|
|
424
|
+
// and another holder reclaims it. Swallow it so a transient
|
|
425
|
+
// error (e.g. the owner file racing a concurrent release, or an
|
|
426
|
+
// EPERM on Windows while a file is being deleted) never surfaces
|
|
427
|
+
// as an unhandled rejection from this interval.
|
|
428
|
+
scopedLogger.debug(
|
|
429
|
+
'heartbeat refresh for ticket %s failed: %s',
|
|
430
|
+
ticketId,
|
|
431
|
+
error instanceof Error ? error.message : String(error),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
394
435
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
} catch (error) {
|
|
399
|
-
// A failed refresh is not fatal by design: the lock goes stale
|
|
400
|
-
// and another holder reclaims it. Swallow it so a transient
|
|
401
|
-
// write error (e.g. the owner file racing a concurrent release,
|
|
402
|
-
// or an EPERM on Windows when the directory is being torn down)
|
|
403
|
-
// never surfaces as an unhandled rejection from this interval.
|
|
404
|
-
scopedLogger.debug(
|
|
405
|
-
'heartbeat refresh for ticket %s failed: %s',
|
|
406
|
-
ticketId,
|
|
407
|
-
error instanceof Error ? error.message : String(error),
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
} finally {
|
|
411
|
-
heartbeatInFlight = false;
|
|
436
|
+
heartbeatTimer = setInterval(() => {
|
|
437
|
+
if (heartbeatInFlight) {
|
|
438
|
+
return;
|
|
412
439
|
}
|
|
440
|
+
|
|
441
|
+
heartbeatInFlight = refreshHeartbeat().finally(() => {
|
|
442
|
+
heartbeatInFlight = null;
|
|
443
|
+
});
|
|
413
444
|
}, heartbeatIntervalMs);
|
|
414
445
|
heartbeatTimer.unref?.();
|
|
415
446
|
};
|
package/src/run.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import type { Config as JestConfig } from 'jest-runner';
|
|
3
2
|
import { TestResult as JestTestResult } from '@jest/test-result';
|
|
4
3
|
import type {
|
|
@@ -9,6 +8,7 @@ import type { HarnessSession } from './harness-session.js';
|
|
|
9
8
|
import { formatResultsErrors } from 'jest-message-util';
|
|
10
9
|
import { toTestResult } from './toTestResult.js';
|
|
11
10
|
import { formatHarnessErrorMessage } from './format-harness-error.js';
|
|
11
|
+
import { toRelativePosixPath } from './relative-path.js';
|
|
12
12
|
|
|
13
13
|
// Helper function to flatten nested test suites into a flat array of tests with hierarchy
|
|
14
14
|
const flattenTests = (
|
|
@@ -91,14 +91,14 @@ export const runHarnessTestFile: RunHarnessTestFile = async ({
|
|
|
91
91
|
session,
|
|
92
92
|
}) => {
|
|
93
93
|
const start = Date.now();
|
|
94
|
-
const relativeTestPath =
|
|
94
|
+
const relativeTestPath = toRelativePosixPath(globalConfig.rootDir, testPath);
|
|
95
95
|
|
|
96
96
|
// Extract setup files from Jest config and convert to relative paths
|
|
97
97
|
const setupFiles = projectConfig.setupFiles?.map((setupFile) =>
|
|
98
|
-
|
|
98
|
+
toRelativePosixPath(globalConfig.rootDir, setupFile)
|
|
99
99
|
);
|
|
100
100
|
const setupFilesAfterEnv = projectConfig.setupFilesAfterEnv?.map(
|
|
101
|
-
(setupFile) =>
|
|
101
|
+
(setupFile) => toRelativePosixPath(globalConfig.rootDir, setupFile)
|
|
102
102
|
);
|
|
103
103
|
const testTimeout =
|
|
104
104
|
session.config.testTimeout ??
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Config } from 'jest-runner';
|
|
2
|
+
import {
|
|
3
|
+
createHarnessSession,
|
|
4
|
+
type HarnessSession,
|
|
5
|
+
} from './harness-session.js';
|
|
6
|
+
|
|
7
|
+
// Jest constructs a new runner (and scheduler) for every watch-mode run, so a
|
|
8
|
+
// per-instance session would leave the previous run's session holding the
|
|
9
|
+
// resource lock while the next run queues behind it. Watch-mode sessions
|
|
10
|
+
// therefore live at module scope and are shared by all runner instances.
|
|
11
|
+
//
|
|
12
|
+
// There is no explicit teardown here: in interactive watch mode Jest exits
|
|
13
|
+
// via process.exit on Ctrl-C / `q`, which does not dispose the session. The
|
|
14
|
+
// lock is reclaimed by the lock manager's pid / stale-owner detection.
|
|
15
|
+
type WatchSessionEntry = {
|
|
16
|
+
key: string;
|
|
17
|
+
promise: Promise<HarnessSession>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let current: WatchSessionEntry | null = null;
|
|
21
|
+
|
|
22
|
+
// Everything createHarnessSession reads from the Jest config.
|
|
23
|
+
const getSessionKey = (globalConfig: Config.GlobalConfig): string =>
|
|
24
|
+
JSON.stringify([globalConfig.rootDir, Boolean(globalConfig.collectCoverage)]);
|
|
25
|
+
|
|
26
|
+
export const getWatchSession = (
|
|
27
|
+
globalConfig: Config.GlobalConfig
|
|
28
|
+
): Promise<HarnessSession> => {
|
|
29
|
+
const key = getSessionKey(globalConfig);
|
|
30
|
+
|
|
31
|
+
if (current?.key === key) {
|
|
32
|
+
return current.promise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const stale = current;
|
|
36
|
+
// The entry is installed synchronously so concurrent callers share it, even
|
|
37
|
+
// while a stale session is still being disposed.
|
|
38
|
+
const entry: WatchSessionEntry = {
|
|
39
|
+
key,
|
|
40
|
+
promise: (async () => {
|
|
41
|
+
if (stale) {
|
|
42
|
+
// A failed disposal must not fail the run.
|
|
43
|
+
await stale.promise
|
|
44
|
+
.then((session) => session.dispose())
|
|
45
|
+
.catch(() => undefined);
|
|
46
|
+
}
|
|
47
|
+
return createHarnessSession(globalConfig);
|
|
48
|
+
})(),
|
|
49
|
+
};
|
|
50
|
+
current = entry;
|
|
51
|
+
|
|
52
|
+
// Never cache a failed creation; the next run retries.
|
|
53
|
+
entry.promise.catch(() => {
|
|
54
|
+
if (current === entry) {
|
|
55
|
+
current = null;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return entry.promise;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** @internal Test-only: forget the module-level session without disposing. */
|
|
63
|
+
export const resetWatchSessionForTests = (): void => {
|
|
64
|
+
current = null;
|
|
65
|
+
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { type AppMonitor, type HarnessPlatformRunner } from '@react-native-harness/platforms';
|
|
2
|
-
export type CrashSupervisorState = 'idle' | 'launching' | 'ready' | 'running' | 'disposing';
|
|
3
|
-
export type CrashSupervisor = {
|
|
4
|
-
setActiveTestFile: (testFilePath: string | null) => void;
|
|
5
|
-
beginLaunch: (testFilePath: string) => void;
|
|
6
|
-
markReady: () => void;
|
|
7
|
-
beginTestRun: (testFilePath: string) => void;
|
|
8
|
-
stop: () => Promise<void>;
|
|
9
|
-
start: () => Promise<void>;
|
|
10
|
-
waitForCrash: (testFilePath: string) => Promise<never>;
|
|
11
|
-
isReady: () => boolean;
|
|
12
|
-
cancelCrashWaiters: () => void;
|
|
13
|
-
reset: () => void;
|
|
14
|
-
dispose: () => Promise<void>;
|
|
15
|
-
};
|
|
16
|
-
export type CrashSupervisorOptions = {
|
|
17
|
-
appMonitor: AppMonitor;
|
|
18
|
-
platformRunner: HarnessPlatformRunner;
|
|
19
|
-
};
|
|
20
|
-
export declare const createCrashSupervisor: ({ appMonitor, platformRunner, }: CrashSupervisorOptions) => CrashSupervisor;
|
|
21
|
-
//# sourceMappingURL=crash-supervisor.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"crash-supervisor.d.ts","sourceRoot":"","sources":["../src/crash-supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAIf,KAAK,qBAAqB,EAC3B,MAAM,iCAAiC,CAAC;AAUzC,MAAM,MAAM,oBAAoB,GAC5B,MAAM,GACN,WAAW,GACX,OAAO,GACP,SAAS,GACT,WAAW,CAAC;AAEhB,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,EAAE,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzD,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IACvD,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,qBAAqB,CAAC;CACvC,CAAC;AAmCF,eAAO,MAAM,qBAAqB,GAAI,iCAGnC,sBAAsB,KAAG,eAkP3B,CAAC"}
|
package/dist/crash-supervisor.js
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { NativeCrashError, } from './errors.js';
|
|
2
|
-
import { logger } from '@react-native-harness/tools';
|
|
3
|
-
const crashLogger = logger.child('crash');
|
|
4
|
-
const getCrashPhase = (state) => state === 'running' ? 'execution' : 'startup';
|
|
5
|
-
const mergeCrashDetails = (phase, initialDetails, enrichedDetails, fallbackSummary) => ({
|
|
6
|
-
phase,
|
|
7
|
-
source: enrichedDetails?.source ?? initialDetails?.source,
|
|
8
|
-
summary: enrichedDetails?.summary ?? initialDetails?.summary ?? fallbackSummary,
|
|
9
|
-
signal: enrichedDetails?.signal ?? initialDetails?.signal,
|
|
10
|
-
exceptionType: enrichedDetails?.exceptionType ?? initialDetails?.exceptionType,
|
|
11
|
-
processName: enrichedDetails?.processName ?? initialDetails?.processName,
|
|
12
|
-
pid: enrichedDetails?.pid ?? initialDetails?.pid,
|
|
13
|
-
stackTrace: enrichedDetails?.stackTrace ?? initialDetails?.stackTrace,
|
|
14
|
-
rawLines: enrichedDetails?.rawLines ?? initialDetails?.rawLines,
|
|
15
|
-
artifactType: enrichedDetails?.artifactType ?? initialDetails?.artifactType,
|
|
16
|
-
artifactPath: enrichedDetails?.artifactPath ?? initialDetails?.artifactPath,
|
|
17
|
-
});
|
|
18
|
-
export const createCrashSupervisor = ({ appMonitor, platformRunner, }) => {
|
|
19
|
-
let state = 'idle';
|
|
20
|
-
let activeTestFilePath = null;
|
|
21
|
-
let crashRejectors = new Set();
|
|
22
|
-
let disposed = false;
|
|
23
|
-
let monitoring = true;
|
|
24
|
-
let isResolvingCrash = false;
|
|
25
|
-
const getCrashDetailsProvider = () => {
|
|
26
|
-
if ('getCrashDetails' in appMonitor) {
|
|
27
|
-
return appMonitor;
|
|
28
|
-
}
|
|
29
|
-
if (platformRunner.getCrashDetails) {
|
|
30
|
-
return platformRunner;
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
};
|
|
34
|
-
const rejectCrashWaiters = (testFilePath, details) => {
|
|
35
|
-
const error = new NativeCrashError(testFilePath, details);
|
|
36
|
-
for (const reject of crashRejectors) {
|
|
37
|
-
reject(error);
|
|
38
|
-
}
|
|
39
|
-
crashRejectors = new Set();
|
|
40
|
-
};
|
|
41
|
-
const handleCrash = async (reason, details) => {
|
|
42
|
-
if (isResolvingCrash ||
|
|
43
|
-
(state !== 'launching' && state !== 'running' && state !== 'ready')) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
if (!activeTestFilePath) {
|
|
47
|
-
crashLogger.debug('ignoring crash signal without active test: %s', reason);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
isResolvingCrash = true;
|
|
51
|
-
crashLogger.debug('native crash detected during %s (state=%s reason=%s)', activeTestFilePath, state, reason || '(none)');
|
|
52
|
-
for (const line of details?.rawLines ?? []) {
|
|
53
|
-
crashLogger.debug('%s', line);
|
|
54
|
-
}
|
|
55
|
-
const phase = getCrashPhase(state);
|
|
56
|
-
const testFilePath = activeTestFilePath;
|
|
57
|
-
state = 'idle';
|
|
58
|
-
try {
|
|
59
|
-
const enrichedDetails = await getCrashDetailsProvider()?.getCrashDetails?.({
|
|
60
|
-
processName: details?.processName,
|
|
61
|
-
pid: details?.pid,
|
|
62
|
-
occurredAt: Date.now(),
|
|
63
|
-
});
|
|
64
|
-
const mergedDetails = mergeCrashDetails(phase, details, enrichedDetails, reason);
|
|
65
|
-
crashLogger.debug('crash details: %o', {
|
|
66
|
-
phase: mergedDetails.phase,
|
|
67
|
-
source: mergedDetails.source,
|
|
68
|
-
summary: mergedDetails.summary,
|
|
69
|
-
signal: mergedDetails.signal,
|
|
70
|
-
exceptionType: mergedDetails.exceptionType,
|
|
71
|
-
processName: mergedDetails.processName,
|
|
72
|
-
pid: mergedDetails.pid,
|
|
73
|
-
});
|
|
74
|
-
rejectCrashWaiters(testFilePath, mergedDetails);
|
|
75
|
-
}
|
|
76
|
-
finally {
|
|
77
|
-
isResolvingCrash = false;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
const confirmCrash = async (reason, details) => {
|
|
81
|
-
if (disposed || !monitoring || state === 'disposing') {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
const isRunning = await platformRunner.isAppRunning();
|
|
86
|
-
if (!isRunning) {
|
|
87
|
-
void handleCrash(reason, details);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
crashLogger.debug('crash confirmation failed', error);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
const appMonitorListener = (event) => {
|
|
95
|
-
if (disposed || !monitoring) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (event.type === 'app_started') {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (event.type === 'app_exited') {
|
|
102
|
-
const details = {
|
|
103
|
-
source: event.crashDetails?.source ?? event.source,
|
|
104
|
-
summary: event.crashDetails?.summary,
|
|
105
|
-
signal: event.crashDetails?.signal,
|
|
106
|
-
exceptionType: event.crashDetails?.exceptionType,
|
|
107
|
-
processName: event.crashDetails?.processName,
|
|
108
|
-
pid: event.crashDetails?.pid ?? event.pid,
|
|
109
|
-
stackTrace: event.crashDetails?.stackTrace,
|
|
110
|
-
rawLines: event.crashDetails?.rawLines ??
|
|
111
|
-
(event.line ? [event.line] : undefined),
|
|
112
|
-
};
|
|
113
|
-
if (event.isConfirmed ?? event.source === 'polling') {
|
|
114
|
-
void handleCrash('', details);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
void confirmCrash('', details);
|
|
118
|
-
}
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (event.type === 'possible_crash') {
|
|
122
|
-
const details = {
|
|
123
|
-
source: event.crashDetails?.source ?? event.source,
|
|
124
|
-
summary: event.crashDetails?.summary,
|
|
125
|
-
signal: event.crashDetails?.signal,
|
|
126
|
-
exceptionType: event.crashDetails?.exceptionType,
|
|
127
|
-
processName: event.crashDetails?.processName,
|
|
128
|
-
pid: event.crashDetails?.pid ?? event.pid,
|
|
129
|
-
stackTrace: event.crashDetails?.stackTrace,
|
|
130
|
-
rawLines: event.crashDetails?.rawLines ??
|
|
131
|
-
(event.line ? [event.line] : undefined),
|
|
132
|
-
};
|
|
133
|
-
if (event.isConfirmed) {
|
|
134
|
-
void handleCrash(`possible crash signal (${event.source ?? 'unknown'})`, details);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
void confirmCrash(`possible crash signal (${event.source ?? 'unknown'})`, details);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
appMonitor.addListener(appMonitorListener);
|
|
142
|
-
const setActiveTestFile = (testFilePath) => {
|
|
143
|
-
activeTestFilePath = testFilePath;
|
|
144
|
-
};
|
|
145
|
-
const beginLaunch = (testFilePath) => {
|
|
146
|
-
activeTestFilePath = testFilePath;
|
|
147
|
-
state = 'launching';
|
|
148
|
-
};
|
|
149
|
-
const markReady = () => {
|
|
150
|
-
if (state !== 'disposing') {
|
|
151
|
-
state = 'ready';
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
const beginTestRun = (testFilePath) => {
|
|
155
|
-
activeTestFilePath = testFilePath;
|
|
156
|
-
state = 'running';
|
|
157
|
-
};
|
|
158
|
-
const stop = async () => {
|
|
159
|
-
monitoring = false;
|
|
160
|
-
await appMonitor.stop();
|
|
161
|
-
};
|
|
162
|
-
const start = async () => {
|
|
163
|
-
monitoring = true;
|
|
164
|
-
await appMonitor.start();
|
|
165
|
-
};
|
|
166
|
-
const waitForCrash = (testFilePath) => new Promise((_, reject) => {
|
|
167
|
-
if (disposed) {
|
|
168
|
-
reject(new NativeCrashError(testFilePath, {
|
|
169
|
-
phase: 'startup',
|
|
170
|
-
source: 'polling',
|
|
171
|
-
summary: 'Crash supervisor disposed while waiting for app startup.',
|
|
172
|
-
}));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
crashRejectors.add(reject);
|
|
176
|
-
});
|
|
177
|
-
const isReady = () => state === 'ready' || state === 'running';
|
|
178
|
-
const cancelCrashWaiters = () => {
|
|
179
|
-
crashRejectors = new Set();
|
|
180
|
-
};
|
|
181
|
-
const reset = () => {
|
|
182
|
-
cancelCrashWaiters();
|
|
183
|
-
isResolvingCrash = false;
|
|
184
|
-
if (state !== 'disposing') {
|
|
185
|
-
state = 'idle';
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
const dispose = async () => {
|
|
189
|
-
disposed = true;
|
|
190
|
-
monitoring = false;
|
|
191
|
-
state = 'disposing';
|
|
192
|
-
crashRejectors = new Set();
|
|
193
|
-
isResolvingCrash = false;
|
|
194
|
-
appMonitor.removeListener(appMonitorListener);
|
|
195
|
-
await appMonitor.dispose();
|
|
196
|
-
};
|
|
197
|
-
return {
|
|
198
|
-
setActiveTestFile,
|
|
199
|
-
beginLaunch,
|
|
200
|
-
markReady,
|
|
201
|
-
beginTestRun,
|
|
202
|
-
stop,
|
|
203
|
-
start,
|
|
204
|
-
waitForCrash,
|
|
205
|
-
isReady,
|
|
206
|
-
cancelCrashWaiters,
|
|
207
|
-
reset,
|
|
208
|
-
dispose,
|
|
209
|
-
};
|
|
210
|
-
};
|
package/dist/event-mapper.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { TestEvents } from '@jest/test-result';
|
|
2
|
-
import type { Test } from 'jest-runner';
|
|
3
|
-
import type { BridgeEvents } from '@react-native-harness/bridge';
|
|
4
|
-
import type { TestCaseResult } from '@jest/test-result';
|
|
5
|
-
export type EventMapperState = {
|
|
6
|
-
currentTest?: Test;
|
|
7
|
-
suiteHierarchy: string[];
|
|
8
|
-
testResults: Map<string, TestCaseResult>;
|
|
9
|
-
fileStartTime: number;
|
|
10
|
-
};
|
|
11
|
-
export type HarnessEventMapper = {
|
|
12
|
-
handleBridgeEvent: (event: BridgeEvents, test: Test) => void;
|
|
13
|
-
reset: () => void;
|
|
14
|
-
};
|
|
15
|
-
export declare const getHarnessEventMapper: (emit: <T extends keyof TestEvents>(eventName: T, eventData: TestEvents[T]) => void) => HarnessEventMapper;
|
|
16
|
-
//# sourceMappingURL=event-mapper.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"event-mapper.d.ts","sourceRoot":"","sources":["../src/event-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EASV,YAAY,EACb,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACzC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iBAAiB,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC7D,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,CAAC,CAAC,SAAS,MAAM,UAAU,EAC/B,SAAS,EAAE,CAAC,EACZ,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,KACrB,IAAI,KACR,kBA8JF,CAAC"}
|
package/dist/event-mapper.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
export const getHarnessEventMapper = (emit) => {
|
|
2
|
-
const state = {
|
|
3
|
-
suiteHierarchy: [],
|
|
4
|
-
testResults: new Map(),
|
|
5
|
-
fileStartTime: 0,
|
|
6
|
-
};
|
|
7
|
-
const handleBridgeEvent = (event, test) => {
|
|
8
|
-
// Filter to only process test runner events
|
|
9
|
-
if (event.type.startsWith('test-') ||
|
|
10
|
-
event.type.startsWith('suite-') ||
|
|
11
|
-
event.type.startsWith('file-')) {
|
|
12
|
-
switch (event.type) {
|
|
13
|
-
case 'file-started':
|
|
14
|
-
handleFileStarted(event, test);
|
|
15
|
-
break;
|
|
16
|
-
case 'file-finished':
|
|
17
|
-
handleFileFinished(event, test);
|
|
18
|
-
break;
|
|
19
|
-
case 'suite-started':
|
|
20
|
-
handleSuiteStarted(event);
|
|
21
|
-
break;
|
|
22
|
-
case 'suite-finished':
|
|
23
|
-
handleSuiteFinished(event);
|
|
24
|
-
break;
|
|
25
|
-
case 'test-started':
|
|
26
|
-
handleTestStarted(event, test);
|
|
27
|
-
break;
|
|
28
|
-
case 'test-finished':
|
|
29
|
-
handleTestFinished(event, test);
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
const handleFileStarted = (event, test) => {
|
|
35
|
-
state.currentTest = test;
|
|
36
|
-
state.fileStartTime = Date.now();
|
|
37
|
-
state.suiteHierarchy = [];
|
|
38
|
-
state.testResults.clear();
|
|
39
|
-
// Don't emit test-file-start here - that should be handled by the Jest runner
|
|
40
|
-
// when it actually starts running the test file
|
|
41
|
-
};
|
|
42
|
-
const handleFileFinished = (_event, _test) => {
|
|
43
|
-
// File finished - all test results should already be emitted
|
|
44
|
-
// The final TestResult will be handled by the runHarnessTestFile function
|
|
45
|
-
};
|
|
46
|
-
const handleSuiteStarted = (event) => {
|
|
47
|
-
state.suiteHierarchy.push(event.name);
|
|
48
|
-
};
|
|
49
|
-
const handleSuiteFinished = (event) => {
|
|
50
|
-
// Remove the suite from hierarchy when it finishes
|
|
51
|
-
const index = state.suiteHierarchy.lastIndexOf(event.name);
|
|
52
|
-
if (index !== -1) {
|
|
53
|
-
state.suiteHierarchy.splice(index, 1);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
const handleTestStarted = (event, test) => {
|
|
57
|
-
const testCaseStartInfo = {
|
|
58
|
-
ancestorTitles: [...state.suiteHierarchy],
|
|
59
|
-
fullName: `${state.suiteHierarchy.join(' ')} ${event.name}`.trim(),
|
|
60
|
-
title: event.name,
|
|
61
|
-
mode: 'skip', // Default mode, will be updated based on test status
|
|
62
|
-
};
|
|
63
|
-
emit('test-case-start', [test.path, testCaseStartInfo]);
|
|
64
|
-
};
|
|
65
|
-
const handleTestFinished = (event, test) => {
|
|
66
|
-
const testCaseResult = {
|
|
67
|
-
ancestorTitles: [...state.suiteHierarchy],
|
|
68
|
-
duration: event.duration,
|
|
69
|
-
failureDetails: [],
|
|
70
|
-
failureMessages: event.error ? [formatError(event.error)] : [],
|
|
71
|
-
fullName: `${state.suiteHierarchy.join(' ')} ${event.name}`.trim(),
|
|
72
|
-
numPassingAsserts: event.status === 'passed' ? 1 : 0,
|
|
73
|
-
status: mapStatus(event.status),
|
|
74
|
-
title: event.name,
|
|
75
|
-
startedAt: Date.now() - event.duration,
|
|
76
|
-
};
|
|
77
|
-
state.testResults.set(event.name, testCaseResult);
|
|
78
|
-
emit('test-case-result', [test.path, testCaseResult]);
|
|
79
|
-
};
|
|
80
|
-
const mapStatus = (status) => {
|
|
81
|
-
return status;
|
|
82
|
-
};
|
|
83
|
-
const formatError = (error) => {
|
|
84
|
-
let errorMessage = `${error.name}: ${error.message}`;
|
|
85
|
-
if (error.codeFrame) {
|
|
86
|
-
errorMessage += '\n\n' + formatCodeFrame(error.codeFrame);
|
|
87
|
-
}
|
|
88
|
-
return errorMessage;
|
|
89
|
-
};
|
|
90
|
-
const formatCodeFrame = (codeFrame) => {
|
|
91
|
-
const lines = codeFrame.content.split('\n');
|
|
92
|
-
const lineNumber = codeFrame.location?.row || 1;
|
|
93
|
-
const column = codeFrame.location?.column || 1;
|
|
94
|
-
let formatted = `\n${codeFrame.fileName}:${lineNumber}:${column}\n`;
|
|
95
|
-
// Add context lines around the error
|
|
96
|
-
const startLine = Math.max(0, lineNumber - 3);
|
|
97
|
-
const endLine = Math.min(lines.length, lineNumber + 2);
|
|
98
|
-
for (let i = startLine; i < endLine; i++) {
|
|
99
|
-
const lineNum = i + 1;
|
|
100
|
-
const line = lines[i];
|
|
101
|
-
const isErrorLine = lineNum === lineNumber;
|
|
102
|
-
formatted += `${lineNum.toString().padStart(3)} | ${line}\n`;
|
|
103
|
-
if (isErrorLine && column > 0) {
|
|
104
|
-
const pointer = ' '.repeat(column + 5) + '^';
|
|
105
|
-
formatted += pointer + '\n';
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return formatted;
|
|
109
|
-
};
|
|
110
|
-
const reset = () => {
|
|
111
|
-
state.suiteHierarchy = [];
|
|
112
|
-
state.testResults.clear();
|
|
113
|
-
state.fileStartTime = 0;
|
|
114
|
-
state.currentTest = undefined;
|
|
115
|
-
};
|
|
116
|
-
return {
|
|
117
|
-
handleBridgeEvent,
|
|
118
|
-
reset,
|
|
119
|
-
};
|
|
120
|
-
};
|
package/dist/global-setup.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"global-setup.d.ts","sourceRoot":"","sources":["../src/global-setup.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AA4CxD,yBAA+B,YAAY,EAAE,UAAU,CAAC,YAAY,iBA4CnE"}
|