@react-native-harness/jest 1.4.1 → 1.6.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/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +2 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +79 -32
- package/dist/resource-lock.d.ts.map +1 -1
- package/dist/resource-lock.js +12 -2
- package/dist/toJestStatus.d.ts +4 -0
- package/dist/toJestStatus.d.ts.map +1 -0
- package/dist/toJestStatus.js +2 -0
- package/dist/toTestResult.d.ts +3 -2
- package/dist/toTestResult.d.ts.map +1 -1
- package/dist/toTestResult.js +2 -1
- package/package.json +8 -8
- package/src/execute-run.ts +2 -1
- package/src/harness-session.ts +91 -29
- package/src/resource-lock.ts +15 -2
- package/src/toJestStatus.ts +6 -0
- package/src/toTestResult.ts +5 -3
|
@@ -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;AAGrB,OAAO,EAAE,KAAK,cAAc,EAAwB,MAAM,sBAAsB,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;AAGrB,OAAO,EAAE,KAAK,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AA8BjF,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;AA+FnB,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,CAsOd,CAAC"}
|
package/dist/execute-run.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createPlatformSkippedTestResult, shouldRunHarnessTestFile, } from './te
|
|
|
8
8
|
import { formatHarnessErrorMessage } from './format-harness-error.js';
|
|
9
9
|
import { logger } from '@react-native-harness/tools';
|
|
10
10
|
import { printSummary, writeTraceFile } from './diagnostics/index.js';
|
|
11
|
+
import { toJestStatus } from './toJestStatus.js';
|
|
11
12
|
const diagnosticsLogger = logger.child('diagnostics');
|
|
12
13
|
const createRunSummary = () => ({ passed: 0, failed: 0, skipped: 0, todo: 0 });
|
|
13
14
|
const applyJestResultToSummary = (summary, result) => {
|
|
@@ -59,7 +60,7 @@ const emitHarnessTestFinished = async (emitEvent, event) => {
|
|
|
59
60
|
location,
|
|
60
61
|
numPassingAsserts: event.status === 'passed' ? 1 : 0,
|
|
61
62
|
startedAt: event.startedAt,
|
|
62
|
-
status: event.status,
|
|
63
|
+
status: toJestStatus(event.status),
|
|
63
64
|
title: event.name,
|
|
64
65
|
};
|
|
65
66
|
await emitEvent('test-case-result', event.file, testCaseResult);
|
|
@@ -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, logger, getTimeoutSignal, ensureSignalsDeliverable, waitForAbort, } from '@react-native-harness/tools';
|
|
6
|
+
import { createCrashArtifactWriter, createDiagnostics, delay, logger, getTimeoutSignal, ensureSignalsDeliverable, waitForAbort, } 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';
|
|
@@ -24,6 +24,19 @@ const defaultResourceLockManager = createResourceLockManager();
|
|
|
24
24
|
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
|
+
const DISPOSE_HOOK_TIMEOUT_MS = 5_000;
|
|
28
|
+
const waitForBoundedSettlement = async (promise, timeoutMs) => {
|
|
29
|
+
const timeout = delay(timeoutMs);
|
|
30
|
+
try {
|
|
31
|
+
return await Promise.race([
|
|
32
|
+
promise.then((value) => ({ status: 'fulfilled', value }), (reason) => ({ status: 'rejected', reason })),
|
|
33
|
+
timeout.promise.then(() => null),
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
timeout.cancel();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
27
40
|
export const waitForBridgeDisconnectOrTimeout = async ({ bridge, connection, timeoutMs, }) => {
|
|
28
41
|
if (bridge.connection !== connection) {
|
|
29
42
|
return true;
|
|
@@ -244,6 +257,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
244
257
|
preRunMessage.remove(process.stderr);
|
|
245
258
|
const setupStartedAt = Date.now();
|
|
246
259
|
const { harnessConfig, platform, projectRoot, configProjectRoot } = await loadConfig(globalConfig);
|
|
260
|
+
// Metro platform for prewarm and startup detection. Vega apps request
|
|
261
|
+
// "kepler" bundles while the harness platform id stays "vega".
|
|
262
|
+
const bundlePlatform = platform.bundlePlatform ?? platform.platformId;
|
|
247
263
|
applyEnvVars(harnessConfig, globalConfig);
|
|
248
264
|
const diagnostics = createDiagnostics({
|
|
249
265
|
enabled: isDiagnosticsEnabled(harnessConfig),
|
|
@@ -260,7 +276,11 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
260
276
|
// child the runner threads it into.
|
|
261
277
|
ensureSignalsDeliverable();
|
|
262
278
|
const sessionController = new AbortController();
|
|
263
|
-
|
|
279
|
+
let receivedEarlySignal = false;
|
|
280
|
+
const onEarlySignal = () => {
|
|
281
|
+
receivedEarlySignal = true;
|
|
282
|
+
sessionController.abort();
|
|
283
|
+
};
|
|
264
284
|
process.once('SIGTERM', onEarlySignal);
|
|
265
285
|
process.once('SIGINT', onEarlySignal);
|
|
266
286
|
const resourceLockKey = await (platform.getResourceLockKey?.() ?? getDefaultResourceLockKey(platform));
|
|
@@ -341,6 +361,11 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
341
361
|
sessionLogger.debug('bridge initialized on Metro websocket path %s', HARNESS_BRIDGE_PATH);
|
|
342
362
|
let metroInstance;
|
|
343
363
|
let platformInstance;
|
|
364
|
+
const initialized = {
|
|
365
|
+
metro: null,
|
|
366
|
+
platform: null,
|
|
367
|
+
};
|
|
368
|
+
let initializationCancelled = false;
|
|
344
369
|
// Noop until the Metro instance resolves and the deriver is attached.
|
|
345
370
|
let disposeMetroDiagnostics = () => undefined;
|
|
346
371
|
try {
|
|
@@ -348,11 +373,18 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
348
373
|
diagnostics.measure('metro.init', () => getMetroInstance({
|
|
349
374
|
projectRoot,
|
|
350
375
|
harnessConfig: runtimeConfig,
|
|
376
|
+
metroConfigEnhancer: platform.metroConfigEnhancer,
|
|
377
|
+
platformId: platform.platformId,
|
|
378
|
+
platformConfig: platform.config,
|
|
351
379
|
websocketEndpoints: {
|
|
352
380
|
[HARNESS_BRIDGE_PATH]: bridge.ws,
|
|
353
381
|
},
|
|
354
382
|
watchMode: globalConfig.watch || globalConfig.watchAll,
|
|
355
383
|
}, sessionController.signal).then((instance) => {
|
|
384
|
+
if (initializationCancelled) {
|
|
385
|
+
return instance.dispose().then(() => instance);
|
|
386
|
+
}
|
|
387
|
+
initialized.metro = instance;
|
|
356
388
|
sessionLogger.debug('Metro initialized');
|
|
357
389
|
// Attach the Metro diagnostics deriver before the eager prewarm
|
|
358
390
|
// fires so its bundle build/request events land in the trace.
|
|
@@ -365,7 +397,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
365
397
|
const prewarmSpan = diagnostics.start('metro.prewarm');
|
|
366
398
|
instance
|
|
367
399
|
.prewarm({
|
|
368
|
-
platform:
|
|
400
|
+
platform: bundlePlatform,
|
|
369
401
|
signal: sessionController.signal,
|
|
370
402
|
})
|
|
371
403
|
.then((completed) => prewarmSpan.end({ completed }))
|
|
@@ -384,6 +416,10 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
384
416
|
crashArtifactWriter,
|
|
385
417
|
diagnostics,
|
|
386
418
|
})).then((instance) => {
|
|
419
|
+
if (initializationCancelled) {
|
|
420
|
+
return instance.dispose().then(() => instance);
|
|
421
|
+
}
|
|
422
|
+
initialized.platform = instance;
|
|
387
423
|
sessionLogger.debug('platform runner initialized');
|
|
388
424
|
return instance;
|
|
389
425
|
});
|
|
@@ -392,10 +428,16 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
392
428
|
]);
|
|
393
429
|
}
|
|
394
430
|
catch (error) {
|
|
395
|
-
//
|
|
396
|
-
//
|
|
431
|
+
// Roll back every resource whose initialization won a concurrent race.
|
|
432
|
+
// Leases are released by the outer catch.
|
|
433
|
+
initializationCancelled = true;
|
|
434
|
+
sessionController.abort();
|
|
397
435
|
disposeMetroDiagnostics();
|
|
398
|
-
await
|
|
436
|
+
await Promise.allSettled([
|
|
437
|
+
bridge.dispose(),
|
|
438
|
+
initialized.metro?.dispose(),
|
|
439
|
+
initialized.platform?.dispose(),
|
|
440
|
+
]);
|
|
399
441
|
throw error;
|
|
400
442
|
}
|
|
401
443
|
const appLaunchOptions = platform.config.appLaunchOptions;
|
|
@@ -423,7 +465,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
423
465
|
const appReadyBaseOptions = {
|
|
424
466
|
metroInstance,
|
|
425
467
|
bridge,
|
|
426
|
-
platformId:
|
|
468
|
+
platformId: bundlePlatform,
|
|
427
469
|
bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
|
|
428
470
|
readyTimeout: runtimeConfig.bridgeTimeout,
|
|
429
471
|
maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
|
|
@@ -483,7 +525,10 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
483
525
|
sessionLogger.debug('disposing session (reason=%s)', reason);
|
|
484
526
|
const disposeSpan = diagnostics.start('dispose.total', { reason });
|
|
485
527
|
let hookError;
|
|
486
|
-
|
|
528
|
+
// Abort only cancels finite/in-flight work. Ownership remains explicit
|
|
529
|
+
// below, so this cannot race a platform or app-session disposal path.
|
|
530
|
+
sessionController.abort();
|
|
531
|
+
const hookWork = (async () => {
|
|
487
532
|
await hooks.drain();
|
|
488
533
|
await pluginManager.callHook('harness:after-run', {
|
|
489
534
|
runId: currentRun?.runId,
|
|
@@ -501,9 +546,13 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
501
546
|
error: currentRun?.error,
|
|
502
547
|
});
|
|
503
548
|
await hooks.drain();
|
|
549
|
+
})();
|
|
550
|
+
const hookResult = await waitForBoundedSettlement(hookWork, DISPOSE_HOOK_TIMEOUT_MS);
|
|
551
|
+
if (hookResult === null) {
|
|
552
|
+
hookError = new Error(`Timed out after ${DISPOSE_HOOK_TIMEOUT_MS}ms while running disposal hooks.`);
|
|
504
553
|
}
|
|
505
|
-
|
|
506
|
-
hookError =
|
|
554
|
+
else if (hookResult.status === 'rejected') {
|
|
555
|
+
hookError = hookResult.reason;
|
|
507
556
|
}
|
|
508
557
|
if (runtimeConfig.forwardClientLogs) {
|
|
509
558
|
metroInstance.events.removeListener(clientLogListener);
|
|
@@ -532,28 +581,23 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
532
581
|
sessionLogger.warn('failed to collect native coverage: %s', error);
|
|
533
582
|
}
|
|
534
583
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
finally {
|
|
554
|
-
await resourceLease.release();
|
|
555
|
-
sessionController.abort();
|
|
556
|
-
}
|
|
584
|
+
// The app session (and its log streams) must finish before platform
|
|
585
|
+
// disposal can remove the underlying device. Run independent cleanup
|
|
586
|
+
// all-settled so one failure cannot strand another owned resource.
|
|
587
|
+
const appCleanup = await Promise.allSettled([
|
|
588
|
+
crashMonitor.dispose(),
|
|
589
|
+
disposeCurrentAppSession(),
|
|
590
|
+
]);
|
|
591
|
+
const infrastructureCleanup = await Promise.allSettled([
|
|
592
|
+
bridge.dispose(),
|
|
593
|
+
platformInstance.dispose(),
|
|
594
|
+
metroInstance.dispose(),
|
|
595
|
+
metroPortLease?.release(),
|
|
596
|
+
]);
|
|
597
|
+
const leaseCleanup = await Promise.allSettled([resourceLease.release()]);
|
|
598
|
+
const cleanupError = [...appCleanup, ...infrastructureCleanup, ...leaseCleanup]
|
|
599
|
+
.find((result) => result.status === 'rejected')
|
|
600
|
+
?.reason;
|
|
557
601
|
sessionLogger.debug('session resources disposed');
|
|
558
602
|
if (hookError) {
|
|
559
603
|
disposeSpan.fail(hookError);
|
|
@@ -786,6 +830,9 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
|
|
|
786
830
|
// (e.g. a platform init call abandoned after a readiness timeout race).
|
|
787
831
|
sessionController.abort();
|
|
788
832
|
await Promise.allSettled([resourceLease.release(), metroPortLease?.release()]);
|
|
833
|
+
if (receivedEarlySignal) {
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
789
836
|
throw error;
|
|
790
837
|
}
|
|
791
838
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-lock.d.ts","sourceRoot":"","sources":["../src/resource-lock.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAiBzE,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,CACP,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,0BAA0B,KACjC,OAAO,CAAC,aAAa,CAAC,CAAC;CAC7B,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5C,CAAC;AAqQF,eAAO,MAAM,yBAAyB,GACpC,UAAS,0BAA+B,KACvC,
|
|
1
|
+
{"version":3,"file":"resource-lock.d.ts","sourceRoot":"","sources":["../src/resource-lock.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAiBzE,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,CACP,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,0BAA0B,KACjC,OAAO,CAAC,aAAa,CAAC,CAAC;CAC7B,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5C,CAAC;AAqQF,eAAO,MAAM,yBAAyB,GACpC,UAAS,0BAA+B,KACvC,mBA+KF,CAAC"}
|
package/dist/resource-lock.js
CHANGED
|
@@ -239,8 +239,18 @@ export const createResourceLockManager = (options = {}) => {
|
|
|
239
239
|
if (released) {
|
|
240
240
|
return;
|
|
241
241
|
}
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
try {
|
|
243
|
+
await writeJsonFileAtomic(paths.ownerFilePath, nextMetadata);
|
|
244
|
+
scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
// A failed refresh is not fatal by design: the lock goes stale
|
|
248
|
+
// and another holder reclaims it. Swallow it so a transient
|
|
249
|
+
// write error (e.g. the owner file racing a concurrent release,
|
|
250
|
+
// or an EPERM on Windows when the directory is being torn down)
|
|
251
|
+
// never surfaces as an unhandled rejection from this interval.
|
|
252
|
+
scopedLogger.debug('heartbeat refresh for ticket %s failed: %s', ticketId, error instanceof Error ? error.message : String(error));
|
|
253
|
+
}
|
|
244
254
|
}
|
|
245
255
|
finally {
|
|
246
256
|
heartbeatInFlight = false;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toJestStatus.d.ts","sourceRoot":"","sources":["../src/toJestStatus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,eAAO,MAAM,YAAY,GAAI,QAAQ,gBAAgB,KAAG,MACb,CAAC"}
|
package/dist/toTestResult.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TestResult } from '@jest/test-result';
|
|
2
|
+
import type { TestResultStatus } from '@react-native-harness/bridge';
|
|
2
3
|
export type Options = {
|
|
3
4
|
stats: {
|
|
4
5
|
failures: number;
|
|
@@ -16,7 +17,7 @@ export type Options = {
|
|
|
16
17
|
testPath?: string;
|
|
17
18
|
title?: string;
|
|
18
19
|
fullName?: string;
|
|
19
|
-
status:
|
|
20
|
+
status: TestResultStatus;
|
|
20
21
|
location?: {
|
|
21
22
|
column: number;
|
|
22
23
|
line: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toTestResult.d.ts","sourceRoot":"","sources":["../src/toTestResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"toTestResult.d.ts","sourceRoot":"","sources":["../src/toTestResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,MAAM,MAAM,OAAO,GAAG;IACpB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,KAAK,CAAC;QACX,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,gBAAgB,CAAC;QACzB,QAAQ,CAAC,EAAE;YACT,MAAM,EAAE,MAAM,CAAC;YACf,IAAI,EAAE,MAAM,CAAC;SACd,CAAC;QACF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC,CAAC;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC;CACnC,CAAC;AAwDF,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,UAiB/C,CAAC"}
|
package/dist/toTestResult.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toJestStatus } from './toJestStatus.js';
|
|
1
2
|
const getPerfStats = ({ stats }) => {
|
|
2
3
|
const start = new Date(stats.start).getTime();
|
|
3
4
|
const end = new Date(stats.end).getTime();
|
|
@@ -38,7 +39,7 @@ const getTestResults = ({ errorMessage, tests, jestTestPath, }) => {
|
|
|
38
39
|
failureMessages: actualErrorMessage ? [actualErrorMessage] : [],
|
|
39
40
|
fullName: test.fullName || test.testPath || jestTestPath || '',
|
|
40
41
|
numPassingAsserts: test.status === 'passed' ? 1 : 0,
|
|
41
|
-
status: test.status,
|
|
42
|
+
status: toJestStatus(test.status),
|
|
42
43
|
title: test.title || '',
|
|
43
44
|
location: test.location,
|
|
44
45
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-harness/jest",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
"jest-util": "^30.2.0",
|
|
40
40
|
"tslib": "^2.3.0",
|
|
41
41
|
"yargs": "^17.7.2",
|
|
42
|
-
"@react-native-harness/
|
|
43
|
-
"@react-native-harness/
|
|
44
|
-
"@react-native-harness/
|
|
45
|
-
"@react-native-harness/
|
|
46
|
-
"@react-native-harness/
|
|
47
|
-
"@react-native-harness/
|
|
48
|
-
"@react-native-harness/
|
|
42
|
+
"@react-native-harness/cache": "1.6.0",
|
|
43
|
+
"@react-native-harness/config": "1.6.0",
|
|
44
|
+
"@react-native-harness/bundler-metro": "1.6.0",
|
|
45
|
+
"@react-native-harness/platforms": "1.6.0",
|
|
46
|
+
"@react-native-harness/tools": "1.6.0",
|
|
47
|
+
"@react-native-harness/plugins": "1.6.0",
|
|
48
|
+
"@react-native-harness/bridge": "1.6.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/yargs": "^17.0.32",
|
package/src/execute-run.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { formatHarnessErrorMessage } from './format-harness-error.js';
|
|
33
33
|
import { logger } from '@react-native-harness/tools';
|
|
34
34
|
import { printSummary, writeTraceFile } from './diagnostics/index.js';
|
|
35
|
+
import { toJestStatus } from './toJestStatus.js';
|
|
35
36
|
|
|
36
37
|
const diagnosticsLogger = logger.child('diagnostics');
|
|
37
38
|
|
|
@@ -109,7 +110,7 @@ const emitHarnessTestFinished = async (
|
|
|
109
110
|
location,
|
|
110
111
|
numPassingAsserts: event.status === 'passed' ? 1 : 0,
|
|
111
112
|
startedAt: event.startedAt,
|
|
112
|
-
status: event.status,
|
|
113
|
+
status: toJestStatus(event.status),
|
|
113
114
|
title: event.name,
|
|
114
115
|
};
|
|
115
116
|
|
package/src/harness-session.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import {
|
|
39
39
|
createCrashArtifactWriter,
|
|
40
40
|
createDiagnostics,
|
|
41
|
+
delay,
|
|
41
42
|
logger,
|
|
42
43
|
getTimeoutSignal,
|
|
43
44
|
ensureSignalsDeliverable,
|
|
@@ -94,6 +95,25 @@ const ignorePromiseRejection = () => undefined;
|
|
|
94
95
|
const isBridgeDisconnectError = (error: unknown) =>
|
|
95
96
|
error instanceof Error && error.message === 'App bridge disconnected';
|
|
96
97
|
const TEST_RUN_BRIDGE_STABILITY_WAIT_MS = 500;
|
|
98
|
+
const DISPOSE_HOOK_TIMEOUT_MS = 5_000;
|
|
99
|
+
|
|
100
|
+
const waitForBoundedSettlement = async <T>(
|
|
101
|
+
promise: Promise<T>,
|
|
102
|
+
timeoutMs: number
|
|
103
|
+
): Promise<PromiseSettledResult<T> | null> => {
|
|
104
|
+
const timeout = delay(timeoutMs);
|
|
105
|
+
try {
|
|
106
|
+
return await Promise.race([
|
|
107
|
+
promise.then(
|
|
108
|
+
(value) => ({ status: 'fulfilled', value }) as const,
|
|
109
|
+
(reason) => ({ status: 'rejected', reason }) as const
|
|
110
|
+
),
|
|
111
|
+
timeout.promise.then(() => null),
|
|
112
|
+
]);
|
|
113
|
+
} finally {
|
|
114
|
+
timeout.cancel();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
97
117
|
|
|
98
118
|
type DisconnectObservableBridge = {
|
|
99
119
|
readonly connection: AppConnection | null;
|
|
@@ -464,6 +484,9 @@ export const createHarnessSession = async (
|
|
|
464
484
|
|
|
465
485
|
const setupStartedAt = Date.now();
|
|
466
486
|
const { harnessConfig, platform, projectRoot, configProjectRoot } = await loadConfig(globalConfig);
|
|
487
|
+
// Metro platform for prewarm and startup detection. Vega apps request
|
|
488
|
+
// "kepler" bundles while the harness platform id stays "vega".
|
|
489
|
+
const bundlePlatform = platform.bundlePlatform ?? platform.platformId;
|
|
467
490
|
applyEnvVars(harnessConfig, globalConfig);
|
|
468
491
|
|
|
469
492
|
const diagnostics = createDiagnostics({
|
|
@@ -487,7 +510,11 @@ export const createHarnessSession = async (
|
|
|
487
510
|
// child the runner threads it into.
|
|
488
511
|
ensureSignalsDeliverable();
|
|
489
512
|
const sessionController = new AbortController();
|
|
490
|
-
|
|
513
|
+
let receivedEarlySignal = false;
|
|
514
|
+
const onEarlySignal = () => {
|
|
515
|
+
receivedEarlySignal = true;
|
|
516
|
+
sessionController.abort();
|
|
517
|
+
};
|
|
491
518
|
process.once('SIGTERM', onEarlySignal);
|
|
492
519
|
process.once('SIGINT', onEarlySignal);
|
|
493
520
|
|
|
@@ -584,6 +611,11 @@ export const createHarnessSession = async (
|
|
|
584
611
|
|
|
585
612
|
let metroInstance: MetroInstance;
|
|
586
613
|
let platformInstance: HarnessPlatformRunner;
|
|
614
|
+
const initialized = {
|
|
615
|
+
metro: null as MetroInstance | null,
|
|
616
|
+
platform: null as HarnessPlatformRunner | null,
|
|
617
|
+
};
|
|
618
|
+
let initializationCancelled = false;
|
|
587
619
|
// Noop until the Metro instance resolves and the deriver is attached.
|
|
588
620
|
let disposeMetroDiagnostics: () => void = () => undefined;
|
|
589
621
|
|
|
@@ -594,6 +626,9 @@ export const createHarnessSession = async (
|
|
|
594
626
|
{
|
|
595
627
|
projectRoot,
|
|
596
628
|
harnessConfig: runtimeConfig,
|
|
629
|
+
metroConfigEnhancer: platform.metroConfigEnhancer,
|
|
630
|
+
platformId: platform.platformId,
|
|
631
|
+
platformConfig: platform.config,
|
|
597
632
|
websocketEndpoints: {
|
|
598
633
|
[HARNESS_BRIDGE_PATH]: bridge.ws as unknown as MetroWebSocketEndpoint,
|
|
599
634
|
},
|
|
@@ -601,6 +636,10 @@ export const createHarnessSession = async (
|
|
|
601
636
|
},
|
|
602
637
|
sessionController.signal,
|
|
603
638
|
).then((instance) => {
|
|
639
|
+
if (initializationCancelled) {
|
|
640
|
+
return instance.dispose().then(() => instance);
|
|
641
|
+
}
|
|
642
|
+
initialized.metro = instance;
|
|
604
643
|
sessionLogger.debug('Metro initialized');
|
|
605
644
|
// Attach the Metro diagnostics deriver before the eager prewarm
|
|
606
645
|
// fires so its bundle build/request events land in the trace.
|
|
@@ -613,7 +652,7 @@ export const createHarnessSession = async (
|
|
|
613
652
|
const prewarmSpan = diagnostics.start('metro.prewarm');
|
|
614
653
|
instance
|
|
615
654
|
.prewarm({
|
|
616
|
-
platform:
|
|
655
|
+
platform: bundlePlatform,
|
|
617
656
|
signal: sessionController.signal,
|
|
618
657
|
})
|
|
619
658
|
.then((completed) => prewarmSpan.end({ completed }))
|
|
@@ -638,6 +677,10 @@ export const createHarnessSession = async (
|
|
|
638
677
|
diagnostics,
|
|
639
678
|
} satisfies HarnessPlatformInitOptions),
|
|
640
679
|
).then((instance) => {
|
|
680
|
+
if (initializationCancelled) {
|
|
681
|
+
return instance.dispose().then(() => instance);
|
|
682
|
+
}
|
|
683
|
+
initialized.platform = instance;
|
|
641
684
|
sessionLogger.debug('platform runner initialized');
|
|
642
685
|
return instance;
|
|
643
686
|
});
|
|
@@ -647,10 +690,16 @@ export const createHarnessSession = async (
|
|
|
647
690
|
),
|
|
648
691
|
]);
|
|
649
692
|
} catch (error) {
|
|
650
|
-
//
|
|
651
|
-
//
|
|
693
|
+
// Roll back every resource whose initialization won a concurrent race.
|
|
694
|
+
// Leases are released by the outer catch.
|
|
695
|
+
initializationCancelled = true;
|
|
696
|
+
sessionController.abort();
|
|
652
697
|
disposeMetroDiagnostics();
|
|
653
|
-
await
|
|
698
|
+
await Promise.allSettled([
|
|
699
|
+
bridge.dispose(),
|
|
700
|
+
initialized.metro?.dispose(),
|
|
701
|
+
initialized.platform?.dispose(),
|
|
702
|
+
]);
|
|
654
703
|
throw error;
|
|
655
704
|
}
|
|
656
705
|
|
|
@@ -685,7 +734,7 @@ export const createHarnessSession = async (
|
|
|
685
734
|
const appReadyBaseOptions: AppReadyOptions = {
|
|
686
735
|
metroInstance,
|
|
687
736
|
bridge,
|
|
688
|
-
platformId:
|
|
737
|
+
platformId: bundlePlatform,
|
|
689
738
|
bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
|
|
690
739
|
readyTimeout: runtimeConfig.bridgeTimeout,
|
|
691
740
|
maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
|
|
@@ -759,7 +808,11 @@ export const createHarnessSession = async (
|
|
|
759
808
|
const disposeSpan = diagnostics.start('dispose.total', { reason });
|
|
760
809
|
let hookError: unknown;
|
|
761
810
|
|
|
762
|
-
|
|
811
|
+
// Abort only cancels finite/in-flight work. Ownership remains explicit
|
|
812
|
+
// below, so this cannot race a platform or app-session disposal path.
|
|
813
|
+
sessionController.abort();
|
|
814
|
+
|
|
815
|
+
const hookWork = (async () => {
|
|
763
816
|
await hooks.drain();
|
|
764
817
|
await pluginManager.callHook('harness:after-run', {
|
|
765
818
|
runId: currentRun?.runId,
|
|
@@ -777,8 +830,17 @@ export const createHarnessSession = async (
|
|
|
777
830
|
error: currentRun?.error,
|
|
778
831
|
});
|
|
779
832
|
await hooks.drain();
|
|
780
|
-
}
|
|
781
|
-
|
|
833
|
+
})();
|
|
834
|
+
const hookResult = await waitForBoundedSettlement(
|
|
835
|
+
hookWork,
|
|
836
|
+
DISPOSE_HOOK_TIMEOUT_MS
|
|
837
|
+
);
|
|
838
|
+
if (hookResult === null) {
|
|
839
|
+
hookError = new Error(
|
|
840
|
+
`Timed out after ${DISPOSE_HOOK_TIMEOUT_MS}ms while running disposal hooks.`
|
|
841
|
+
);
|
|
842
|
+
} else if (hookResult.status === 'rejected') {
|
|
843
|
+
hookError = hookResult.reason;
|
|
782
844
|
}
|
|
783
845
|
|
|
784
846
|
if (runtimeConfig.forwardClientLogs) {
|
|
@@ -809,26 +871,23 @@ export const createHarnessSession = async (
|
|
|
809
871
|
}
|
|
810
872
|
}
|
|
811
873
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
await resourceLease.release();
|
|
830
|
-
sessionController.abort();
|
|
831
|
-
}
|
|
874
|
+
// The app session (and its log streams) must finish before platform
|
|
875
|
+
// disposal can remove the underlying device. Run independent cleanup
|
|
876
|
+
// all-settled so one failure cannot strand another owned resource.
|
|
877
|
+
const appCleanup = await Promise.allSettled([
|
|
878
|
+
crashMonitor.dispose(),
|
|
879
|
+
disposeCurrentAppSession(),
|
|
880
|
+
]);
|
|
881
|
+
const infrastructureCleanup = await Promise.allSettled([
|
|
882
|
+
bridge.dispose(),
|
|
883
|
+
platformInstance.dispose(),
|
|
884
|
+
metroInstance.dispose(),
|
|
885
|
+
metroPortLease?.release(),
|
|
886
|
+
]);
|
|
887
|
+
const leaseCleanup = await Promise.allSettled([resourceLease.release()]);
|
|
888
|
+
const cleanupError = [...appCleanup, ...infrastructureCleanup, ...leaseCleanup]
|
|
889
|
+
.find((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
890
|
+
?.reason;
|
|
832
891
|
|
|
833
892
|
sessionLogger.debug('session resources disposed');
|
|
834
893
|
|
|
@@ -1102,6 +1161,9 @@ export const createHarnessSession = async (
|
|
|
1102
1161
|
// (e.g. a platform init call abandoned after a readiness timeout race).
|
|
1103
1162
|
sessionController.abort();
|
|
1104
1163
|
await Promise.allSettled([resourceLease.release(), metroPortLease?.release()]);
|
|
1164
|
+
if (receivedEarlySignal) {
|
|
1165
|
+
process.exit(1);
|
|
1166
|
+
}
|
|
1105
1167
|
throw error;
|
|
1106
1168
|
}
|
|
1107
1169
|
};
|
package/src/resource-lock.ts
CHANGED
|
@@ -392,8 +392,21 @@ export const createResourceLockManager = (
|
|
|
392
392
|
return;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
|
|
396
|
-
|
|
395
|
+
try {
|
|
396
|
+
await writeJsonFileAtomic(paths.ownerFilePath, nextMetadata);
|
|
397
|
+
scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
|
|
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
|
+
}
|
|
397
410
|
} finally {
|
|
398
411
|
heartbeatInFlight = false;
|
|
399
412
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Status } from '@jest/test-result';
|
|
2
|
+
import type { TestResultStatus } from '@react-native-harness/bridge';
|
|
3
|
+
|
|
4
|
+
// Jest's own runners expose skipped assertions as `pending` to reporters.
|
|
5
|
+
export const toJestStatus = (status: TestResultStatus): Status =>
|
|
6
|
+
status === 'skipped' ? 'pending' : status;
|
package/src/toTestResult.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TestResult } from '@jest/test-result';
|
|
2
|
+
import type { TestResultStatus } from '@react-native-harness/bridge';
|
|
3
|
+
import { toJestStatus } from './toJestStatus.js';
|
|
2
4
|
|
|
3
5
|
export type Options = {
|
|
4
6
|
stats: {
|
|
@@ -17,7 +19,7 @@ export type Options = {
|
|
|
17
19
|
testPath?: string;
|
|
18
20
|
title?: string;
|
|
19
21
|
fullName?: string;
|
|
20
|
-
status:
|
|
22
|
+
status: TestResultStatus;
|
|
21
23
|
location?: {
|
|
22
24
|
column: number;
|
|
23
25
|
line: number;
|
|
@@ -75,7 +77,7 @@ const getTestResults = ({
|
|
|
75
77
|
failureMessages: actualErrorMessage ? [actualErrorMessage] : [],
|
|
76
78
|
fullName: test.fullName || test.testPath || jestTestPath || '',
|
|
77
79
|
numPassingAsserts: test.status === 'passed' ? 1 : 0,
|
|
78
|
-
status: test.status,
|
|
80
|
+
status: toJestStatus(test.status),
|
|
79
81
|
title: test.title || '',
|
|
80
82
|
location: test.location,
|
|
81
83
|
};
|