@react-native-harness/jest 1.1.0-rc.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/harness.test.js +59 -1
- package/dist/__tests__/metro-port.test.d.ts +2 -0
- package/dist/__tests__/metro-port.test.d.ts.map +1 -0
- package/dist/__tests__/metro-port.test.js +52 -0
- package/dist/__tests__/resource-lock.test.js +36 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +11 -0
- package/dist/harness.d.ts +1 -0
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +26 -13
- package/dist/logs.d.ts +1 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +3 -0
- package/dist/metro-port.d.ts +16 -0
- package/dist/metro-port.d.ts.map +1 -0
- package/dist/metro-port.js +48 -0
- package/dist/resource-lock.d.ts.map +1 -1
- package/dist/resource-lock.js +32 -11
- package/dist/setup.js +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/__tests__/harness.test.ts +89 -1
- package/src/__tests__/metro-port.test.ts +61 -0
- package/src/__tests__/resource-lock.test.ts +58 -4
- package/src/errors.ts +13 -0
- package/src/harness.ts +34 -12
- package/src/logs.ts +11 -0
- package/src/metro-port.ts +76 -0
- package/src/resource-lock.ts +59 -37
- package/src/setup.ts +1 -1
- package/dist/cli-args.test.d.ts +0 -2
- package/dist/cli-args.test.d.ts.map +0 -1
- package/dist/cli-args.test.js +0 -1
- package/dist/crash-monitor.d.ts +0 -16
- package/dist/crash-monitor.d.ts.map +0 -1
- package/dist/crash-monitor.js +0 -75
- 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/jest-options.d.ts +0 -2
- package/dist/jest-options.d.ts.map +0 -1
- package/dist/jest-options.js +0 -1
- package/dist/types.d.ts +0 -17
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-harness/jest",
|
|
3
|
-
"version": "1.1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"p-limit": "^7.1.1",
|
|
41
41
|
"tslib": "^2.3.0",
|
|
42
42
|
"yargs": "^17.7.2",
|
|
43
|
-
"@react-native-harness/bridge": "1.1.0
|
|
44
|
-
"@react-native-harness/
|
|
45
|
-
"@react-native-harness/bundler-metro": "1.1.0
|
|
46
|
-
"@react-native-harness/platforms": "1.1.0
|
|
47
|
-
"@react-native-harness/
|
|
48
|
-
"@react-native-harness/
|
|
43
|
+
"@react-native-harness/bridge": "1.1.0",
|
|
44
|
+
"@react-native-harness/plugins": "1.1.0",
|
|
45
|
+
"@react-native-harness/bundler-metro": "1.1.0",
|
|
46
|
+
"@react-native-harness/platforms": "1.1.0",
|
|
47
|
+
"@react-native-harness/config": "1.1.0",
|
|
48
|
+
"@react-native-harness/tools": "1.1.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/yargs": "^17.0.32",
|
|
@@ -2,6 +2,7 @@ import { EventEmitter } from 'node:events';
|
|
|
2
2
|
import { HARNESS_BRIDGE_PATH } from '@react-native-harness/bridge';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import type { Config as HarnessConfig } from '@react-native-harness/config';
|
|
5
|
+
import { MetroPortRangeExhaustedError } from '../errors.js';
|
|
5
6
|
import { definePlugin } from '@react-native-harness/plugins';
|
|
6
7
|
import type {
|
|
7
8
|
AppMonitor,
|
|
@@ -25,10 +26,12 @@ const mocks = vi.hoisted(() => ({
|
|
|
25
26
|
getMetroInstance: vi.fn(),
|
|
26
27
|
isMetroCacheReusable: vi.fn(() => false),
|
|
27
28
|
logMetroCacheReused: vi.fn(),
|
|
29
|
+
logMetroPortFallback: vi.fn(),
|
|
28
30
|
logRunnerStarting: vi.fn(),
|
|
29
31
|
logRunnerStillWaitingInQueue: vi.fn(),
|
|
30
32
|
logRunnerWaitingInQueue: vi.fn(),
|
|
31
33
|
waitForMetroBackedAppReady: vi.fn(),
|
|
34
|
+
isPortAvailable: vi.fn(async () => true),
|
|
32
35
|
}));
|
|
33
36
|
|
|
34
37
|
vi.mock('@react-native-harness/bundler-metro', async () => {
|
|
@@ -39,6 +42,7 @@ vi.mock('@react-native-harness/bundler-metro', async () => {
|
|
|
39
42
|
return {
|
|
40
43
|
...actual,
|
|
41
44
|
getMetroInstance: mocks.getMetroInstance,
|
|
45
|
+
isPortAvailable: mocks.isPortAvailable,
|
|
42
46
|
isMetroCacheReusable: mocks.isMetroCacheReusable,
|
|
43
47
|
waitForMetroBackedAppReady: mocks.waitForMetroBackedAppReady,
|
|
44
48
|
};
|
|
@@ -50,6 +54,7 @@ vi.mock('@react-native-harness/bridge/server', () => ({
|
|
|
50
54
|
|
|
51
55
|
vi.mock('../logs.js', () => ({
|
|
52
56
|
logMetroCacheReused: mocks.logMetroCacheReused,
|
|
57
|
+
logMetroPortFallback: mocks.logMetroPortFallback,
|
|
53
58
|
logRunnerStarting: mocks.logRunnerStarting,
|
|
54
59
|
logRunnerStillWaitingInQueue: mocks.logRunnerStillWaitingInQueue,
|
|
55
60
|
logRunnerWaitingInQueue: mocks.logRunnerWaitingInQueue,
|
|
@@ -195,6 +200,8 @@ const createHarnessConfig = (
|
|
|
195
200
|
|
|
196
201
|
beforeEach(() => {
|
|
197
202
|
vi.clearAllMocks();
|
|
203
|
+
mocks.isPortAvailable.mockReset();
|
|
204
|
+
mocks.isPortAvailable.mockResolvedValue(true);
|
|
198
205
|
});
|
|
199
206
|
|
|
200
207
|
afterEach(() => {
|
|
@@ -377,7 +384,9 @@ describe('getHarness', () => {
|
|
|
377
384
|
|
|
378
385
|
expect(runner).toHaveBeenCalledWith(
|
|
379
386
|
platform.config,
|
|
380
|
-
expect.
|
|
387
|
+
expect.objectContaining({
|
|
388
|
+
metroPort: 8081,
|
|
389
|
+
}),
|
|
381
390
|
expect.objectContaining({
|
|
382
391
|
signal: expect.any(AbortSignal),
|
|
383
392
|
})
|
|
@@ -386,6 +395,85 @@ describe('getHarness', () => {
|
|
|
386
395
|
await harness.dispose();
|
|
387
396
|
});
|
|
388
397
|
|
|
398
|
+
it('resolves and exposes a fallback Metro port before platform init', async () => {
|
|
399
|
+
const { serverBridge } = createBridgeServer();
|
|
400
|
+
const appMonitor = createAppMonitor();
|
|
401
|
+
const platformInstance = createPlatformRunner({
|
|
402
|
+
createAppMonitor: () => appMonitor.appMonitor,
|
|
403
|
+
});
|
|
404
|
+
const metroInstance = createMetroInstance();
|
|
405
|
+
|
|
406
|
+
mocks.getBridgeServer.mockResolvedValue(serverBridge);
|
|
407
|
+
mocks.getMetroInstance.mockResolvedValue(metroInstance);
|
|
408
|
+
mocks.isPortAvailable
|
|
409
|
+
.mockResolvedValueOnce(false)
|
|
410
|
+
.mockResolvedValueOnce(true);
|
|
411
|
+
|
|
412
|
+
const runner = vi.fn(async () => platformInstance);
|
|
413
|
+
(
|
|
414
|
+
globalThis as typeof globalThis & {
|
|
415
|
+
__HARNESS_PLATFORM_RUNNER__?: (...args: unknown[]) => Promise<unknown>;
|
|
416
|
+
}
|
|
417
|
+
).__HARNESS_PLATFORM_RUNNER__ = runner;
|
|
418
|
+
|
|
419
|
+
const platform: HarnessPlatform = {
|
|
420
|
+
config: {},
|
|
421
|
+
getResourceLockKey: () => 'android:emulator:Pixel_8_API_35',
|
|
422
|
+
name: 'android',
|
|
423
|
+
platformId: 'android',
|
|
424
|
+
runner: `data:text/javascript,${encodeURIComponent(
|
|
425
|
+
'export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);'
|
|
426
|
+
)}`,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const harness = await getHarness(
|
|
430
|
+
createHarnessConfig(),
|
|
431
|
+
platform,
|
|
432
|
+
'/tmp/project'
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
expect(harness.config.metroPort).toBe(8082);
|
|
436
|
+
expect(mocks.getMetroInstance).toHaveBeenCalledWith(
|
|
437
|
+
expect.objectContaining({
|
|
438
|
+
harnessConfig: expect.objectContaining({
|
|
439
|
+
metroPort: 8082,
|
|
440
|
+
}),
|
|
441
|
+
}),
|
|
442
|
+
expect.any(AbortSignal)
|
|
443
|
+
);
|
|
444
|
+
expect(runner).toHaveBeenCalledWith(
|
|
445
|
+
platform.config,
|
|
446
|
+
expect.objectContaining({
|
|
447
|
+
metroPort: 8082,
|
|
448
|
+
}),
|
|
449
|
+
expect.objectContaining({
|
|
450
|
+
signal: expect.any(AbortSignal),
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
expect(mocks.logMetroPortFallback).toHaveBeenCalledWith(8081, 8082);
|
|
454
|
+
|
|
455
|
+
await harness.dispose();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('fails when no Metro port is available in the retry window', async () => {
|
|
459
|
+
mocks.isPortAvailable.mockResolvedValue(false);
|
|
460
|
+
|
|
461
|
+
const platform: HarnessPlatform = {
|
|
462
|
+
config: {},
|
|
463
|
+
getResourceLockKey: () => 'android:emulator:Pixel_8_API_35',
|
|
464
|
+
name: 'android',
|
|
465
|
+
platformId: 'android',
|
|
466
|
+
runner: 'data:text/javascript,export default async () => ({})',
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
await expect(
|
|
470
|
+
getHarness(createHarnessConfig(), platform, '/tmp/project')
|
|
471
|
+
).rejects.toBeInstanceOf(MetroPortRangeExhaustedError);
|
|
472
|
+
|
|
473
|
+
expect(mocks.getBridgeServer).not.toHaveBeenCalled();
|
|
474
|
+
expect(mocks.getMetroInstance).not.toHaveBeenCalled();
|
|
475
|
+
});
|
|
476
|
+
|
|
389
477
|
it('falls back to a default resource lock key for platforms without getResourceLockKey', async () => {
|
|
390
478
|
const { serverBridge } = createBridgeServer();
|
|
391
479
|
const appMonitor = createAppMonitor();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { Config as HarnessConfig } from '@react-native-harness/config';
|
|
3
|
+
import type { HarnessPlatform } from '@react-native-harness/platforms';
|
|
4
|
+
import { resolveHarnessMetroPort } from '../metro-port.js';
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
isPortAvailable: vi.fn(async () => true),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('@react-native-harness/bundler-metro', () => ({
|
|
11
|
+
isPortAvailable: mocks.isPortAvailable,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const createConfig = (overrides: Partial<HarnessConfig> = {}): HarnessConfig =>
|
|
15
|
+
({
|
|
16
|
+
appRegistryComponentName: 'App',
|
|
17
|
+
bridgeTimeout: 60_000,
|
|
18
|
+
bundleStartTimeout: 60_000,
|
|
19
|
+
crashDetectionInterval: 500,
|
|
20
|
+
defaultRunner: 'ios-device',
|
|
21
|
+
detectNativeCrashes: true,
|
|
22
|
+
disableViewFlattening: false,
|
|
23
|
+
entryPoint: 'index.js',
|
|
24
|
+
forwardClientLogs: false,
|
|
25
|
+
maxAppRestarts: 2,
|
|
26
|
+
metroPort: 8081,
|
|
27
|
+
platformReadyTimeout: 300_000,
|
|
28
|
+
resetEnvironmentBetweenTestFiles: true,
|
|
29
|
+
runners: [],
|
|
30
|
+
unstable__enableMetroCache: false,
|
|
31
|
+
unstable__skipAlreadyIncludedModules: false,
|
|
32
|
+
...overrides,
|
|
33
|
+
} as HarnessConfig);
|
|
34
|
+
|
|
35
|
+
describe('resolveHarnessMetroPort', () => {
|
|
36
|
+
it('skips fallback allocation for iOS physical device runners', async () => {
|
|
37
|
+
const acquire = vi.fn();
|
|
38
|
+
const config = createConfig();
|
|
39
|
+
const platform: HarnessPlatform = {
|
|
40
|
+
config: {},
|
|
41
|
+
name: 'ios-device',
|
|
42
|
+
platformId: 'ios',
|
|
43
|
+
runner: 'unused',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const result = await resolveHarnessMetroPort({
|
|
47
|
+
config,
|
|
48
|
+
platform,
|
|
49
|
+
resourceLockManager: {
|
|
50
|
+
acquire,
|
|
51
|
+
},
|
|
52
|
+
signal: new AbortController().signal,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result.config).toBe(config);
|
|
56
|
+
expect(result.metroPortLease).toBeNull();
|
|
57
|
+
expect(result.didFallback).toBe(false);
|
|
58
|
+
expect(acquire).not.toHaveBeenCalled();
|
|
59
|
+
expect(mocks.isPortAvailable).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import {
|
|
6
6
|
createResourceLockManager,
|
|
7
7
|
hashResourceLockKey,
|
|
@@ -12,7 +12,7 @@ describe('resource lock manager', () => {
|
|
|
12
12
|
|
|
13
13
|
beforeEach(async () => {
|
|
14
14
|
rootDir = await fs.mkdtemp(
|
|
15
|
-
path.join(os.tmpdir(), 'react-native-harness-resource-lock-test-')
|
|
15
|
+
path.join(os.tmpdir(), 'react-native-harness-resource-lock-test-'),
|
|
16
16
|
);
|
|
17
17
|
});
|
|
18
18
|
|
|
@@ -30,7 +30,7 @@ describe('resource lock manager', () => {
|
|
|
30
30
|
const order: string[] = [];
|
|
31
31
|
|
|
32
32
|
const firstLease = await manager.acquire(
|
|
33
|
-
'ios:simulator:iPhone 17 Pro:26.2'
|
|
33
|
+
'ios:simulator:iPhone 17 Pro:26.2',
|
|
34
34
|
);
|
|
35
35
|
const secondAcquire = manager
|
|
36
36
|
.acquire('ios:simulator:iPhone 17 Pro:26.2', {
|
|
@@ -124,7 +124,7 @@ describe('resource lock manager', () => {
|
|
|
124
124
|
createdAt: Date.now() - 1000,
|
|
125
125
|
heartbeatAt: Date.now() - 1000,
|
|
126
126
|
}),
|
|
127
|
-
'utf8'
|
|
127
|
+
'utf8',
|
|
128
128
|
);
|
|
129
129
|
|
|
130
130
|
const lease = await manager.acquire(key);
|
|
@@ -135,4 +135,58 @@ describe('resource lock manager', () => {
|
|
|
135
135
|
|
|
136
136
|
await lease.release();
|
|
137
137
|
});
|
|
138
|
+
|
|
139
|
+
it('keeps owner metadata valid when heartbeat writes overlap', async () => {
|
|
140
|
+
const manager = createResourceLockManager({
|
|
141
|
+
rootDir,
|
|
142
|
+
pollIntervalMs: 5,
|
|
143
|
+
heartbeatIntervalMs: 10,
|
|
144
|
+
staleLockTimeoutMs: 200,
|
|
145
|
+
});
|
|
146
|
+
const key = 'ios:simulator:iPhone 17 Pro:26.2';
|
|
147
|
+
const ownerFilePath = path.join(
|
|
148
|
+
rootDir,
|
|
149
|
+
hashResourceLockKey(key),
|
|
150
|
+
'owner.json',
|
|
151
|
+
);
|
|
152
|
+
const actualWriteFile = fs.writeFile.bind(fs);
|
|
153
|
+
const writeFileSpy = vi
|
|
154
|
+
.spyOn(fs, 'writeFile')
|
|
155
|
+
.mockImplementation(async (file, data, options) => {
|
|
156
|
+
if (
|
|
157
|
+
typeof file === 'string' &&
|
|
158
|
+
file.startsWith(ownerFilePath) &&
|
|
159
|
+
file !== ownerFilePath
|
|
160
|
+
) {
|
|
161
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return await actualWriteFile(file, data, options);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const lease = await manager.acquire(key);
|
|
169
|
+
const initialOwner = JSON.parse(
|
|
170
|
+
await fs.readFile(ownerFilePath, 'utf8'),
|
|
171
|
+
) as ResourceLockOwner;
|
|
172
|
+
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
174
|
+
|
|
175
|
+
for (let index = 0; index < 5; index += 1) {
|
|
176
|
+
const owner = JSON.parse(
|
|
177
|
+
await fs.readFile(ownerFilePath, 'utf8'),
|
|
178
|
+
) as ResourceLockOwner;
|
|
179
|
+
expect(owner.ticketId).toBe(initialOwner.ticketId);
|
|
180
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await lease.release();
|
|
184
|
+
} finally {
|
|
185
|
+
writeFileSpy.mockRestore();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
138
188
|
});
|
|
189
|
+
|
|
190
|
+
type ResourceLockOwner = {
|
|
191
|
+
ticketId: string;
|
|
192
|
+
};
|
package/src/errors.ts
CHANGED
|
@@ -36,6 +36,19 @@ export class PlatformReadyTimeoutError extends HarnessError {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export class MetroPortRangeExhaustedError extends HarnessError {
|
|
40
|
+
constructor(
|
|
41
|
+
public readonly initialPort: number,
|
|
42
|
+
public readonly attempts: number
|
|
43
|
+
) {
|
|
44
|
+
const finalPort = initialPort + attempts - 1;
|
|
45
|
+
super(
|
|
46
|
+
`Harness could not find an available Metro port in the range ${initialPort}-${finalPort}.`
|
|
47
|
+
);
|
|
48
|
+
this.name = 'MetroPortRangeExhaustedError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
export type NativeCrashPhase = 'startup' | 'execution';
|
|
40
53
|
|
|
41
54
|
export type NativeCrashDetails = AppCrashDetails & {
|
package/src/harness.ts
CHANGED
|
@@ -49,11 +49,13 @@ import { createClientLogListener } from './client-log-handler.js';
|
|
|
49
49
|
import path from 'node:path';
|
|
50
50
|
import {
|
|
51
51
|
logMetroCacheReused,
|
|
52
|
+
logMetroPortFallback,
|
|
52
53
|
logRunnerStarting,
|
|
53
54
|
logRunnerStillWaitingInQueue,
|
|
54
55
|
logRunnerWaitingInQueue,
|
|
55
56
|
} from './logs.js';
|
|
56
57
|
import { createResourceLockManager } from './resource-lock.js';
|
|
58
|
+
import { resolveHarnessMetroPort } from './metro-port.js';
|
|
57
59
|
|
|
58
60
|
const harnessLogger = logger.child('runtime');
|
|
59
61
|
const resourceLockManager = createResourceLockManager();
|
|
@@ -73,6 +75,7 @@ export type HarnessRunState = {
|
|
|
73
75
|
|
|
74
76
|
export type Harness = {
|
|
75
77
|
context: HarnessContext;
|
|
78
|
+
config: HarnessConfig;
|
|
76
79
|
runTests: (
|
|
77
80
|
path: string,
|
|
78
81
|
options: HarnessRunTestsOptions
|
|
@@ -285,17 +288,33 @@ const getHarnessInternal = async (
|
|
|
285
288
|
resourceLockKey
|
|
286
289
|
);
|
|
287
290
|
try {
|
|
288
|
-
|
|
291
|
+
const {
|
|
292
|
+
config: runtimeConfig,
|
|
293
|
+
metroPortLease,
|
|
294
|
+
initialMetroPort,
|
|
295
|
+
didFallback,
|
|
296
|
+
} = await resolveHarnessMetroPort({
|
|
297
|
+
config,
|
|
298
|
+
platform,
|
|
299
|
+
resourceLockManager,
|
|
300
|
+
signal,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (didFallback) {
|
|
304
|
+
logMetroPortFallback(initialMetroPort, runtimeConfig.metroPort);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
maybeLogMetroCacheReuse(runtimeConfig, platform, projectRoot);
|
|
289
308
|
const pluginAbortController = new AbortController();
|
|
290
309
|
const pluginManager = createHarnessPluginManager<
|
|
291
310
|
HarnessConfig,
|
|
292
311
|
HarnessPlatform
|
|
293
312
|
>({
|
|
294
|
-
plugins: (
|
|
313
|
+
plugins: (runtimeConfig.plugins ?? []) as Array<
|
|
295
314
|
HarnessPlugin<object, HarnessConfig, HarnessPlatform>
|
|
296
315
|
>,
|
|
297
316
|
projectRoot,
|
|
298
|
-
config,
|
|
317
|
+
config: runtimeConfig,
|
|
299
318
|
runner: platform,
|
|
300
319
|
abortSignal: pluginAbortController.signal,
|
|
301
320
|
});
|
|
@@ -361,7 +380,7 @@ const getHarnessInternal = async (
|
|
|
361
380
|
|
|
362
381
|
const serverBridge = await getBridgeServer({
|
|
363
382
|
noServer: true,
|
|
364
|
-
timeout:
|
|
383
|
+
timeout: runtimeConfig.bridgeTimeout,
|
|
365
384
|
context,
|
|
366
385
|
});
|
|
367
386
|
harnessLogger.debug(
|
|
@@ -377,7 +396,7 @@ const getHarnessInternal = async (
|
|
|
377
396
|
getMetroInstance(
|
|
378
397
|
{
|
|
379
398
|
projectRoot,
|
|
380
|
-
harnessConfig:
|
|
399
|
+
harnessConfig: runtimeConfig,
|
|
381
400
|
websocketEndpoints: {
|
|
382
401
|
[HARNESS_BRIDGE_PATH]:
|
|
383
402
|
serverBridge.ws as unknown as MetroWebSocketEndpoint,
|
|
@@ -389,12 +408,12 @@ const getHarnessInternal = async (
|
|
|
389
408
|
return instance;
|
|
390
409
|
}),
|
|
391
410
|
withPlatformReadyTimeout({
|
|
392
|
-
timeout:
|
|
411
|
+
timeout: runtimeConfig.platformReadyTimeout,
|
|
393
412
|
signal,
|
|
394
413
|
work: async () => {
|
|
395
414
|
return await import(platform.runner)
|
|
396
415
|
.then((module) =>
|
|
397
|
-
module.default(platform.config,
|
|
416
|
+
module.default(platform.config, runtimeConfig, {
|
|
398
417
|
signal,
|
|
399
418
|
} satisfies HarnessPlatformInitOptions)
|
|
400
419
|
)
|
|
@@ -408,6 +427,7 @@ const getHarnessInternal = async (
|
|
|
408
427
|
} catch (error) {
|
|
409
428
|
await Promise.allSettled([
|
|
410
429
|
resourceLease.release(),
|
|
430
|
+
metroPortLease?.release(),
|
|
411
431
|
serverBridge.dispose(),
|
|
412
432
|
]);
|
|
413
433
|
throw error;
|
|
@@ -625,7 +645,7 @@ const getHarnessInternal = async (
|
|
|
625
645
|
appMonitor.addListener(onAppMonitorEvent);
|
|
626
646
|
harnessLogger.debug('registered runtime, bridge, and Metro listeners');
|
|
627
647
|
|
|
628
|
-
if (
|
|
648
|
+
if (runtimeConfig.forwardClientLogs) {
|
|
629
649
|
metroInstance.events.addListener(clientLogListener);
|
|
630
650
|
harnessLogger.debug('client log forwarding enabled');
|
|
631
651
|
}
|
|
@@ -656,7 +676,7 @@ const getHarnessInternal = async (
|
|
|
656
676
|
hookError = error;
|
|
657
677
|
}
|
|
658
678
|
|
|
659
|
-
if (
|
|
679
|
+
if (runtimeConfig.forwardClientLogs) {
|
|
660
680
|
metroInstance.events.removeListener(clientLogListener);
|
|
661
681
|
}
|
|
662
682
|
metroInstance.events.removeListener(onMetroEvent);
|
|
@@ -671,6 +691,7 @@ const getHarnessInternal = async (
|
|
|
671
691
|
serverBridge.dispose(),
|
|
672
692
|
platformInstance.dispose(),
|
|
673
693
|
metroInstance.dispose(),
|
|
694
|
+
metroPortLease?.release(),
|
|
674
695
|
]);
|
|
675
696
|
} catch (error) {
|
|
676
697
|
cleanupError = error;
|
|
@@ -744,9 +765,9 @@ const getHarnessInternal = async (
|
|
|
744
765
|
serverBridge,
|
|
745
766
|
platformInstance: platformInstance as HarnessPlatformRunner,
|
|
746
767
|
platformId: platform.platformId,
|
|
747
|
-
bundleStartTimeout:
|
|
748
|
-
readyTimeout:
|
|
749
|
-
maxAppRestarts:
|
|
768
|
+
bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
|
|
769
|
+
readyTimeout: runtimeConfig.bridgeTimeout,
|
|
770
|
+
maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
|
|
750
771
|
testFilePath,
|
|
751
772
|
crashSupervisor,
|
|
752
773
|
appLaunchOptions,
|
|
@@ -786,6 +807,7 @@ const getHarnessInternal = async (
|
|
|
786
807
|
|
|
787
808
|
return {
|
|
788
809
|
context,
|
|
810
|
+
config: runtimeConfig,
|
|
789
811
|
runTests: async (path, options) => {
|
|
790
812
|
await flushPendingHooks();
|
|
791
813
|
activeTestFilePath = path;
|
package/src/logs.ts
CHANGED
|
@@ -45,6 +45,17 @@ export const logMetroCacheReused = (runner: HarnessPlatform): void => {
|
|
|
45
45
|
log(`${TAG} Reusing Metro cache for ${chalk.bold(runner.name)}\n`);
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
export const logMetroPortFallback = (
|
|
49
|
+
initialPort: number,
|
|
50
|
+
selectedPort: number
|
|
51
|
+
): void => {
|
|
52
|
+
log(
|
|
53
|
+
`${TAG} Harness could not use Metro port ${chalk.bold(
|
|
54
|
+
String(initialPort)
|
|
55
|
+
)} and is using ${chalk.bold(String(selectedPort))} instead.\n`
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
48
59
|
export const getErrorMessage = (error: HarnessError): string => {
|
|
49
60
|
return `${ERROR_TAG} ${error.message}\n`;
|
|
50
61
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Config as HarnessConfig } from '@react-native-harness/config';
|
|
2
|
+
import { isPortAvailable } from '@react-native-harness/bundler-metro';
|
|
3
|
+
import type { HarnessPlatform } from '@react-native-harness/platforms';
|
|
4
|
+
import { MetroPortRangeExhaustedError } from './errors.js';
|
|
5
|
+
import type { ResourceLease, ResourceLockManager } from './resource-lock.js';
|
|
6
|
+
|
|
7
|
+
export const METRO_PORT_SCAN_ATTEMPTS = 10;
|
|
8
|
+
|
|
9
|
+
const getPortLockKey = (host: string | undefined, port: number): string => {
|
|
10
|
+
return `metro-port:${host?.trim() || '*'}:${port}`;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const isIosPhysicalRunner = (platform: HarnessPlatform): boolean => {
|
|
14
|
+
return platform.platformId === 'ios' && platform.name.includes('device');
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const resolveHarnessMetroPort = async (options: {
|
|
18
|
+
config: HarnessConfig;
|
|
19
|
+
platform: HarnessPlatform;
|
|
20
|
+
resourceLockManager: ResourceLockManager;
|
|
21
|
+
signal: AbortSignal;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
config: HarnessConfig;
|
|
24
|
+
metroPortLease: ResourceLease | null;
|
|
25
|
+
initialMetroPort: number;
|
|
26
|
+
didFallback: boolean;
|
|
27
|
+
}> => {
|
|
28
|
+
const { config, platform, resourceLockManager, signal } = options;
|
|
29
|
+
const initialMetroPort = config.metroPort;
|
|
30
|
+
const host = config.host?.trim();
|
|
31
|
+
|
|
32
|
+
if (isIosPhysicalRunner(platform)) {
|
|
33
|
+
return {
|
|
34
|
+
config,
|
|
35
|
+
metroPortLease: null,
|
|
36
|
+
initialMetroPort,
|
|
37
|
+
didFallback: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (let attempt = 0; attempt < METRO_PORT_SCAN_ATTEMPTS; attempt += 1) {
|
|
42
|
+
const candidatePort = initialMetroPort + attempt;
|
|
43
|
+
const metroPortLease = await resourceLockManager.acquire(
|
|
44
|
+
getPortLockKey(host, candidatePort),
|
|
45
|
+
{ signal }
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
if (!(await isPortAvailable(candidatePort, host))) {
|
|
50
|
+
await metroPortLease.release();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
config:
|
|
56
|
+
candidatePort === initialMetroPort
|
|
57
|
+
? config
|
|
58
|
+
: {
|
|
59
|
+
...config,
|
|
60
|
+
metroPort: candidatePort,
|
|
61
|
+
},
|
|
62
|
+
metroPortLease,
|
|
63
|
+
initialMetroPort,
|
|
64
|
+
didFallback: candidatePort !== initialMetroPort,
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
await metroPortLease.release();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new MetroPortRangeExhaustedError(
|
|
73
|
+
initialMetroPort,
|
|
74
|
+
METRO_PORT_SCAN_ATTEMPTS
|
|
75
|
+
);
|
|
76
|
+
};
|