@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/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;
|
|
@@ -268,6 +299,23 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
268
299
|
label: 'session.config.load',
|
|
269
300
|
duration: Date.now() - setupStartedAt,
|
|
270
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
|
+
});
|
|
271
319
|
sessionLogger.debug('creating session for runner=%s platform=%s', platform.name, platform.platformId);
|
|
272
320
|
// Single, session-lifetime AbortController: it aborts on early SIGTERM/SIGINT
|
|
273
321
|
// during setup (below) and, once the session is up, in dispose()'s finally
|
|
@@ -283,11 +331,11 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
283
331
|
};
|
|
284
332
|
process.once('SIGTERM', onEarlySignal);
|
|
285
333
|
process.once('SIGINT', onEarlySignal);
|
|
286
|
-
const resourceLockKey = await (platform.getResourceLockKey?.() ?? getDefaultResourceLockKey(platform));
|
|
334
|
+
const resourceLockKey = await disposeTelemetryOnFailure(Promise.resolve(platform.getResourceLockKey?.() ?? getDefaultResourceLockKey(platform)));
|
|
287
335
|
let didWaitForResourceLock = false;
|
|
288
336
|
let lastStillWaitingLogAt = 0;
|
|
289
337
|
logTestRunHeader(platform);
|
|
290
|
-
const resourceLease = await diagnostics.measure('session.lock.wait', () => lockManager.acquire(resourceLockKey, {
|
|
338
|
+
const resourceLease = await disposeTelemetryOnFailure(diagnostics.measure('session.lock.wait', () => lockManager.acquire(resourceLockKey, {
|
|
291
339
|
signal: sessionController.signal,
|
|
292
340
|
onWait: () => {
|
|
293
341
|
didWaitForResourceLock = true;
|
|
@@ -301,7 +349,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
301
349
|
logRunnerStillWaitingInQueue(platform);
|
|
302
350
|
sessionLogger.debug('still waiting in queue for runner=%s key=%s elapsedMs=%d', platform.name, resourceLockKey, elapsedMs);
|
|
303
351
|
},
|
|
304
|
-
}), { lockKey: resourceLockKey });
|
|
352
|
+
}), { lockKey: resourceLockKey }));
|
|
305
353
|
if (didWaitForResourceLock)
|
|
306
354
|
logRunnerStarting(platform);
|
|
307
355
|
sessionLogger.debug('resource lock acquired for runner=%s key=%s', platform.name, resourceLockKey);
|
|
@@ -347,7 +395,10 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
347
395
|
let currentRun = null;
|
|
348
396
|
const getCurrentRunId = () => currentRun?.runId;
|
|
349
397
|
const clientLogCollector = createClientLogCollector();
|
|
350
|
-
const context = {
|
|
398
|
+
const context = {
|
|
399
|
+
platform,
|
|
400
|
+
updateSnapshot: globalConfig.updateSnapshot,
|
|
401
|
+
};
|
|
351
402
|
const crashArtifactWriter = createCrashArtifactWriter({
|
|
352
403
|
runnerName: platform.name,
|
|
353
404
|
platformId: platform.platformId,
|
|
@@ -357,6 +408,11 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
357
408
|
noServer: true,
|
|
358
409
|
timeout: runtimeConfig.bridgeTimeout,
|
|
359
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
|
+
},
|
|
360
416
|
}));
|
|
361
417
|
sessionLogger.debug('bridge initialized on Metro websocket path %s', HARNESS_BRIDGE_PATH);
|
|
362
418
|
let metroInstance;
|
|
@@ -368,6 +424,28 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
368
424
|
let initializationCancelled = false;
|
|
369
425
|
// Noop until the Metro instance resolves and the deriver is attached.
|
|
370
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
|
+
};
|
|
371
449
|
try {
|
|
372
450
|
[metroInstance, platformInstance] = await Promise.all([
|
|
373
451
|
diagnostics.measure('metro.init', () => getMetroInstance({
|
|
@@ -386,30 +464,20 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
386
464
|
}
|
|
387
465
|
initialized.metro = instance;
|
|
388
466
|
sessionLogger.debug('Metro initialized');
|
|
467
|
+
telemetry.phase('metro.ready');
|
|
389
468
|
// Attach the Metro diagnostics deriver before the eager prewarm
|
|
390
469
|
// fires so its bundle build/request events land in the trace.
|
|
391
470
|
disposeMetroDiagnostics = observeMetroEvents(instance.events, diagnostics);
|
|
392
|
-
if (
|
|
471
|
+
if (eagerPrewarm && !deferPrewarm) {
|
|
393
472
|
// Kick off the first bundle build while the platform is still booting.
|
|
394
|
-
|
|
395
|
-
// signal is the one that matters — later callers' signals cannot
|
|
396
|
-
// cancel it. Not awaited: metro.init must resolve immediately.
|
|
397
|
-
const prewarmSpan = diagnostics.start('metro.prewarm');
|
|
398
|
-
instance
|
|
399
|
-
.prewarm({
|
|
400
|
-
platform: bundlePlatform,
|
|
401
|
-
signal: sessionController.signal,
|
|
402
|
-
})
|
|
403
|
-
.then((completed) => prewarmSpan.end({ completed }))
|
|
404
|
-
// Swallow AbortError on teardown so it never becomes an
|
|
405
|
-
// unhandled rejection; non-abort failures resolve false.
|
|
406
|
-
.catch((error) => prewarmSpan.fail(error));
|
|
473
|
+
startPrewarm(instance);
|
|
407
474
|
}
|
|
408
475
|
return instance;
|
|
409
476
|
})),
|
|
410
477
|
diagnostics.measure('platform.init', () => withPlatformReadyTimeout({
|
|
411
478
|
timeout: runtimeConfig.platformReadyTimeout,
|
|
412
479
|
signal: sessionController.signal,
|
|
480
|
+
getHostPressure: telemetry.hostPressure,
|
|
413
481
|
work: async (signal) => {
|
|
414
482
|
return await import(platform.runner).then((module) => module.default(platform.config, runtimeConfig, {
|
|
415
483
|
signal,
|
|
@@ -421,6 +489,8 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
421
489
|
}
|
|
422
490
|
initialized.platform = instance;
|
|
423
491
|
sessionLogger.debug('platform runner initialized');
|
|
492
|
+
telemetry.attachPlatform(instance);
|
|
493
|
+
telemetry.phase('platform.ready');
|
|
424
494
|
return instance;
|
|
425
495
|
});
|
|
426
496
|
},
|
|
@@ -440,6 +510,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
440
510
|
]);
|
|
441
511
|
throw error;
|
|
442
512
|
}
|
|
513
|
+
if (deferPrewarm) {
|
|
514
|
+
startPrewarm(metroInstance);
|
|
515
|
+
}
|
|
443
516
|
const appLaunchOptions = platform.config.appLaunchOptions;
|
|
444
517
|
let currentAppSession = null;
|
|
445
518
|
const crashMonitor = createCrashMonitor();
|
|
@@ -528,6 +601,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
528
601
|
// Abort only cancels finite/in-flight work. Ownership remains explicit
|
|
529
602
|
// below, so this cannot race a platform or app-session disposal path.
|
|
530
603
|
sessionController.abort();
|
|
604
|
+
telemetry.phase('dispose');
|
|
531
605
|
const hookWork = (async () => {
|
|
532
606
|
await hooks.drain();
|
|
533
607
|
await pluginManager.callHook('harness:after-run', {
|
|
@@ -554,6 +628,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
554
628
|
else if (hookResult.status === 'rejected') {
|
|
555
629
|
hookError = hookResult.reason;
|
|
556
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);
|
|
557
634
|
if (runtimeConfig.forwardClientLogs) {
|
|
558
635
|
metroInstance.events.removeListener(clientLogListener);
|
|
559
636
|
}
|
|
@@ -610,7 +687,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
610
687
|
disposeSpan.end();
|
|
611
688
|
};
|
|
612
689
|
const dispose = (reason = 'normal') => {
|
|
613
|
-
disposePromise ??= disposeOnce(reason);
|
|
690
|
+
disposePromise ??= disposeOnce(reason).finally(() => telemetry.dispose());
|
|
614
691
|
return disposePromise;
|
|
615
692
|
};
|
|
616
693
|
// Switch from setup-phase signal handling to dispose-based handling now
|
|
@@ -667,6 +744,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
667
744
|
await hooks.drain();
|
|
668
745
|
sessionLogger.debug('app is ready for %s', testFilePath);
|
|
669
746
|
appReadySpan.end({ file: testFilePath, reused: false });
|
|
747
|
+
telemetry.phase('app.ready', { file: testFilePath });
|
|
670
748
|
};
|
|
671
749
|
// Shared by restartApp (crash/timeout recovery) and the 'process' reset
|
|
672
750
|
// strategy: kill the current app session and either wait for a fresh one
|
|
@@ -792,6 +870,8 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
792
870
|
config: runtimeConfig,
|
|
793
871
|
context,
|
|
794
872
|
diagnostics,
|
|
873
|
+
sessionId: telemetry.sessionId,
|
|
874
|
+
lifecycle: telemetry.lifecycle,
|
|
795
875
|
onTestRunnerEvent: (listener) => {
|
|
796
876
|
testRunnerEventListeners.add(listener);
|
|
797
877
|
return () => {
|
|
@@ -811,6 +891,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
811
891
|
},
|
|
812
892
|
setRunState: (state) => {
|
|
813
893
|
currentRun = state;
|
|
894
|
+
if (state) {
|
|
895
|
+
telemetry.trackRun(state.runId);
|
|
896
|
+
}
|
|
814
897
|
},
|
|
815
898
|
dispose: async (reason = 'normal') => {
|
|
816
899
|
try {
|
|
@@ -829,6 +912,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
829
912
|
// Cancels any inflight init work still holding sessionController.signal
|
|
830
913
|
// (e.g. a platform init call abandoned after a readiness timeout race).
|
|
831
914
|
sessionController.abort();
|
|
915
|
+
await telemetry.dispose();
|
|
832
916
|
await Promise.allSettled([resourceLease.release(), metroPortLease?.release()]);
|
|
833
917
|
if (receivedEarlySignal) {
|
|
834
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"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHarnessSession } from './harness-session.js';
|
|
2
|
+
import { getWatchSession } from './watch-session.js';
|
|
2
3
|
import { executeRun } from './execute-run.js';
|
|
3
4
|
import { HarnessError } from '@react-native-harness/tools';
|
|
4
5
|
import { getErrorMessage } from './logs.js';
|
|
@@ -6,7 +7,6 @@ export default class JestHarness {
|
|
|
6
7
|
isSerial = true;
|
|
7
8
|
supportsEventEmitters = true;
|
|
8
9
|
#globalConfig;
|
|
9
|
-
#session = null;
|
|
10
10
|
#listeners = new Map();
|
|
11
11
|
constructor(globalConfig) {
|
|
12
12
|
this.#globalConfig = globalConfig;
|
|
@@ -27,11 +27,12 @@ export default class JestHarness {
|
|
|
27
27
|
throw new Error('Parallel test running is not supported');
|
|
28
28
|
}
|
|
29
29
|
const isWatchMode = this.#globalConfig.watch || this.#globalConfig.watchAll;
|
|
30
|
+
let session = null;
|
|
30
31
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
await executeRun(
|
|
32
|
+
session = isWatchMode
|
|
33
|
+
? await getWatchSession(this.#globalConfig)
|
|
34
|
+
: await createHarnessSession(this.#globalConfig);
|
|
35
|
+
await executeRun(session, tests, watcher, this.#emit, this.#globalConfig);
|
|
35
36
|
}
|
|
36
37
|
catch (error) {
|
|
37
38
|
if (error instanceof HarnessError) {
|
|
@@ -41,8 +42,7 @@ export default class JestHarness {
|
|
|
41
42
|
}
|
|
42
43
|
finally {
|
|
43
44
|
if (!isWatchMode) {
|
|
44
|
-
await
|
|
45
|
-
this.#session = null;
|
|
45
|
+
await session?.dispose();
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
package/dist/jest-preset.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jest-preset.d.ts","sourceRoot":"","sources":["../src/jest-preset.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"jest-preset.d.ts","sourceRoot":"","sources":["../src/jest-preset.ts"],"names":[],"mappings":";;;;;;AAAA,wBAoBE"}
|