@react-native-harness/jest 1.1.0-rc.2 → 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.
- package/dist/__tests__/errors.test.js +6 -1
- package/dist/__tests__/harness-cache.test.js +1 -1
- package/dist/__tests__/harness.test.js +205 -3
- 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.d.ts +2 -0
- package/dist/__tests__/resource-lock.test.d.ts.map +1 -0
- package/dist/__tests__/resource-lock.test.js +138 -0
- package/dist/action-hooks.d.ts +9 -0
- package/dist/action-hooks.d.ts.map +1 -0
- package/dist/action-hooks.js +36 -0
- package/dist/cli-args.test.d.ts +2 -0
- package/dist/cli-args.test.d.ts.map +1 -0
- package/dist/cli-args.test.js +1 -0
- package/dist/crash-monitor.d.ts +16 -0
- package/dist/crash-monitor.d.ts.map +1 -0
- package/dist/crash-monitor.js +75 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -2
- package/dist/event-mapper.d.ts +16 -0
- package/dist/event-mapper.d.ts.map +1 -0
- package/dist/event-mapper.js +120 -0
- package/dist/global-setup.d.ts +3 -0
- package/dist/global-setup.d.ts.map +1 -0
- package/dist/global-setup.js +60 -0
- package/dist/global-teardown.d.ts +3 -0
- package/dist/global-teardown.d.ts.map +1 -0
- package/dist/global-teardown.js +11 -0
- package/dist/harness-reporter.d.ts +8 -0
- package/dist/harness-reporter.d.ts.map +1 -0
- package/dist/harness-reporter.js +12 -0
- package/dist/harness.d.ts +1 -0
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +491 -386
- package/dist/jest-options.d.ts +2 -0
- package/dist/jest-options.d.ts.map +1 -0
- package/dist/jest-options.js +1 -0
- package/dist/logs.d.ts +4 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +12 -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 +25 -0
- package/dist/resource-lock.d.ts.map +1 -0
- package/dist/resource-lock.js +299 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +12 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +7 -7
- package/src/__tests__/errors.test.ts +18 -6
- package/src/__tests__/harness-cache.test.ts +2 -2
- package/src/__tests__/harness.test.ts +342 -19
- package/src/__tests__/metro-port.test.ts +61 -0
- package/src/__tests__/resource-lock.test.ts +192 -0
- package/src/action-hooks.ts +66 -0
- package/src/errors.ts +23 -4
- package/src/harness.ts +609 -447
- package/src/logs.ts +24 -3
- package/src/metro-port.ts +76 -0
- package/src/resource-lock.ts +472 -0
- package/src/setup.ts +22 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jest-options.d.ts","sourceRoot":"","sources":["../src/jest-options.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/logs.d.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { HarnessError } from '@react-native-harness/tools';
|
|
|
3
3
|
export declare const log: (message: string) => void;
|
|
4
4
|
export declare const logTestRunHeader: (runner: HarnessPlatform) => void;
|
|
5
5
|
export declare const logTestEnvironmentReady: (runner: HarnessPlatform) => void;
|
|
6
|
+
export declare const logRunnerWaitingInQueue: (runner: HarnessPlatform) => void;
|
|
7
|
+
export declare const logRunnerStillWaitingInQueue: (runner: HarnessPlatform) => void;
|
|
8
|
+
export declare const logRunnerStarting: (runner: HarnessPlatform) => void;
|
|
6
9
|
export declare const logMetroPrewarmCompleted: (runner: HarnessPlatform) => void;
|
|
7
10
|
export declare const logMetroCacheReused: (runner: HarnessPlatform) => void;
|
|
11
|
+
export declare const logMetroPortFallback: (initialPort: number, selectedPort: number) => void;
|
|
8
12
|
export declare const getErrorMessage: (error: HarnessError) => string;
|
|
9
13
|
//# sourceMappingURL=logs.d.ts.map
|
package/dist/logs.d.ts.map
CHANGED
|
@@ -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,wBAAwB,GAAI,QAAQ,eAAe,KAAG,
|
|
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
|
@@ -15,12 +15,24 @@ export const logTestRunHeader = (runner) => {
|
|
|
15
15
|
export const logTestEnvironmentReady = (runner) => {
|
|
16
16
|
log(`${TAG} Runner ${chalk.bold(runner.name)} ready\n`);
|
|
17
17
|
};
|
|
18
|
+
export const logRunnerWaitingInQueue = (runner) => {
|
|
19
|
+
log(`${TAG} Runner ${chalk.bold(runner.name)} is busy, waiting in queue\n`);
|
|
20
|
+
};
|
|
21
|
+
export const logRunnerStillWaitingInQueue = (runner) => {
|
|
22
|
+
log(`${TAG} Still waiting in queue for ${chalk.bold(runner.name)} runner\n`);
|
|
23
|
+
};
|
|
24
|
+
export const logRunnerStarting = (runner) => {
|
|
25
|
+
log(`${TAG} Runner ${chalk.bold(runner.name)} is starting\n`);
|
|
26
|
+
};
|
|
18
27
|
export const logMetroPrewarmCompleted = (runner) => {
|
|
19
28
|
log(`${TAG} Metro pre-warm for ${chalk.bold(runner.name)} completed\n`);
|
|
20
29
|
};
|
|
21
30
|
export const logMetroCacheReused = (runner) => {
|
|
22
31
|
log(`${TAG} Reusing Metro cache for ${chalk.bold(runner.name)}\n`);
|
|
23
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
|
+
};
|
|
24
36
|
export const getErrorMessage = (error) => {
|
|
25
37
|
return `${ERROR_TAG} ${error.message}\n`;
|
|
26
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
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type HarnessLogger } from '@react-native-harness/tools';
|
|
2
|
+
export type ResourceLockAcquireOptions = {
|
|
3
|
+
signal?: AbortSignal;
|
|
4
|
+
onWait?: () => void;
|
|
5
|
+
onStillWaiting?: (elapsedMs: number) => void;
|
|
6
|
+
};
|
|
7
|
+
export type ResourceLease = {
|
|
8
|
+
release: () => Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
export type ResourceLockManager = {
|
|
11
|
+
acquire: (key: string, options?: ResourceLockAcquireOptions) => Promise<ResourceLease>;
|
|
12
|
+
};
|
|
13
|
+
type ResourceLockManagerOptions = {
|
|
14
|
+
rootDir?: string;
|
|
15
|
+
pollIntervalMs?: number;
|
|
16
|
+
heartbeatIntervalMs?: number;
|
|
17
|
+
staleLockTimeoutMs?: number;
|
|
18
|
+
pid?: number;
|
|
19
|
+
logger?: HarnessLogger;
|
|
20
|
+
isProcessActive?: (pid: number) => boolean;
|
|
21
|
+
};
|
|
22
|
+
export declare const hashResourceLockKey: (key: string) => string;
|
|
23
|
+
export declare const createResourceLockManager: (options?: ResourceLockManagerOptions) => ResourceLockManager;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=resource-lock.d.ts.map
|
|
@@ -0,0 +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;AAmPF,eAAO,MAAM,yBAAyB,GACpC,UAAS,0BAA+B,KACvC,mBAkKF,CAAC"}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { logger } from '@react-native-harness/tools';
|
|
6
|
+
const resourceLockLogger = logger.child('resource-lock');
|
|
7
|
+
const DEFAULT_ROOT_DIR = path.join(os.tmpdir(), 'react-native-harness-locks');
|
|
8
|
+
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
9
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 2000;
|
|
10
|
+
const DEFAULT_STALE_LOCK_TIMEOUT_MS = 15000;
|
|
11
|
+
const wait = async (ms) => {
|
|
12
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
};
|
|
14
|
+
const createAbortError = () => new DOMException('The operation was aborted', 'AbortError');
|
|
15
|
+
export const hashResourceLockKey = (key) => {
|
|
16
|
+
return crypto.createHash('sha256').update(key).digest('hex');
|
|
17
|
+
};
|
|
18
|
+
const getPathsForKey = (rootDir, key) => {
|
|
19
|
+
const keyDir = path.join(rootDir, hashResourceLockKey(key));
|
|
20
|
+
return {
|
|
21
|
+
rootDir,
|
|
22
|
+
keyDir,
|
|
23
|
+
queueDir: path.join(keyDir, 'queue'),
|
|
24
|
+
ownerFilePath: path.join(keyDir, 'owner.json'),
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const createTicketId = (createdAt, pid) => {
|
|
28
|
+
return `${createdAt
|
|
29
|
+
.toString()
|
|
30
|
+
.padStart(16, '0')}-${pid}-${crypto.randomUUID()}`;
|
|
31
|
+
};
|
|
32
|
+
const isMissingFileError = (error) => {
|
|
33
|
+
return (error instanceof Error &&
|
|
34
|
+
'code' in error &&
|
|
35
|
+
typeof error.code === 'string' &&
|
|
36
|
+
error.code === 'ENOENT');
|
|
37
|
+
};
|
|
38
|
+
const isExclusiveCreateError = (error) => {
|
|
39
|
+
return (error instanceof Error &&
|
|
40
|
+
'code' in error &&
|
|
41
|
+
typeof error.code === 'string' &&
|
|
42
|
+
error.code === 'EEXIST');
|
|
43
|
+
};
|
|
44
|
+
const ensureLockDirectories = async (paths) => {
|
|
45
|
+
await fs.mkdir(paths.queueDir, { recursive: true });
|
|
46
|
+
};
|
|
47
|
+
const readJsonFile = async (filePath) => {
|
|
48
|
+
try {
|
|
49
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
50
|
+
return JSON.parse(content);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (isMissingFileError(error)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
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
|
+
};
|
|
70
|
+
const removeFileIfPresent = async (filePath) => {
|
|
71
|
+
try {
|
|
72
|
+
await fs.rm(filePath, { force: true });
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!isMissingFileError(error)) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const isPidActive = (pid) => {
|
|
81
|
+
try {
|
|
82
|
+
process.kill(pid, 0);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return !(error instanceof Error &&
|
|
87
|
+
'code' in error &&
|
|
88
|
+
typeof error.code === 'string' &&
|
|
89
|
+
error.code === 'ESRCH');
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const isMetadataStale = (metadata, now, staleLockTimeoutMs, isProcessActive) => {
|
|
93
|
+
if (!isProcessActive(metadata.pid)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return now - metadata.heartbeatAt > staleLockTimeoutMs;
|
|
97
|
+
};
|
|
98
|
+
const isQueuedTicketStale = (metadata, isProcessActive) => {
|
|
99
|
+
return !isProcessActive(metadata.pid);
|
|
100
|
+
};
|
|
101
|
+
const waitForPollInterval = (ms, signal) => {
|
|
102
|
+
if (!signal) {
|
|
103
|
+
return wait(ms);
|
|
104
|
+
}
|
|
105
|
+
if (signal.aborted) {
|
|
106
|
+
return Promise.reject(signal.reason ?? createAbortError());
|
|
107
|
+
}
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const timer = setTimeout(() => {
|
|
110
|
+
signal.removeEventListener('abort', onAbort);
|
|
111
|
+
resolve();
|
|
112
|
+
}, ms);
|
|
113
|
+
const onAbort = () => {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
signal.removeEventListener('abort', onAbort);
|
|
116
|
+
reject(signal.reason ?? createAbortError());
|
|
117
|
+
};
|
|
118
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const readQueueTickets = async (queueDir) => {
|
|
122
|
+
const ticketEntries = await fs.readdir(queueDir, { withFileTypes: true });
|
|
123
|
+
const tickets = await Promise.all(ticketEntries
|
|
124
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
125
|
+
.map(async (entry) => ({
|
|
126
|
+
name: entry.name,
|
|
127
|
+
metadata: await readJsonFile(path.join(queueDir, entry.name)),
|
|
128
|
+
})));
|
|
129
|
+
return tickets
|
|
130
|
+
.filter((entry) => entry.metadata !== null)
|
|
131
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
132
|
+
.map((entry) => entry.metadata);
|
|
133
|
+
};
|
|
134
|
+
const cleanupQueue = async (options) => {
|
|
135
|
+
const { paths, currentTicketId, logger, isProcessActive } = options;
|
|
136
|
+
const tickets = await readQueueTickets(paths.queueDir);
|
|
137
|
+
const activeTickets = [];
|
|
138
|
+
for (const ticket of tickets) {
|
|
139
|
+
const isCurrentTicket = ticket.ticketId === currentTicketId;
|
|
140
|
+
const isStale = !isCurrentTicket && isQueuedTicketStale(ticket, isProcessActive);
|
|
141
|
+
if (isStale) {
|
|
142
|
+
logger.debug('removing stale queued ticket %s for key %s', ticket.ticketId, ticket.key);
|
|
143
|
+
await removeFileIfPresent(path.join(paths.queueDir, `${ticket.ticketId}.json`));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
activeTickets.push(ticket);
|
|
147
|
+
}
|
|
148
|
+
return activeTickets;
|
|
149
|
+
};
|
|
150
|
+
const maybeClearStaleOwner = async (options) => {
|
|
151
|
+
const { ownerFilePath, staleLockTimeoutMs, now, logger, isProcessActive } = options;
|
|
152
|
+
const owner = await readJsonFile(ownerFilePath);
|
|
153
|
+
if (!owner) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (!isMetadataStale(owner, now, staleLockTimeoutMs, isProcessActive)) {
|
|
157
|
+
return owner;
|
|
158
|
+
}
|
|
159
|
+
logger.debug('removing stale owner ticket %s for key %s', owner.ticketId, owner.key);
|
|
160
|
+
await removeFileIfPresent(ownerFilePath);
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
163
|
+
const claimOwnership = async (ownerFilePath, metadata) => {
|
|
164
|
+
try {
|
|
165
|
+
await fs.writeFile(ownerFilePath, JSON.stringify(metadata), {
|
|
166
|
+
encoding: 'utf8',
|
|
167
|
+
flag: 'wx',
|
|
168
|
+
});
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (isExclusiveCreateError(error)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
export const createResourceLockManager = (options = {}) => {
|
|
179
|
+
const rootDir = options.rootDir ?? DEFAULT_ROOT_DIR;
|
|
180
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
181
|
+
const heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
182
|
+
const staleLockTimeoutMs = options.staleLockTimeoutMs ?? DEFAULT_STALE_LOCK_TIMEOUT_MS;
|
|
183
|
+
const pid = options.pid ?? process.pid;
|
|
184
|
+
const scopedLogger = options.logger ?? resourceLockLogger;
|
|
185
|
+
const isProcessActive = options.isProcessActive ?? isPidActive;
|
|
186
|
+
return {
|
|
187
|
+
acquire: async (key, acquireOptions = {}) => {
|
|
188
|
+
const paths = getPathsForKey(rootDir, key);
|
|
189
|
+
await ensureLockDirectories(paths);
|
|
190
|
+
const createdAt = Date.now();
|
|
191
|
+
const ticketId = createTicketId(createdAt, pid);
|
|
192
|
+
const ticketPath = path.join(paths.queueDir, `${ticketId}.json`);
|
|
193
|
+
const metadata = {
|
|
194
|
+
ticketId,
|
|
195
|
+
key,
|
|
196
|
+
pid,
|
|
197
|
+
createdAt,
|
|
198
|
+
heartbeatAt: createdAt,
|
|
199
|
+
};
|
|
200
|
+
await fs.writeFile(ticketPath, JSON.stringify(metadata), 'utf8');
|
|
201
|
+
scopedLogger.debug('queued ticket %s for key %s', ticketId, key);
|
|
202
|
+
let heartbeatTimer = null;
|
|
203
|
+
let heartbeatInFlight = false;
|
|
204
|
+
let released = false;
|
|
205
|
+
let didNotifyWait = false;
|
|
206
|
+
const waitStartedAt = Date.now();
|
|
207
|
+
const release = async () => {
|
|
208
|
+
if (released) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
released = true;
|
|
212
|
+
if (heartbeatTimer) {
|
|
213
|
+
clearInterval(heartbeatTimer);
|
|
214
|
+
heartbeatTimer = null;
|
|
215
|
+
}
|
|
216
|
+
const owner = await readJsonFile(paths.ownerFilePath);
|
|
217
|
+
if (owner?.ticketId === ticketId) {
|
|
218
|
+
await removeFileIfPresent(paths.ownerFilePath);
|
|
219
|
+
}
|
|
220
|
+
await removeFileIfPresent(ticketPath);
|
|
221
|
+
scopedLogger.debug('released ticket %s for key %s', ticketId, key);
|
|
222
|
+
};
|
|
223
|
+
const startHeartbeat = () => {
|
|
224
|
+
heartbeatTimer = setInterval(async () => {
|
|
225
|
+
if (heartbeatInFlight) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
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;
|
|
247
|
+
}
|
|
248
|
+
}, heartbeatIntervalMs);
|
|
249
|
+
heartbeatTimer.unref?.();
|
|
250
|
+
};
|
|
251
|
+
try {
|
|
252
|
+
while (true) {
|
|
253
|
+
acquireOptions.signal?.throwIfAborted();
|
|
254
|
+
const now = Date.now();
|
|
255
|
+
const activeTickets = await cleanupQueue({
|
|
256
|
+
paths,
|
|
257
|
+
currentTicketId: ticketId,
|
|
258
|
+
logger: scopedLogger,
|
|
259
|
+
isProcessActive,
|
|
260
|
+
});
|
|
261
|
+
const ownIndex = activeTickets.findIndex((entry) => entry.ticketId === ticketId);
|
|
262
|
+
if (ownIndex === -1) {
|
|
263
|
+
throw new Error(`Queued ticket ${ticketId} disappeared before acquisition.`);
|
|
264
|
+
}
|
|
265
|
+
const owner = await maybeClearStaleOwner({
|
|
266
|
+
ownerFilePath: paths.ownerFilePath,
|
|
267
|
+
staleLockTimeoutMs,
|
|
268
|
+
now,
|
|
269
|
+
logger: scopedLogger,
|
|
270
|
+
isProcessActive,
|
|
271
|
+
});
|
|
272
|
+
if (ownIndex === 0 && owner === null) {
|
|
273
|
+
const claimed = await claimOwnership(paths.ownerFilePath, {
|
|
274
|
+
...metadata,
|
|
275
|
+
heartbeatAt: Date.now(),
|
|
276
|
+
});
|
|
277
|
+
if (claimed) {
|
|
278
|
+
await removeFileIfPresent(ticketPath);
|
|
279
|
+
startHeartbeat();
|
|
280
|
+
scopedLogger.debug('acquired lock for key %s with ticket %s', key, ticketId);
|
|
281
|
+
return { release };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!didNotifyWait) {
|
|
285
|
+
didNotifyWait = true;
|
|
286
|
+
acquireOptions.onWait?.();
|
|
287
|
+
}
|
|
288
|
+
acquireOptions.onStillWaiting?.(Date.now() - waitStartedAt);
|
|
289
|
+
scopedLogger.debug('waiting for key %s with ticket %s at queue position %d', key, ticketId, ownIndex + 1);
|
|
290
|
+
await waitForPollInterval(pollIntervalMs, acquireOptions.signal);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
await release();
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
};
|
package/dist/setup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AA4CxD,eAAO,MAAM,KAAK,GAAU,cAAc,UAAU,CAAC,YAAY,kBA6EhE,CAAC"}
|
package/dist/setup.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getAdditionalCliArgs } from './cli-args.js';
|
|
|
5
5
|
import { logTestEnvironmentReady, logTestRunHeader } from './logs.js';
|
|
6
6
|
import { NoRunnerSpecifiedError, RunnerNotFoundError } from './errors.js';
|
|
7
7
|
import { logger } from '@react-native-harness/tools';
|
|
8
|
+
import { createActionHooksPlugin } from './action-hooks.js';
|
|
8
9
|
const setupLogger = logger.child('setup');
|
|
9
10
|
const getHarnessConfig = async (globalConfig) => {
|
|
10
11
|
const projectRoot = globalConfig.rootDir;
|
|
@@ -45,7 +46,17 @@ export const setup = async (globalConfig) => {
|
|
|
45
46
|
metroPort: cliArgs.metroPort,
|
|
46
47
|
});
|
|
47
48
|
}
|
|
49
|
+
if (process.env.PRE_RUN_HOOK || process.env.AFTER_RUN_HOOK) {
|
|
50
|
+
harnessConfig = ConfigSchema.parse({
|
|
51
|
+
...harnessConfig,
|
|
52
|
+
plugins: [...(harnessConfig.plugins ?? []), createActionHooksPlugin()],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
48
55
|
const selectedRunner = getHarnessRunner(harnessConfig, cliArgs);
|
|
56
|
+
if (harnessConfig.webSocketPort != null &&
|
|
57
|
+
harnessConfig.webSocketPort !== harnessConfig.metroPort) {
|
|
58
|
+
logger.warn(`Config option "webSocketPort" is deprecated and ignored. Harness now uses metroPort (${harnessConfig.metroPort}) for bridge traffic.`);
|
|
59
|
+
}
|
|
49
60
|
if (globalConfig.collectCoverage) {
|
|
50
61
|
// This is going to be used by @react-native-harness/babel-preset
|
|
51
62
|
// to enable instrumentation of test files.
|
|
@@ -64,6 +75,6 @@ export const setup = async (globalConfig) => {
|
|
|
64
75
|
const harness = await getHarness(harnessConfig, selectedRunner, globalConfig.rootDir);
|
|
65
76
|
logTestEnvironmentReady(selectedRunner);
|
|
66
77
|
setupLogger.debug('Harness instance is ready');
|
|
67
|
-
global.HARNESS_CONFIG =
|
|
78
|
+
global.HARNESS_CONFIG = harness.config;
|
|
68
79
|
global.HARNESS = harness;
|
|
69
80
|
};
|