@react-native-harness/jest 1.5.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 +110 -23
- 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 +143 -22
- 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/errors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatPath, HarnessError } from '@react-native-harness/tools';
|
|
1
|
+
import { formatHostPressure, formatPath, HarnessError, isHostStarved, } 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() {
|
|
@@ -20,13 +20,28 @@ export class InitializationTimeoutError extends HarnessError {
|
|
|
20
20
|
this.name = 'InitializationTimeoutError';
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
const buildPlatformReadyTimeoutMessage = (timeout, hostPressure) => {
|
|
24
|
+
const prefix = `The platform did not become ready within ${timeout}ms.`;
|
|
25
|
+
if (hostPressure && isHostStarved(hostPressure)) {
|
|
26
|
+
return `${prefix} The host was starved for resources during startup (${formatHostPressure(hostPressure)}). A larger "platformReadyTimeout" is unlikely to help; reduce other load on the host or use a larger machine.`;
|
|
27
|
+
}
|
|
28
|
+
const suggestion = `Increase "platformReadyTimeout" if your device, simulator, or emulator needs more time to start.`;
|
|
29
|
+
return hostPressure
|
|
30
|
+
? `${prefix} ${suggestion} Host during startup: ${formatHostPressure(hostPressure)}.`
|
|
31
|
+
: `${prefix} ${suggestion}`;
|
|
32
|
+
};
|
|
23
33
|
export class PlatformReadyTimeoutError extends HarnessError {
|
|
24
34
|
timeout;
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
hostPressure;
|
|
36
|
+
constructor(timeout, hostPressure = null) {
|
|
37
|
+
super(buildPlatformReadyTimeoutMessage(timeout, hostPressure));
|
|
27
38
|
this.timeout = timeout;
|
|
39
|
+
this.hostPressure = hostPressure;
|
|
28
40
|
this.name = 'PlatformReadyTimeoutError';
|
|
29
41
|
}
|
|
42
|
+
get hostStarved() {
|
|
43
|
+
return this.hostPressure !== null && isHostStarved(this.hostPressure);
|
|
44
|
+
}
|
|
30
45
|
}
|
|
31
46
|
export class MetroPortRangeExhaustedError extends HarnessError {
|
|
32
47
|
initialPort;
|
|
@@ -128,3 +143,14 @@ export class RuntimeDisconnectError extends HarnessError {
|
|
|
128
143
|
return this.details.phase;
|
|
129
144
|
}
|
|
130
145
|
}
|
|
146
|
+
// The runtime sends this name through the runTests RPC when its native host
|
|
147
|
+
// surface is replaced while a test file is still running.
|
|
148
|
+
export class HostSurfaceReplacedError extends HarnessError {
|
|
149
|
+
testFilePath;
|
|
150
|
+
constructor(testFilePath, cause) {
|
|
151
|
+
super(cause.message, { cause });
|
|
152
|
+
this.testFilePath = testFilePath;
|
|
153
|
+
this.name = 'HostSurfaceReplacedError';
|
|
154
|
+
this.stack = cause.stack;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAErB,OAAO,EAAE,KAAK,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AAsCjF,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;AA8GnB,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,CAsRd,CAAC"}
|
package/dist/execute-run.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
import { runHarnessTestFile } from './run.js';
|
|
3
|
+
import { toRelativePosixPath } from './relative-path.js';
|
|
4
4
|
import { resolveResetStrategyKind } from './environment-reset.js';
|
|
5
|
-
import { NativeCrashError, RuntimeDisconnectError, StartupStallError, } from './errors.js';
|
|
5
|
+
import { HostSurfaceReplacedError, NativeCrashError, RuntimeDisconnectError, StartupStallError, } from './errors.js';
|
|
6
6
|
import { AppBridgeDisconnectedError, DeviceNotRespondingError, } from '@react-native-harness/bridge/server';
|
|
7
7
|
import { createPlatformSkippedTestResult, shouldRunHarnessTestFile, } from './test-file-platform-filter.js';
|
|
8
8
|
import { formatHarnessErrorMessage } from './format-harness-error.js';
|
|
9
9
|
import { logger } from '@react-native-harness/tools';
|
|
10
|
-
import { printSummary, writeTraceFile } from './diagnostics/index.js';
|
|
10
|
+
import { appendStepSummary, printSummary, summarizeRunKpis, writeSummaryFile, writeTraceFile, } from './diagnostics/index.js';
|
|
11
11
|
import { toJestStatus } from './toJestStatus.js';
|
|
12
12
|
const diagnosticsLogger = logger.child('diagnostics');
|
|
13
13
|
const createRunSummary = () => ({ passed: 0, failed: 0, skipped: 0, todo: 0 });
|
|
@@ -23,13 +23,22 @@ class CancelRun extends Error {
|
|
|
23
23
|
this.name = 'CancelRun';
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
const isRuntimeFailure = (err) => err instanceof NativeCrashError ||
|
|
27
|
+
err instanceof RuntimeDisconnectError ||
|
|
28
|
+
err instanceof StartupStallError ||
|
|
29
|
+
err instanceof AppBridgeDisconnectedError ||
|
|
30
|
+
err instanceof DeviceNotRespondingError;
|
|
31
|
+
const formatRunningTest = (test) => `Running test: "${[...test.ancestorTitles, test.name].join(' › ')}"`;
|
|
32
|
+
const buildTestFailure = (err, runningTest = null) => {
|
|
33
|
+
if (isRuntimeFailure(err)) {
|
|
34
|
+
if (!runningTest) {
|
|
35
|
+
return { message: err.message, stack: '' };
|
|
36
|
+
}
|
|
37
|
+
const [headline, ...details] = err.message.split('\n');
|
|
38
|
+
return {
|
|
39
|
+
message: [headline, formatRunningTest(runningTest), ...details].join('\n'),
|
|
40
|
+
stack: '',
|
|
41
|
+
};
|
|
33
42
|
}
|
|
34
43
|
return err;
|
|
35
44
|
};
|
|
@@ -80,10 +89,19 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
|
|
|
80
89
|
const startTime = Date.now();
|
|
81
90
|
const watchMode = globalConfig.watch || globalConfig.watchAll;
|
|
82
91
|
const rootDir = globalConfig.rootDir ?? process.cwd();
|
|
83
|
-
const testFiles = tests.map((t) =>
|
|
92
|
+
const testFiles = tests.map((t) => toRelativePosixPath(rootDir, t.path));
|
|
84
93
|
const summary = createRunSummary();
|
|
85
94
|
let caseEventChain = Promise.resolve();
|
|
95
|
+
// The device runs one test at a time, so a test that started and has not
|
|
96
|
+
// finished is the one the app was running if it crashes or disconnects.
|
|
97
|
+
let runningTest = null;
|
|
86
98
|
const unsubscribe = session.onTestRunnerEvent((event) => {
|
|
99
|
+
if (event.type === 'test-started') {
|
|
100
|
+
runningTest = event;
|
|
101
|
+
}
|
|
102
|
+
else if (event.type === 'test-finished') {
|
|
103
|
+
runningTest = null;
|
|
104
|
+
}
|
|
87
105
|
if (isHarnessCaseEvent(event)) {
|
|
88
106
|
caseEventChain = caseEventChain.then(() => event.type === 'test-started'
|
|
89
107
|
? emitHarnessTestStarted(emitEvent, event)
|
|
@@ -106,6 +124,11 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
|
|
|
106
124
|
return state;
|
|
107
125
|
};
|
|
108
126
|
updateRunState();
|
|
127
|
+
session.lifecycle.emit({
|
|
128
|
+
type: 'phase',
|
|
129
|
+
phase: 'run.start',
|
|
130
|
+
attrs: { runId },
|
|
131
|
+
});
|
|
109
132
|
await session.callHook('run:started', {
|
|
110
133
|
runId,
|
|
111
134
|
startTime,
|
|
@@ -128,8 +151,9 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
|
|
|
128
151
|
for (const test of tests) {
|
|
129
152
|
if (watcher.isInterrupted())
|
|
130
153
|
throw new CancelRun();
|
|
131
|
-
const relativeTestPath =
|
|
154
|
+
const relativeTestPath = toRelativePosixPath(rootDir, test.path);
|
|
132
155
|
const fileStartedAt = Date.now();
|
|
156
|
+
runningTest = null;
|
|
133
157
|
let emittedTestFileFinished = false;
|
|
134
158
|
const emitTestFileFinished = async (options) => {
|
|
135
159
|
emittedTestFileFinished = true;
|
|
@@ -143,6 +167,11 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
|
|
|
143
167
|
};
|
|
144
168
|
await session.callHook('test-file:started', { runId, file: relativeTestPath });
|
|
145
169
|
const fileSpan = diagnostics.start('run.file', { file: relativeTestPath });
|
|
170
|
+
session.lifecycle.emit({
|
|
171
|
+
type: 'phase',
|
|
172
|
+
phase: 'test-file.start',
|
|
173
|
+
attrs: { file: relativeTestPath },
|
|
174
|
+
});
|
|
146
175
|
let fileStatus = 'failed';
|
|
147
176
|
try {
|
|
148
177
|
if (!shouldRunHarnessTestFile(test.path, platformId, knownPlatformIds)) {
|
|
@@ -216,7 +245,10 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
|
|
|
216
245
|
shouldRestartAfterTimeout = false;
|
|
217
246
|
}
|
|
218
247
|
}
|
|
219
|
-
catch (
|
|
248
|
+
catch (error) {
|
|
249
|
+
const err = error instanceof Error && error.name === 'HostSurfaceReplacedError'
|
|
250
|
+
? new HostSurfaceReplacedError(test.path, error)
|
|
251
|
+
: error;
|
|
220
252
|
fileStatus = 'failed';
|
|
221
253
|
if (!emittedTestFileFinished) {
|
|
222
254
|
await emitTestFileFinished({
|
|
@@ -225,25 +257,31 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
|
|
|
225
257
|
result: null,
|
|
226
258
|
});
|
|
227
259
|
}
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
err instanceof StartupStallError ||
|
|
231
|
-
err instanceof AppBridgeDisconnectedError ||
|
|
232
|
-
err instanceof DeviceNotRespondingError;
|
|
233
|
-
if (isRuntimeFailure) {
|
|
260
|
+
const isFileRuntimeFailure = err instanceof HostSurfaceReplacedError || isRuntimeFailure(err);
|
|
261
|
+
if (isFileRuntimeFailure) {
|
|
234
262
|
summary.failed += 1;
|
|
235
263
|
updateRunState();
|
|
236
264
|
}
|
|
237
265
|
if (err instanceof NativeCrashError || err instanceof RuntimeDisconnectError) {
|
|
238
266
|
session.resetCrashState();
|
|
239
267
|
}
|
|
240
|
-
updateRunState({ error:
|
|
268
|
+
updateRunState({ error: isFileRuntimeFailure ? undefined : err });
|
|
241
269
|
await caseEventChain;
|
|
242
|
-
await emitEvent('test-file-failure', test, buildTestFailure(err));
|
|
270
|
+
await emitEvent('test-file-failure', test, buildTestFailure(err, runningTest));
|
|
271
|
+
if (err instanceof HostSurfaceReplacedError) {
|
|
272
|
+
// Abandoned async test work may still be running in this process.
|
|
273
|
+
await session.restartApp(test.path);
|
|
274
|
+
isFirstTest = true;
|
|
275
|
+
}
|
|
243
276
|
}
|
|
244
277
|
}
|
|
245
278
|
finally {
|
|
246
279
|
fileSpan.end({ status: fileStatus });
|
|
280
|
+
session.lifecycle.emit({
|
|
281
|
+
type: 'phase',
|
|
282
|
+
phase: 'test-file.end',
|
|
283
|
+
attrs: { file: relativeTestPath, status: fileStatus },
|
|
284
|
+
});
|
|
247
285
|
}
|
|
248
286
|
}
|
|
249
287
|
}
|
|
@@ -267,12 +305,26 @@ export const executeRun = async (session, tests, watcher, emitEvent, globalConfi
|
|
|
267
305
|
await caseEventChain;
|
|
268
306
|
unsubscribe();
|
|
269
307
|
runSpan.end({ runId, status: finalStatus });
|
|
308
|
+
// Idle time between runs (watch mode) is not part of any run's metrics.
|
|
309
|
+
session.lifecycle.emit({
|
|
310
|
+
type: 'phase',
|
|
311
|
+
phase: 'run.end',
|
|
312
|
+
attrs: { runId, status: finalStatus },
|
|
313
|
+
});
|
|
270
314
|
if (session.diagnostics.enabled) {
|
|
271
315
|
const entries = session.diagnostics.flush();
|
|
272
316
|
try {
|
|
273
317
|
printSummary(entries, (line) => diagnosticsLogger.info(line));
|
|
274
318
|
const tracePath = await writeTraceFile(entries, { projectRoot: rootDir, runId });
|
|
275
319
|
diagnosticsLogger.info(`wrote diagnostics trace to ${tracePath}`);
|
|
320
|
+
const kpis = summarizeRunKpis(entries);
|
|
321
|
+
const summaryPath = await writeSummaryFile(kpis, {
|
|
322
|
+
projectRoot: rootDir,
|
|
323
|
+
runId,
|
|
324
|
+
sessionId: session.sessionId,
|
|
325
|
+
});
|
|
326
|
+
diagnosticsLogger.info(`wrote diagnostics summary to ${summaryPath}`);
|
|
327
|
+
await appendStepSummary(kpis);
|
|
276
328
|
}
|
|
277
329
|
catch (reportError) {
|
|
278
330
|
diagnosticsLogger.warn('failed to write diagnostics report: %s', reportError);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format-harness-error.d.ts","sourceRoot":"","sources":["../src/format-harness-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AA6CpE,eAAO,MAAM,yBAAyB,GACpC,OAAO,eAAe,GAAG,SAAS,EAClC,UAAS;IACP,aAAa,CAAC,EAAE,MAAM,CAAC;CACnB,KACL,MAAM,GAAG,
|
|
1
|
+
{"version":3,"file":"format-harness-error.d.ts","sourceRoot":"","sources":["../src/format-harness-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AA6CpE,eAAO,MAAM,yBAAyB,GACpC,OAAO,eAAe,GAAG,SAAS,EAClC,UAAS;IACP,aAAa,CAAC,EAAE,MAAM,CAAC;CACnB,KACL,MAAM,GAAG,SA2BX,CAAC"}
|
|
@@ -30,6 +30,9 @@ export const formatHarnessErrorMessage = (error, options = {}) => {
|
|
|
30
30
|
return undefined;
|
|
31
31
|
}
|
|
32
32
|
const parts = [error.message];
|
|
33
|
+
if (error.diff) {
|
|
34
|
+
parts.push(error.diff);
|
|
35
|
+
}
|
|
33
36
|
const pendingPromiseDetails = error.diagnostics?.pendingPromises
|
|
34
37
|
? formatPendingPromises(error.diagnostics.pendingPromises, options.testStartedAt)
|
|
35
38
|
: null;
|
|
@@ -2,12 +2,13 @@ import { type HarnessBridge, type AppConnection } from '@react-native-harness/br
|
|
|
2
2
|
import { type HarnessContext, type TestRunnerEvents, type TestExecutionOptions, type TestSuiteResult } from '@react-native-harness/bridge';
|
|
3
3
|
import { type HarnessPlatform } from '@react-native-harness/platforms';
|
|
4
4
|
import { type HarnessPluginManager, type HarnessRunStatus, type HarnessRunSummary } from '@react-native-harness/plugins';
|
|
5
|
-
import { type Diagnostics } from '@react-native-harness/tools';
|
|
5
|
+
import { type Diagnostics, type HostPressure } from '@react-native-harness/tools';
|
|
6
6
|
import { type Config as HarnessConfig } from '@react-native-harness/config';
|
|
7
7
|
import type { Config as JestConfig } from 'jest-runner';
|
|
8
8
|
import { type CrashMonitor } from './crash-monitor.js';
|
|
9
9
|
import { type ClientLogBuffer } from './client-log-handler.js';
|
|
10
10
|
import { type ResourceLockManager } from './resource-lock.js';
|
|
11
|
+
import { type SessionTelemetry } from './diagnostics/index.js';
|
|
11
12
|
type DisconnectObservableBridge = {
|
|
12
13
|
readonly connection: AppConnection | null;
|
|
13
14
|
on: (event: 'disconnected', listener: () => void) => void;
|
|
@@ -38,6 +39,10 @@ export type HarnessSession = {
|
|
|
38
39
|
readonly config: HarnessConfig;
|
|
39
40
|
readonly context: HarnessContext;
|
|
40
41
|
readonly diagnostics: Diagnostics;
|
|
42
|
+
/** Identifies this session; names its incremental diagnostics log. */
|
|
43
|
+
readonly sessionId: string;
|
|
44
|
+
/** Run lifecycle phases, for telemetry probes (see `SessionTelemetry`). */
|
|
45
|
+
readonly lifecycle: SessionTelemetry['lifecycle'];
|
|
41
46
|
onTestRunnerEvent: (listener: (event: TestRunnerEvents) => void) => () => void;
|
|
42
47
|
runTestFile: (path: string, options: HarnessRunTestsOptions) => Promise<TestSuiteResult>;
|
|
43
48
|
ensureAppReady: (testFilePath: string) => Promise<void>;
|
|
@@ -54,6 +59,8 @@ export declare const withPlatformReadyTimeout: <T>(options: {
|
|
|
54
59
|
timeout: number;
|
|
55
60
|
signal: AbortSignal;
|
|
56
61
|
work: (signal: AbortSignal) => Promise<T>;
|
|
62
|
+
/** Host pressure at the moment of the timeout, attached to the error. */
|
|
63
|
+
getHostPressure?: () => HostPressure | null;
|
|
57
64
|
}) => Promise<T>;
|
|
58
65
|
export declare const waitForStartupCrash: ({ crashMonitor, detectNativeCrashes, testFilePath, signal, }: {
|
|
59
66
|
crashMonitor: CrashMonitor;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness-session.d.ts","sourceRoot":"","sources":["../src/harness-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAEL,KAAK,cAAc,EAEnB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACrB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAGL,KAAK,eAAe,EAGrB,MAAM,iCAAiC,CAAC;AASzC,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AACvC,OAAO,
|
|
1
|
+
{"version":3,"file":"harness-session.d.ts","sourceRoot":"","sources":["../src/harness-session.ts"],"names":[],"mappings":"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"}
|
package/dist/harness-session.js
CHANGED
|
@@ -3,7 +3,7 @@ import { HARNESS_BRIDGE_PATH, } from '@react-native-harness/bridge';
|
|
|
3
3
|
import { createHarnessCache } from '@react-native-harness/cache';
|
|
4
4
|
import { getMetroInstance, waitForMetroBackedAppReady, } from '@react-native-harness/bundler-metro';
|
|
5
5
|
import { createHarnessPluginManager, } from '@react-native-harness/plugins';
|
|
6
|
-
import { createCrashArtifactWriter, createDiagnostics, delay, logger, getTimeoutSignal, ensureSignalsDeliverable, waitForAbort, } from '@react-native-harness/tools';
|
|
6
|
+
import { createCrashArtifactWriter, createDiagnostics, delay, logger, getTimeoutSignal, ensureSignalsDeliverable, waitForAbort, checkHostHealth, formatGitHubWarning, formatSaturatedHostMessage, isCI, readProcessUsage, } from '@react-native-harness/tools';
|
|
7
7
|
import { getConfig, isDiagnosticsEnabled, resolveMetroCacheEnabled, ConfigSchema, } from '@react-native-harness/config';
|
|
8
8
|
import { preRunMessage } from 'jest-util';
|
|
9
9
|
import path from 'node:path';
|
|
@@ -15,7 +15,7 @@ import { createHookQueue } from './hook-queue.js';
|
|
|
15
15
|
import { createClientLogCollector, } from './client-log-handler.js';
|
|
16
16
|
import { createActionHooksPlugin } from './action-hooks.js';
|
|
17
17
|
import { createResourceLockManager, } from './resource-lock.js';
|
|
18
|
-
import { observeBridgeEvents, observeMetroEvents } from './diagnostics/index.js';
|
|
18
|
+
import { observeBridgeEvents, observeMetroEvents, startSessionTelemetry, } from './diagnostics/index.js';
|
|
19
19
|
import { resolveHarnessMetroPort } from './metro-port.js';
|
|
20
20
|
import { getAdditionalCliArgs } from './cli-args.js';
|
|
21
21
|
import { logMetroCacheReused, logMetroPortFallback, logNativeCoverageCollected, logRunnerStarting, logRunnerStillWaitingInQueue, logRunnerWaitingInQueue, logTestEnvironmentReady, logTestRunHeader, } from './logs.js';
|
|
@@ -25,6 +25,8 @@ const ignorePromiseRejection = () => undefined;
|
|
|
25
25
|
const isBridgeDisconnectError = (error) => error instanceof Error && error.message === 'App bridge disconnected';
|
|
26
26
|
const TEST_RUN_BRIDGE_STABILITY_WAIT_MS = 500;
|
|
27
27
|
const DISPOSE_HOOK_TIMEOUT_MS = 5_000;
|
|
28
|
+
// Upper bound on waiting for end-of-run telemetry before tearing resources down.
|
|
29
|
+
const DISPOSE_TELEMETRY_TIMEOUT_MS = 10_000;
|
|
28
30
|
const waitForBoundedSettlement = async (promise, timeoutMs) => {
|
|
29
31
|
const timeout = delay(timeoutMs);
|
|
30
32
|
try {
|
|
@@ -99,7 +101,7 @@ export const withPlatformReadyTimeout = async (options) => {
|
|
|
99
101
|
return await Promise.race([
|
|
100
102
|
options.work(options.signal),
|
|
101
103
|
abortWait.promise.catch(() => {
|
|
102
|
-
throw new PlatformReadyTimeoutError(options.timeout);
|
|
104
|
+
throw new PlatformReadyTimeoutError(options.timeout, options.getHostPressure?.() ?? null);
|
|
103
105
|
}),
|
|
104
106
|
]);
|
|
105
107
|
}
|
|
@@ -107,6 +109,35 @@ export const withPlatformReadyTimeout = async (options) => {
|
|
|
107
109
|
abortWait.cancel();
|
|
108
110
|
}
|
|
109
111
|
};
|
|
112
|
+
// A saturated host is exactly where `ps` is slow; it took over 10s on CI.
|
|
113
|
+
const HOST_HEALTH_PROCESS_LISTING_TIMEOUT_MS = 30_000;
|
|
114
|
+
// Tells, before anything is booted, whether the host is already saturated by
|
|
115
|
+
// something other than this run. Only a warning: a saturated runner can still
|
|
116
|
+
// pass, but a later startup timeout then has an explanation.
|
|
117
|
+
const reportSaturatedHost = async (diagnostics) => {
|
|
118
|
+
try {
|
|
119
|
+
const verdict = await checkHostHealth();
|
|
120
|
+
diagnostics.mark('telemetry.host_health', {
|
|
121
|
+
load1: verdict.load1,
|
|
122
|
+
cpuCount: verdict.cpuCount,
|
|
123
|
+
idlePct: verdict.idlePct ?? undefined,
|
|
124
|
+
saturated: verdict.saturated,
|
|
125
|
+
});
|
|
126
|
+
if (!verdict.saturated) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const processes = await readProcessUsage(AbortSignal.timeout(HOST_HEALTH_PROCESS_LISTING_TIMEOUT_MS)).catch(() => null);
|
|
130
|
+
const message = formatSaturatedHostMessage(verdict, processes?.topByCpu);
|
|
131
|
+
sessionLogger.warn(message);
|
|
132
|
+
const annotation = formatGitHubWarning('Saturated CI runner', message);
|
|
133
|
+
if (annotation) {
|
|
134
|
+
process.stdout.write(`${annotation}\n`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
sessionLogger.debug('host health check failed: %s', error);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
110
141
|
export const waitForStartupCrash = async ({ crashMonitor, detectNativeCrashes, testFilePath, signal, }) => {
|
|
111
142
|
if (!detectNativeCrashes) {
|
|
112
143
|
return await waitForAbort(signal).promise;
|
|
@@ -257,6 +288,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
257
288
|
preRunMessage.remove(process.stderr);
|
|
258
289
|
const setupStartedAt = Date.now();
|
|
259
290
|
const { harnessConfig, platform, projectRoot, configProjectRoot } = await loadConfig(globalConfig);
|
|
291
|
+
// Metro platform for prewarm and startup detection. Vega apps request
|
|
292
|
+
// "kepler" bundles while the harness platform id stays "vega".
|
|
293
|
+
const bundlePlatform = platform.bundlePlatform ?? platform.platformId;
|
|
260
294
|
applyEnvVars(harnessConfig, globalConfig);
|
|
261
295
|
const diagnostics = createDiagnostics({
|
|
262
296
|
enabled: isDiagnosticsEnabled(harnessConfig),
|
|
@@ -265,6 +299,23 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
265
299
|
label: 'session.config.load',
|
|
266
300
|
duration: Date.now() - setupStartedAt,
|
|
267
301
|
});
|
|
302
|
+
// The one composition point for run telemetry. Everything below only emits
|
|
303
|
+
// lifecycle phases; probes translate them and polled data into diagnostics.
|
|
304
|
+
const telemetry = startSessionTelemetry({
|
|
305
|
+
diagnostics,
|
|
306
|
+
config: harnessConfig,
|
|
307
|
+
projectRoot,
|
|
308
|
+
});
|
|
309
|
+
telemetry.phase('session.created');
|
|
310
|
+
if (isCI()) {
|
|
311
|
+
void reportSaturatedHost(diagnostics);
|
|
312
|
+
}
|
|
313
|
+
// Telemetry runs timers and listens on `exit`, so a setup failure before the
|
|
314
|
+
// main try/catch below must still stop it.
|
|
315
|
+
const disposeTelemetryOnFailure = (work) => work.catch(async (error) => {
|
|
316
|
+
await telemetry.dispose();
|
|
317
|
+
throw error;
|
|
318
|
+
});
|
|
268
319
|
sessionLogger.debug('creating session for runner=%s platform=%s', platform.name, platform.platformId);
|
|
269
320
|
// Single, session-lifetime AbortController: it aborts on early SIGTERM/SIGINT
|
|
270
321
|
// during setup (below) and, once the session is up, in dispose()'s finally
|
|
@@ -280,11 +331,11 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
280
331
|
};
|
|
281
332
|
process.once('SIGTERM', onEarlySignal);
|
|
282
333
|
process.once('SIGINT', onEarlySignal);
|
|
283
|
-
const resourceLockKey = await (platform.getResourceLockKey?.() ?? getDefaultResourceLockKey(platform));
|
|
334
|
+
const resourceLockKey = await disposeTelemetryOnFailure(Promise.resolve(platform.getResourceLockKey?.() ?? getDefaultResourceLockKey(platform)));
|
|
284
335
|
let didWaitForResourceLock = false;
|
|
285
336
|
let lastStillWaitingLogAt = 0;
|
|
286
337
|
logTestRunHeader(platform);
|
|
287
|
-
const resourceLease = await diagnostics.measure('session.lock.wait', () => lockManager.acquire(resourceLockKey, {
|
|
338
|
+
const resourceLease = await disposeTelemetryOnFailure(diagnostics.measure('session.lock.wait', () => lockManager.acquire(resourceLockKey, {
|
|
288
339
|
signal: sessionController.signal,
|
|
289
340
|
onWait: () => {
|
|
290
341
|
didWaitForResourceLock = true;
|
|
@@ -298,7 +349,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
298
349
|
logRunnerStillWaitingInQueue(platform);
|
|
299
350
|
sessionLogger.debug('still waiting in queue for runner=%s key=%s elapsedMs=%d', platform.name, resourceLockKey, elapsedMs);
|
|
300
351
|
},
|
|
301
|
-
}), { lockKey: resourceLockKey });
|
|
352
|
+
}), { lockKey: resourceLockKey }));
|
|
302
353
|
if (didWaitForResourceLock)
|
|
303
354
|
logRunnerStarting(platform);
|
|
304
355
|
sessionLogger.debug('resource lock acquired for runner=%s key=%s', platform.name, resourceLockKey);
|
|
@@ -344,7 +395,10 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
344
395
|
let currentRun = null;
|
|
345
396
|
const getCurrentRunId = () => currentRun?.runId;
|
|
346
397
|
const clientLogCollector = createClientLogCollector();
|
|
347
|
-
const context = {
|
|
398
|
+
const context = {
|
|
399
|
+
platform,
|
|
400
|
+
updateSnapshot: globalConfig.updateSnapshot,
|
|
401
|
+
};
|
|
348
402
|
const crashArtifactWriter = createCrashArtifactWriter({
|
|
349
403
|
runnerName: platform.name,
|
|
350
404
|
platformId: platform.platformId,
|
|
@@ -354,6 +408,11 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
354
408
|
noServer: true,
|
|
355
409
|
timeout: runtimeConfig.bridgeTimeout,
|
|
356
410
|
context,
|
|
411
|
+
liveness: {
|
|
412
|
+
// Loading a test file runs user code that has no timeout of its
|
|
413
|
+
// own, so it gets the same allowance as starting the bundle.
|
|
414
|
+
jsBlockBudget: { fileLoadMs: runtimeConfig.bundleStartTimeout },
|
|
415
|
+
},
|
|
357
416
|
}));
|
|
358
417
|
sessionLogger.debug('bridge initialized on Metro websocket path %s', HARNESS_BRIDGE_PATH);
|
|
359
418
|
let metroInstance;
|
|
@@ -365,6 +424,28 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
365
424
|
let initializationCancelled = false;
|
|
366
425
|
// Noop until the Metro instance resolves and the deriver is attached.
|
|
367
426
|
let disposeMetroDiagnostics = () => undefined;
|
|
427
|
+
const eagerPrewarm = runtimeConfig.eagerPrewarm !== false;
|
|
428
|
+
// On small hosts the bundle build and the platform boot compete for the
|
|
429
|
+
// same few cores, so the platform may ask to boot first.
|
|
430
|
+
const deferPrewarm = eagerPrewarm && (platform.deferMetroPrewarm?.() ?? false);
|
|
431
|
+
if (deferPrewarm) {
|
|
432
|
+
sessionLogger.debug('deferring Metro prewarm until the platform is ready');
|
|
433
|
+
}
|
|
434
|
+
const startPrewarm = (instance) => {
|
|
435
|
+
// prewarm() memoizes this first call, so its session-lifetime setup
|
|
436
|
+
// signal is the one that matters — later callers' signals cannot
|
|
437
|
+
// cancel it. Not awaited: callers must not wait for the bundle.
|
|
438
|
+
const prewarmSpan = diagnostics.start('metro.prewarm', { deferred: deferPrewarm });
|
|
439
|
+
instance
|
|
440
|
+
.prewarm({
|
|
441
|
+
platform: bundlePlatform,
|
|
442
|
+
signal: sessionController.signal,
|
|
443
|
+
})
|
|
444
|
+
.then((completed) => prewarmSpan.end({ completed }))
|
|
445
|
+
// Swallow AbortError on teardown so it never becomes an
|
|
446
|
+
// unhandled rejection; non-abort failures resolve false.
|
|
447
|
+
.catch((error) => prewarmSpan.fail(error));
|
|
448
|
+
};
|
|
368
449
|
try {
|
|
369
450
|
[metroInstance, platformInstance] = await Promise.all([
|
|
370
451
|
diagnostics.measure('metro.init', () => getMetroInstance({
|
|
@@ -383,30 +464,20 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
383
464
|
}
|
|
384
465
|
initialized.metro = instance;
|
|
385
466
|
sessionLogger.debug('Metro initialized');
|
|
467
|
+
telemetry.phase('metro.ready');
|
|
386
468
|
// Attach the Metro diagnostics deriver before the eager prewarm
|
|
387
469
|
// fires so its bundle build/request events land in the trace.
|
|
388
470
|
disposeMetroDiagnostics = observeMetroEvents(instance.events, diagnostics);
|
|
389
|
-
if (
|
|
471
|
+
if (eagerPrewarm && !deferPrewarm) {
|
|
390
472
|
// Kick off the first bundle build while the platform is still booting.
|
|
391
|
-
|
|
392
|
-
// signal is the one that matters — later callers' signals cannot
|
|
393
|
-
// cancel it. Not awaited: metro.init must resolve immediately.
|
|
394
|
-
const prewarmSpan = diagnostics.start('metro.prewarm');
|
|
395
|
-
instance
|
|
396
|
-
.prewarm({
|
|
397
|
-
platform: platform.platformId,
|
|
398
|
-
signal: sessionController.signal,
|
|
399
|
-
})
|
|
400
|
-
.then((completed) => prewarmSpan.end({ completed }))
|
|
401
|
-
// Swallow AbortError on teardown so it never becomes an
|
|
402
|
-
// unhandled rejection; non-abort failures resolve false.
|
|
403
|
-
.catch((error) => prewarmSpan.fail(error));
|
|
473
|
+
startPrewarm(instance);
|
|
404
474
|
}
|
|
405
475
|
return instance;
|
|
406
476
|
})),
|
|
407
477
|
diagnostics.measure('platform.init', () => withPlatformReadyTimeout({
|
|
408
478
|
timeout: runtimeConfig.platformReadyTimeout,
|
|
409
479
|
signal: sessionController.signal,
|
|
480
|
+
getHostPressure: telemetry.hostPressure,
|
|
410
481
|
work: async (signal) => {
|
|
411
482
|
return await import(platform.runner).then((module) => module.default(platform.config, runtimeConfig, {
|
|
412
483
|
signal,
|
|
@@ -418,6 +489,8 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
418
489
|
}
|
|
419
490
|
initialized.platform = instance;
|
|
420
491
|
sessionLogger.debug('platform runner initialized');
|
|
492
|
+
telemetry.attachPlatform(instance);
|
|
493
|
+
telemetry.phase('platform.ready');
|
|
421
494
|
return instance;
|
|
422
495
|
});
|
|
423
496
|
},
|
|
@@ -437,6 +510,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
437
510
|
]);
|
|
438
511
|
throw error;
|
|
439
512
|
}
|
|
513
|
+
if (deferPrewarm) {
|
|
514
|
+
startPrewarm(metroInstance);
|
|
515
|
+
}
|
|
440
516
|
const appLaunchOptions = platform.config.appLaunchOptions;
|
|
441
517
|
let currentAppSession = null;
|
|
442
518
|
const crashMonitor = createCrashMonitor();
|
|
@@ -462,7 +538,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
462
538
|
const appReadyBaseOptions = {
|
|
463
539
|
metroInstance,
|
|
464
540
|
bridge,
|
|
465
|
-
platformId:
|
|
541
|
+
platformId: bundlePlatform,
|
|
466
542
|
bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
|
|
467
543
|
readyTimeout: runtimeConfig.bridgeTimeout,
|
|
468
544
|
maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
|
|
@@ -525,6 +601,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
525
601
|
// Abort only cancels finite/in-flight work. Ownership remains explicit
|
|
526
602
|
// below, so this cannot race a platform or app-session disposal path.
|
|
527
603
|
sessionController.abort();
|
|
604
|
+
telemetry.phase('dispose');
|
|
528
605
|
const hookWork = (async () => {
|
|
529
606
|
await hooks.drain();
|
|
530
607
|
await pluginManager.callHook('harness:after-run', {
|
|
@@ -551,6 +628,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
551
628
|
else if (hookResult.status === 'rejected') {
|
|
552
629
|
hookError = hookResult.reason;
|
|
553
630
|
}
|
|
631
|
+
// End-of-run telemetry has to observe the platform before it is torn
|
|
632
|
+
// down, but must never be able to hold teardown up for long.
|
|
633
|
+
await waitForBoundedSettlement(telemetry.settled(), DISPOSE_TELEMETRY_TIMEOUT_MS);
|
|
554
634
|
if (runtimeConfig.forwardClientLogs) {
|
|
555
635
|
metroInstance.events.removeListener(clientLogListener);
|
|
556
636
|
}
|
|
@@ -607,7 +687,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
607
687
|
disposeSpan.end();
|
|
608
688
|
};
|
|
609
689
|
const dispose = (reason = 'normal') => {
|
|
610
|
-
disposePromise ??= disposeOnce(reason);
|
|
690
|
+
disposePromise ??= disposeOnce(reason).finally(() => telemetry.dispose());
|
|
611
691
|
return disposePromise;
|
|
612
692
|
};
|
|
613
693
|
// Switch from setup-phase signal handling to dispose-based handling now
|
|
@@ -664,6 +744,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
664
744
|
await hooks.drain();
|
|
665
745
|
sessionLogger.debug('app is ready for %s', testFilePath);
|
|
666
746
|
appReadySpan.end({ file: testFilePath, reused: false });
|
|
747
|
+
telemetry.phase('app.ready', { file: testFilePath });
|
|
667
748
|
};
|
|
668
749
|
// Shared by restartApp (crash/timeout recovery) and the 'process' reset
|
|
669
750
|
// strategy: kill the current app session and either wait for a fresh one
|
|
@@ -789,6 +870,8 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
789
870
|
config: runtimeConfig,
|
|
790
871
|
context,
|
|
791
872
|
diagnostics,
|
|
873
|
+
sessionId: telemetry.sessionId,
|
|
874
|
+
lifecycle: telemetry.lifecycle,
|
|
792
875
|
onTestRunnerEvent: (listener) => {
|
|
793
876
|
testRunnerEventListeners.add(listener);
|
|
794
877
|
return () => {
|
|
@@ -808,6 +891,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
808
891
|
},
|
|
809
892
|
setRunState: (state) => {
|
|
810
893
|
currentRun = state;
|
|
894
|
+
if (state) {
|
|
895
|
+
telemetry.trackRun(state.runId);
|
|
896
|
+
}
|
|
811
897
|
},
|
|
812
898
|
dispose: async (reason = 'normal') => {
|
|
813
899
|
try {
|
|
@@ -826,6 +912,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
826
912
|
// Cancels any inflight init work still holding sessionController.signal
|
|
827
913
|
// (e.g. a platform init call abandoned after a readiness timeout race).
|
|
828
914
|
sessionController.abort();
|
|
915
|
+
await telemetry.dispose();
|
|
829
916
|
await Promise.allSettled([resourceLease.release(), metroPortLease?.release()]);
|
|
830
917
|
if (receivedEarlySignal) {
|
|
831
918
|
process.exit(1);
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,2BAA2B,EAC3B,IAAI,EACJ,UAAU,EACV,iBAAiB,EACjB,WAAW,EACZ,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,2BAA2B,EAC3B,IAAI,EACJ,UAAU,EACV,iBAAiB,EACjB,WAAW,EACZ,MAAM,aAAa,CAAC;AAOrB,KAAK,iBAAiB,CAAC,IAAI,SAAS,MAAM,UAAU,IAAI,CACtD,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,KACxB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,CAAC,OAAO,OAAO,WAAY,YAAW,2BAA2B;;IACrE,QAAQ,CAAC,QAAQ,QAAQ;IACzB,QAAQ,CAAC,qBAAqB,EAAG,IAAI,CAAU;gBAKnC,YAAY,EAAE,MAAM,CAAC,YAAY;IAI7C,EAAE,CAAC,IAAI,SAAS,MAAM,UAAU,EAC9B,SAAS,EAAE,IAAI,EACf,QAAQ,EAAE,iBAAiB,CAAC,IAAI,CAAC,GAChC,MAAM,IAAI;IAaP,QAAQ,CACZ,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC;CA8CjB"}
|