@react-native-harness/jest 1.1.0-rc.3 → 1.1.0-rc.4

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.
@@ -1,6 +1,7 @@
1
1
  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
+ import { MetroPortRangeExhaustedError } from '../errors.js';
4
5
  import { definePlugin } from '@react-native-harness/plugins';
5
6
  import { createCrashSupervisor } from '../crash-supervisor.js';
6
7
  const mocks = vi.hoisted(() => ({
@@ -9,16 +10,19 @@ const mocks = vi.hoisted(() => ({
9
10
  getMetroInstance: vi.fn(),
10
11
  isMetroCacheReusable: vi.fn(() => false),
11
12
  logMetroCacheReused: vi.fn(),
13
+ logMetroPortFallback: vi.fn(),
12
14
  logRunnerStarting: vi.fn(),
13
15
  logRunnerStillWaitingInQueue: vi.fn(),
14
16
  logRunnerWaitingInQueue: vi.fn(),
15
17
  waitForMetroBackedAppReady: vi.fn(),
18
+ isPortAvailable: vi.fn(async () => true),
16
19
  }));
17
20
  vi.mock('@react-native-harness/bundler-metro', async () => {
18
21
  const actual = await vi.importActual('@react-native-harness/bundler-metro');
19
22
  return {
20
23
  ...actual,
21
24
  getMetroInstance: mocks.getMetroInstance,
25
+ isPortAvailable: mocks.isPortAvailable,
22
26
  isMetroCacheReusable: mocks.isMetroCacheReusable,
23
27
  waitForMetroBackedAppReady: mocks.waitForMetroBackedAppReady,
24
28
  };
@@ -28,6 +32,7 @@ vi.mock('@react-native-harness/bridge/server', () => ({
28
32
  }));
29
33
  vi.mock('../logs.js', () => ({
30
34
  logMetroCacheReused: mocks.logMetroCacheReused,
35
+ logMetroPortFallback: mocks.logMetroPortFallback,
31
36
  logRunnerStarting: mocks.logRunnerStarting,
32
37
  logRunnerStillWaitingInQueue: mocks.logRunnerStillWaitingInQueue,
33
38
  logRunnerWaitingInQueue: mocks.logRunnerWaitingInQueue,
@@ -148,6 +153,8 @@ const createHarnessConfig = (overrides = {}) => ({
148
153
  });
149
154
  beforeEach(() => {
150
155
  vi.clearAllMocks();
156
+ mocks.isPortAvailable.mockReset();
157
+ mocks.isPortAvailable.mockResolvedValue(true);
151
158
  });
152
159
  afterEach(() => {
153
160
  delete globalThis.__HARNESS_PLATFORM_RUNNER__;
@@ -271,11 +278,62 @@ describe('getHarness', () => {
271
278
  runner: `data:text/javascript,${encodeURIComponent('export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);')}`,
272
279
  };
273
280
  const harness = await getHarness(createHarnessConfig(), platform, '/tmp/project');
274
- expect(runner).toHaveBeenCalledWith(platform.config, expect.any(Object), expect.objectContaining({
281
+ expect(runner).toHaveBeenCalledWith(platform.config, expect.objectContaining({
282
+ metroPort: 8081,
283
+ }), expect.objectContaining({
275
284
  signal: expect.any(AbortSignal),
276
285
  }));
277
286
  await harness.dispose();
278
287
  });
288
+ it('resolves and exposes a fallback Metro port before platform init', async () => {
289
+ const { serverBridge } = createBridgeServer();
290
+ const appMonitor = createAppMonitor();
291
+ const platformInstance = createPlatformRunner({
292
+ createAppMonitor: () => appMonitor.appMonitor,
293
+ });
294
+ const metroInstance = createMetroInstance();
295
+ mocks.getBridgeServer.mockResolvedValue(serverBridge);
296
+ mocks.getMetroInstance.mockResolvedValue(metroInstance);
297
+ mocks.isPortAvailable
298
+ .mockResolvedValueOnce(false)
299
+ .mockResolvedValueOnce(true);
300
+ const runner = vi.fn(async () => platformInstance);
301
+ globalThis.__HARNESS_PLATFORM_RUNNER__ = runner;
302
+ const platform = {
303
+ config: {},
304
+ getResourceLockKey: () => 'android:emulator:Pixel_8_API_35',
305
+ name: 'android',
306
+ platformId: 'android',
307
+ runner: `data:text/javascript,${encodeURIComponent('export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);')}`,
308
+ };
309
+ const harness = await getHarness(createHarnessConfig(), platform, '/tmp/project');
310
+ expect(harness.config.metroPort).toBe(8082);
311
+ expect(mocks.getMetroInstance).toHaveBeenCalledWith(expect.objectContaining({
312
+ harnessConfig: expect.objectContaining({
313
+ metroPort: 8082,
314
+ }),
315
+ }), expect.any(AbortSignal));
316
+ expect(runner).toHaveBeenCalledWith(platform.config, expect.objectContaining({
317
+ metroPort: 8082,
318
+ }), expect.objectContaining({
319
+ signal: expect.any(AbortSignal),
320
+ }));
321
+ expect(mocks.logMetroPortFallback).toHaveBeenCalledWith(8081, 8082);
322
+ await harness.dispose();
323
+ });
324
+ it('fails when no Metro port is available in the retry window', async () => {
325
+ mocks.isPortAvailable.mockResolvedValue(false);
326
+ const platform = {
327
+ config: {},
328
+ getResourceLockKey: () => 'android:emulator:Pixel_8_API_35',
329
+ name: 'android',
330
+ platformId: 'android',
331
+ runner: 'data:text/javascript,export default async () => ({})',
332
+ };
333
+ await expect(getHarness(createHarnessConfig(), platform, '/tmp/project')).rejects.toBeInstanceOf(MetroPortRangeExhaustedError);
334
+ expect(mocks.getBridgeServer).not.toHaveBeenCalled();
335
+ expect(mocks.getMetroInstance).not.toHaveBeenCalled();
336
+ });
279
337
  it('falls back to a default resource lock key for platforms without getResourceLockKey', async () => {
280
338
  const { serverBridge } = createBridgeServer();
281
339
  const appMonitor = createAppMonitor();
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=metro-port.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metro-port.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/metro-port.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { resolveHarnessMetroPort } from '../metro-port.js';
3
+ const mocks = vi.hoisted(() => ({
4
+ isPortAvailable: vi.fn(async () => true),
5
+ }));
6
+ vi.mock('@react-native-harness/bundler-metro', () => ({
7
+ isPortAvailable: mocks.isPortAvailable,
8
+ }));
9
+ const createConfig = (overrides = {}) => ({
10
+ appRegistryComponentName: 'App',
11
+ bridgeTimeout: 60_000,
12
+ bundleStartTimeout: 60_000,
13
+ crashDetectionInterval: 500,
14
+ defaultRunner: 'ios-device',
15
+ detectNativeCrashes: true,
16
+ disableViewFlattening: false,
17
+ entryPoint: 'index.js',
18
+ forwardClientLogs: false,
19
+ maxAppRestarts: 2,
20
+ metroPort: 8081,
21
+ platformReadyTimeout: 300_000,
22
+ resetEnvironmentBetweenTestFiles: true,
23
+ runners: [],
24
+ unstable__enableMetroCache: false,
25
+ unstable__skipAlreadyIncludedModules: false,
26
+ ...overrides,
27
+ });
28
+ describe('resolveHarnessMetroPort', () => {
29
+ it('skips fallback allocation for iOS physical device runners', async () => {
30
+ const acquire = vi.fn();
31
+ const config = createConfig();
32
+ const platform = {
33
+ config: {},
34
+ name: 'ios-device',
35
+ platformId: 'ios',
36
+ runner: 'unused',
37
+ };
38
+ const result = await resolveHarnessMetroPort({
39
+ config,
40
+ platform,
41
+ resourceLockManager: {
42
+ acquire,
43
+ },
44
+ signal: new AbortController().signal,
45
+ });
46
+ expect(result.config).toBe(config);
47
+ expect(result.metroPortLease).toBeNull();
48
+ expect(result.didFallback).toBe(false);
49
+ expect(acquire).not.toHaveBeenCalled();
50
+ expect(mocks.isPortAvailable).not.toHaveBeenCalled();
51
+ });
52
+ });
@@ -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 { createResourceLockManager, hashResourceLockKey, } from '../resource-lock.js';
6
6
  describe('resource lock manager', () => {
7
7
  let rootDir;
@@ -100,4 +100,39 @@ describe('resource lock manager', () => {
100
100
  expect(owner.ticketId).not.toBe('stale-owner');
101
101
  await lease.release();
102
102
  });
103
+ it('keeps owner metadata valid when heartbeat writes overlap', async () => {
104
+ const manager = createResourceLockManager({
105
+ rootDir,
106
+ pollIntervalMs: 5,
107
+ heartbeatIntervalMs: 10,
108
+ staleLockTimeoutMs: 200,
109
+ });
110
+ const key = 'ios:simulator:iPhone 17 Pro:26.2';
111
+ const ownerFilePath = path.join(rootDir, hashResourceLockKey(key), 'owner.json');
112
+ const actualWriteFile = fs.writeFile.bind(fs);
113
+ const writeFileSpy = vi
114
+ .spyOn(fs, 'writeFile')
115
+ .mockImplementation(async (file, data, options) => {
116
+ if (typeof file === 'string' &&
117
+ file.startsWith(ownerFilePath) &&
118
+ file !== ownerFilePath) {
119
+ await new Promise((resolve) => setTimeout(resolve, 25));
120
+ }
121
+ return await actualWriteFile(file, data, options);
122
+ });
123
+ try {
124
+ const lease = await manager.acquire(key);
125
+ const initialOwner = JSON.parse(await fs.readFile(ownerFilePath, 'utf8'));
126
+ await new Promise((resolve) => setTimeout(resolve, 80));
127
+ for (let index = 0; index < 5; index += 1) {
128
+ const owner = JSON.parse(await fs.readFile(ownerFilePath, 'utf8'));
129
+ expect(owner.ticketId).toBe(initialOwner.ticketId);
130
+ await new Promise((resolve) => setTimeout(resolve, 10));
131
+ }
132
+ await lease.release();
133
+ }
134
+ finally {
135
+ writeFileSpy.mockRestore();
136
+ }
137
+ });
103
138
  });
package/dist/errors.d.ts CHANGED
@@ -15,6 +15,11 @@ export declare class PlatformReadyTimeoutError extends HarnessError {
15
15
  readonly timeout: number;
16
16
  constructor(timeout: number);
17
17
  }
18
+ export declare class MetroPortRangeExhaustedError extends HarnessError {
19
+ readonly initialPort: number;
20
+ readonly attempts: number;
21
+ constructor(initialPort: number, attempts: number);
22
+ }
18
23
  export type NativeCrashPhase = 'startup' | 'execution';
19
24
  export type NativeCrashDetails = AppCrashDetails & {
20
25
  phase: NativeCrashPhase;
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EACL,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,qCAAqC,CAAC;AAE7C,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D;AAED,qBAAa,yBAA0B,SAAQ,YAAY;aAC7B,OAAO,EAAE,MAAM;gBAAf,OAAO,EAAE,MAAM;CAM5C;AAED,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG;IACjD,KAAK,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAmDF,qBAAa,gBAAiB,SAAQ,YAAY;aAE9B,YAAY,EAAE,MAAM;aACpB,OAAO,EAAE,kBAAkB;aAC3B,aAAa,CAAC,EAAE,MAAM;gBAFtB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,kBAAkB,EAC3B,aAAa,CAAC,EAAE,MAAM,YAAA;IAOxC,IAAI,KAAK,qBAER;CACF"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EACL,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,qCAAqC,CAAC;AAE7C,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D;AAED,qBAAa,yBAA0B,SAAQ,YAAY;aAC7B,OAAO,EAAE,MAAM;gBAAf,OAAO,EAAE,MAAM;CAM5C;AAED,qBAAa,4BAA6B,SAAQ,YAAY;aAE1C,WAAW,EAAE,MAAM;aACnB,QAAQ,EAAE,MAAM;gBADhB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM;CAQnC;AAED,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG;IACjD,KAAK,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAmDF,qBAAa,gBAAiB,SAAQ,YAAY;aAE9B,YAAY,EAAE,MAAM;aACpB,OAAO,EAAE,kBAAkB;aAC3B,aAAa,CAAC,EAAE,MAAM;gBAFtB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,kBAAkB,EAC3B,aAAa,CAAC,EAAE,MAAM,YAAA;IAOxC,IAAI,KAAK,qBAER;CACF"}
package/dist/errors.js CHANGED
@@ -28,6 +28,17 @@ export class PlatformReadyTimeoutError extends HarnessError {
28
28
  this.name = 'PlatformReadyTimeoutError';
29
29
  }
30
30
  }
31
+ export class MetroPortRangeExhaustedError extends HarnessError {
32
+ initialPort;
33
+ attempts;
34
+ constructor(initialPort, attempts) {
35
+ const finalPort = initialPort + attempts - 1;
36
+ super(`Harness could not find an available Metro port in the range ${initialPort}-${finalPort}.`);
37
+ this.initialPort = initialPort;
38
+ this.attempts = attempts;
39
+ this.name = 'MetroPortRangeExhaustedError';
40
+ }
41
+ }
31
42
  const buildNativeCrashMessage = ({ phase, summary, signal, exceptionType, processName, pid, stackTrace, artifactType, }) => {
32
43
  const lines = [
33
44
  phase === 'startup'
package/dist/harness.d.ts CHANGED
@@ -18,6 +18,7 @@ export type HarnessRunState = {
18
18
  };
19
19
  export type Harness = {
20
20
  context: HarnessContext;
21
+ config: HarnessConfig;
21
22
  runTests: (path: string, options: HarnessRunTestsOptions) => Promise<TestSuiteResult>;
22
23
  ensureAppReady: (testFilePath: string) => Promise<void>;
23
24
  restart: (testFilePath?: string) => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,YAAY,EACb,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAEL,cAAc,EAGd,oBAAoB,EACpB,eAAe,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAEL,KAAK,gBAAgB,EACrB,eAAe,EAEf,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAIL,KAAK,aAAa,EAGnB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAc/B,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAE/E,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,sBAAsB,KAC5B,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9B,cAAc,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,OAAO,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,eAAe,EAAE,eAAe,CAAC;IACjC,QAAQ,EAAE,oBAAoB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC,UAAU,CAAC,CAAC;IAC3E,WAAW,EAAE,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,CAAC;IACxD,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;CAC3C,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,IAIF,CAAC;AAgDF,eAAO,MAAM,eAAe,GAAU,SAAS;IAC7C,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC,KAAG,OAAO,CAAC,IAAI,CA2Ef,CAAC;AAilBF,eAAO,MAAM,UAAU,GACrB,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,OAAO,CAAC,OAAO,CAYjB,CAAC"}
1
+ {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,YAAY,EACb,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAEL,cAAc,EAGd,oBAAoB,EACpB,eAAe,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAEL,KAAK,gBAAgB,EACrB,eAAe,EAEf,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAIL,KAAK,aAAa,EAGnB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAgB/B,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAE/E,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,sBAAsB,KAC5B,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9B,cAAc,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,OAAO,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,eAAe,EAAE,eAAe,CAAC;IACjC,QAAQ,EAAE,oBAAoB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC,UAAU,CAAC,CAAC;IAC3E,WAAW,EAAE,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,CAAC;IACxD,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;CAC3C,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,IAIF,CAAC;AAgDF,eAAO,MAAM,eAAe,GAAU,SAAS;IAC7C,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC,KAAG,OAAO,CAAC,IAAI,CA2Ef,CAAC;AAomBF,eAAO,MAAM,UAAU,GACrB,QAAQ,aAAa,EACrB,UAAU,eAAe,EACzB,aAAa,MAAM,KAClB,OAAO,CAAC,OAAO,CAYjB,CAAC"}
package/dist/harness.js CHANGED
@@ -7,8 +7,9 @@ import { PlatformReadyTimeoutError } from './errors.js';
7
7
  import { createCrashSupervisor, } from './crash-supervisor.js';
8
8
  import { createClientLogListener } from './client-log-handler.js';
9
9
  import path from 'node:path';
10
- import { logMetroCacheReused, logRunnerStarting, logRunnerStillWaitingInQueue, logRunnerWaitingInQueue, } from './logs.js';
10
+ import { logMetroCacheReused, logMetroPortFallback, logRunnerStarting, logRunnerStillWaitingInQueue, logRunnerWaitingInQueue, } from './logs.js';
11
11
  import { createResourceLockManager } from './resource-lock.js';
12
+ import { resolveHarnessMetroPort } from './metro-port.js';
12
13
  const harnessLogger = logger.child('runtime');
13
14
  const resourceLockManager = createResourceLockManager();
14
15
  export const maybeLogMetroCacheReuse = (config, platform, projectRoot) => {
@@ -137,12 +138,21 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
137
138
  }
138
139
  harnessLogger.debug('resource lock acquired for runner=%s key=%s', platform.name, resourceLockKey);
139
140
  try {
140
- maybeLogMetroCacheReuse(config, platform, projectRoot);
141
+ const { config: runtimeConfig, metroPortLease, initialMetroPort, didFallback, } = await resolveHarnessMetroPort({
142
+ config,
143
+ platform,
144
+ resourceLockManager,
145
+ signal,
146
+ });
147
+ if (didFallback) {
148
+ logMetroPortFallback(initialMetroPort, runtimeConfig.metroPort);
149
+ }
150
+ maybeLogMetroCacheReuse(runtimeConfig, platform, projectRoot);
141
151
  const pluginAbortController = new AbortController();
142
152
  const pluginManager = createHarnessPluginManager({
143
- plugins: (config.plugins ?? []),
153
+ plugins: (runtimeConfig.plugins ?? []),
144
154
  projectRoot,
145
- config,
155
+ config: runtimeConfig,
146
156
  runner: platform,
147
157
  abortSignal: pluginAbortController.signal,
148
158
  });
@@ -182,7 +192,7 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
182
192
  };
183
193
  const serverBridge = await getBridgeServer({
184
194
  noServer: true,
185
- timeout: config.bridgeTimeout,
195
+ timeout: runtimeConfig.bridgeTimeout,
186
196
  context,
187
197
  });
188
198
  harnessLogger.debug('starting Metro, platform runner, and bridge initialization');
@@ -192,7 +202,7 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
192
202
  return await Promise.all([
193
203
  getMetroInstance({
194
204
  projectRoot,
195
- harnessConfig: config,
205
+ harnessConfig: runtimeConfig,
196
206
  websocketEndpoints: {
197
207
  [HARNESS_BRIDGE_PATH]: serverBridge.ws,
198
208
  },
@@ -201,11 +211,11 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
201
211
  return instance;
202
212
  }),
203
213
  withPlatformReadyTimeout({
204
- timeout: config.platformReadyTimeout,
214
+ timeout: runtimeConfig.platformReadyTimeout,
205
215
  signal,
206
216
  work: async () => {
207
217
  return await import(platform.runner)
208
- .then((module) => module.default(platform.config, config, {
218
+ .then((module) => module.default(platform.config, runtimeConfig, {
209
219
  signal,
210
220
  }))
211
221
  .then((instance) => {
@@ -219,6 +229,7 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
219
229
  catch (error) {
220
230
  await Promise.allSettled([
221
231
  resourceLease.release(),
232
+ metroPortLease?.release(),
222
233
  serverBridge.dispose(),
223
234
  ]);
224
235
  throw error;
@@ -422,7 +433,7 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
422
433
  metroInstance.events.addListener(onMetroEvent);
423
434
  appMonitor.addListener(onAppMonitorEvent);
424
435
  harnessLogger.debug('registered runtime, bridge, and Metro listeners');
425
- if (config.forwardClientLogs) {
436
+ if (runtimeConfig.forwardClientLogs) {
426
437
  metroInstance.events.addListener(clientLogListener);
427
438
  harnessLogger.debug('client log forwarding enabled');
428
439
  }
@@ -451,7 +462,7 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
451
462
  catch (error) {
452
463
  hookError = error;
453
464
  }
454
- if (config.forwardClientLogs) {
465
+ if (runtimeConfig.forwardClientLogs) {
455
466
  metroInstance.events.removeListener(clientLogListener);
456
467
  }
457
468
  metroInstance.events.removeListener(onMetroEvent);
@@ -466,6 +477,7 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
466
477
  serverBridge.dispose(),
467
478
  platformInstance.dispose(),
468
479
  metroInstance.dispose(),
480
+ metroPortLease?.release(),
469
481
  ]);
470
482
  }
471
483
  catch (error) {
@@ -527,9 +539,9 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
527
539
  serverBridge,
528
540
  platformInstance: platformInstance,
529
541
  platformId: platform.platformId,
530
- bundleStartTimeout: config.bundleStartTimeout ?? 60000,
531
- readyTimeout: config.bridgeTimeout,
532
- maxAppRestarts: config.maxAppRestarts ?? 2,
542
+ bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
543
+ readyTimeout: runtimeConfig.bridgeTimeout,
544
+ maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
533
545
  testFilePath,
534
546
  crashSupervisor,
535
547
  appLaunchOptions,
@@ -560,6 +572,7 @@ const getHarnessInternal = async (config, platform, projectRoot, signal) => {
560
572
  };
561
573
  return {
562
574
  context,
575
+ config: runtimeConfig,
563
576
  runTests: async (path, options) => {
564
577
  await flushPendingHooks();
565
578
  activeTestFilePath = path;
package/dist/logs.d.ts CHANGED
@@ -8,5 +8,6 @@ export declare const logRunnerStillWaitingInQueue: (runner: HarnessPlatform) =>
8
8
  export declare const logRunnerStarting: (runner: HarnessPlatform) => void;
9
9
  export declare const logMetroPrewarmCompleted: (runner: HarnessPlatform) => void;
10
10
  export declare const logMetroCacheReused: (runner: HarnessPlatform) => void;
11
+ export declare const logMetroPortFallback: (initialPort: number, selectedPort: number) => void;
11
12
  export declare const getErrorMessage: (error: HarnessError) => string;
12
13
  //# sourceMappingURL=logs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"logs.d.ts","sourceRoot":"","sources":["../src/logs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAY3D,eAAO,MAAM,GAAG,GAAI,SAAS,MAAM,KAAG,IAErC,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,QAAQ,eAAe,KAAG,IAI1D,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,eAAe,KAAG,IAEjE,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,eAAe,KAAG,IAEjE,CAAC;AAEF,eAAO,MAAM,4BAA4B,GAAI,QAAQ,eAAe,KAAG,IAEtE,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,QAAQ,eAAe,KAAG,IAE3D,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAI,QAAQ,eAAe,KAAG,IAElE,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,QAAQ,eAAe,KAAG,IAE7D,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,OAAO,YAAY,KAAG,MAErD,CAAC"}
1
+ {"version":3,"file":"logs.d.ts","sourceRoot":"","sources":["../src/logs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAY3D,eAAO,MAAM,GAAG,GAAI,SAAS,MAAM,KAAG,IAErC,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,QAAQ,eAAe,KAAG,IAI1D,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,eAAe,KAAG,IAEjE,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,eAAe,KAAG,IAEjE,CAAC;AAEF,eAAO,MAAM,4BAA4B,GAAI,QAAQ,eAAe,KAAG,IAEtE,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,QAAQ,eAAe,KAAG,IAE3D,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAI,QAAQ,eAAe,KAAG,IAElE,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,QAAQ,eAAe,KAAG,IAE7D,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,aAAa,MAAM,EACnB,cAAc,MAAM,KACnB,IAMF,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,OAAO,YAAY,KAAG,MAErD,CAAC"}
package/dist/logs.js CHANGED
@@ -30,6 +30,9 @@ export const logMetroPrewarmCompleted = (runner) => {
30
30
  export const logMetroCacheReused = (runner) => {
31
31
  log(`${TAG} Reusing Metro cache for ${chalk.bold(runner.name)}\n`);
32
32
  };
33
+ export const logMetroPortFallback = (initialPort, selectedPort) => {
34
+ log(`${TAG} Harness could not use Metro port ${chalk.bold(String(initialPort))} and is using ${chalk.bold(String(selectedPort))} instead.\n`);
35
+ };
33
36
  export const getErrorMessage = (error) => {
34
37
  return `${ERROR_TAG} ${error.message}\n`;
35
38
  };
@@ -0,0 +1,16 @@
1
+ import type { Config as HarnessConfig } from '@react-native-harness/config';
2
+ import type { HarnessPlatform } from '@react-native-harness/platforms';
3
+ import type { ResourceLease, ResourceLockManager } from './resource-lock.js';
4
+ export declare const METRO_PORT_SCAN_ATTEMPTS = 10;
5
+ export declare const resolveHarnessMetroPort: (options: {
6
+ config: HarnessConfig;
7
+ platform: HarnessPlatform;
8
+ resourceLockManager: ResourceLockManager;
9
+ signal: AbortSignal;
10
+ }) => Promise<{
11
+ config: HarnessConfig;
12
+ metroPortLease: ResourceLease | null;
13
+ initialMetroPort: number;
14
+ didFallback: boolean;
15
+ }>;
16
+ //# sourceMappingURL=metro-port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metro-port.d.ts","sourceRoot":"","sources":["../src/metro-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAEvE,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE7E,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAU3C,eAAO,MAAM,uBAAuB,GAAU,SAAS;IACrD,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,eAAe,CAAC;IAC1B,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,MAAM,EAAE,WAAW,CAAC;CACrB,KAAG,OAAO,CAAC;IACV,MAAM,EAAE,aAAa,CAAC;IACtB,cAAc,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;CACtB,CAiDA,CAAC"}
@@ -0,0 +1,48 @@
1
+ import { isPortAvailable } from '@react-native-harness/bundler-metro';
2
+ import { MetroPortRangeExhaustedError } from './errors.js';
3
+ export const METRO_PORT_SCAN_ATTEMPTS = 10;
4
+ const getPortLockKey = (host, port) => {
5
+ return `metro-port:${host?.trim() || '*'}:${port}`;
6
+ };
7
+ const isIosPhysicalRunner = (platform) => {
8
+ return platform.platformId === 'ios' && platform.name.includes('device');
9
+ };
10
+ export const resolveHarnessMetroPort = async (options) => {
11
+ const { config, platform, resourceLockManager, signal } = options;
12
+ const initialMetroPort = config.metroPort;
13
+ const host = config.host?.trim();
14
+ if (isIosPhysicalRunner(platform)) {
15
+ return {
16
+ config,
17
+ metroPortLease: null,
18
+ initialMetroPort,
19
+ didFallback: false,
20
+ };
21
+ }
22
+ for (let attempt = 0; attempt < METRO_PORT_SCAN_ATTEMPTS; attempt += 1) {
23
+ const candidatePort = initialMetroPort + attempt;
24
+ const metroPortLease = await resourceLockManager.acquire(getPortLockKey(host, candidatePort), { signal });
25
+ try {
26
+ if (!(await isPortAvailable(candidatePort, host))) {
27
+ await metroPortLease.release();
28
+ continue;
29
+ }
30
+ return {
31
+ config: candidatePort === initialMetroPort
32
+ ? config
33
+ : {
34
+ ...config,
35
+ metroPort: candidatePort,
36
+ },
37
+ metroPortLease,
38
+ initialMetroPort,
39
+ didFallback: candidatePort !== initialMetroPort,
40
+ };
41
+ }
42
+ catch (error) {
43
+ await metroPortLease.release();
44
+ throw error;
45
+ }
46
+ }
47
+ throw new MetroPortRangeExhaustedError(initialMetroPort, METRO_PORT_SCAN_ATTEMPTS);
48
+ };
@@ -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;AAgBF,eAAO,MAAM,mBAAmB,GAAI,KAAK,MAAM,KAAG,MAEjD,CAAC;AAoOF,eAAO,MAAM,yBAAyB,GACpC,UAAS,0BAA+B,KACvC,mBA2JF,CAAC"}
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;AAgBF,eAAO,MAAM,mBAAmB,GAAI,KAAK,MAAM,KAAG,MAEjD,CAAC;AAmPF,eAAO,MAAM,yBAAyB,GACpC,UAAS,0BAA+B,KACvC,mBAkKF,CAAC"}
@@ -56,6 +56,17 @@ const readJsonFile = async (filePath) => {
56
56
  throw error;
57
57
  }
58
58
  };
59
+ const writeJsonFileAtomic = async (filePath, value) => {
60
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
61
+ try {
62
+ await fs.writeFile(tempPath, JSON.stringify(value), 'utf8');
63
+ await fs.rename(tempPath, filePath);
64
+ }
65
+ catch (error) {
66
+ await removeFileIfPresent(tempPath);
67
+ throw error;
68
+ }
69
+ };
59
70
  const removeFileIfPresent = async (filePath) => {
60
71
  try {
61
72
  await fs.rm(filePath, { force: true });
@@ -189,6 +200,7 @@ export const createResourceLockManager = (options = {}) => {
189
200
  await fs.writeFile(ticketPath, JSON.stringify(metadata), 'utf8');
190
201
  scopedLogger.debug('queued ticket %s for key %s', ticketId, key);
191
202
  let heartbeatTimer = null;
203
+ let heartbeatInFlight = false;
192
204
  let released = false;
193
205
  let didNotifyWait = false;
194
206
  const waitStartedAt = Date.now();
@@ -210,20 +222,29 @@ export const createResourceLockManager = (options = {}) => {
210
222
  };
211
223
  const startHeartbeat = () => {
212
224
  heartbeatTimer = setInterval(async () => {
213
- const nextHeartbeatAt = Date.now();
214
- const owner = await readJsonFile(paths.ownerFilePath);
215
- if (released || owner?.ticketId !== ticketId) {
225
+ if (heartbeatInFlight) {
216
226
  return;
217
227
  }
218
- const nextMetadata = {
219
- ...owner,
220
- heartbeatAt: nextHeartbeatAt,
221
- };
222
- if (released) {
223
- return;
228
+ heartbeatInFlight = true;
229
+ try {
230
+ const nextHeartbeatAt = Date.now();
231
+ const owner = await readJsonFile(paths.ownerFilePath);
232
+ if (released || owner?.ticketId !== ticketId) {
233
+ return;
234
+ }
235
+ const nextMetadata = {
236
+ ...owner,
237
+ heartbeatAt: nextHeartbeatAt,
238
+ };
239
+ if (released) {
240
+ return;
241
+ }
242
+ await writeJsonFileAtomic(paths.ownerFilePath, nextMetadata);
243
+ scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
244
+ }
245
+ finally {
246
+ heartbeatInFlight = false;
224
247
  }
225
- await fs.writeFile(paths.ownerFilePath, JSON.stringify(nextMetadata), 'utf8');
226
- scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
227
248
  }, heartbeatIntervalMs);
228
249
  heartbeatTimer.unref?.();
229
250
  };
package/dist/setup.js CHANGED
@@ -75,6 +75,6 @@ export const setup = async (globalConfig) => {
75
75
  const harness = await getHarness(harnessConfig, selectedRunner, globalConfig.rootDir);
76
76
  logTestEnvironmentReady(selectedRunner);
77
77
  setupLogger.debug('Harness instance is ready');
78
- global.HARNESS_CONFIG = harnessConfig;
78
+ global.HARNESS_CONFIG = harness.config;
79
79
  global.HARNESS = harness;
80
80
  };