@harperfast/integration-testing 0.5.0 → 0.5.1
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/harperLifecycle.d.ts +262 -0
- package/dist/harperLifecycle.js +586 -0
- package/dist/harperLifecycle.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/loopbackAddressPool.d.ts +50 -0
- package/dist/loopbackAddressPool.js +348 -0
- package/dist/loopbackAddressPool.js.map +1 -0
- package/dist/portUtils.d.ts +26 -0
- package/dist/portUtils.js +47 -0
- package/dist/portUtils.js.map +1 -0
- package/dist/run.d.ts +2 -0
- package/dist/run.js +178 -0
- package/dist/run.js.map +1 -0
- package/dist/targz.d.ts +6 -0
- package/dist/targz.js +20 -0
- package/dist/targz.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This method attempts to validate all loopback addresses in the pool by trying to
|
|
3
|
+
* bind to each one. It returns an object containing arrays of successfully bound
|
|
4
|
+
* loopback addresses and those that failed along with their errors.
|
|
5
|
+
*
|
|
6
|
+
* It will check all loopback addresses from 127.0.0.2 to 127.0.0.33 (by default).
|
|
7
|
+
*
|
|
8
|
+
* Use the HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT environment variable to
|
|
9
|
+
* adjust the number of loopback addresses to validate (up to 254).
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateLoopbackAddressPool(): Promise<{
|
|
12
|
+
successful: string[];
|
|
13
|
+
failed: {
|
|
14
|
+
loopbackAddress: string;
|
|
15
|
+
error: Error;
|
|
16
|
+
}[];
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* Retrieves the next available loopback address from the pool using a file-based
|
|
20
|
+
* locking mechanism to safely allocate addresses across concurrent test processes.
|
|
21
|
+
*
|
|
22
|
+
* **How it works:**
|
|
23
|
+
* 1. Acquires a file-based lock to prevent race conditions with other processes
|
|
24
|
+
* 2. Reads the pool state (a JSON array of process IDs, with null for available slots)
|
|
25
|
+
* 3. Finds the first available (null) slot and assigns the current process PID to it
|
|
26
|
+
* 4. Writes the updated pool back to disk and releases the lock
|
|
27
|
+
* 5. Validates that the allocated address can actually be bound to
|
|
28
|
+
* 6. Returns the loopback address (e.g., "127.0.0.2")
|
|
29
|
+
*
|
|
30
|
+
* If no addresses are available, waits and retries until one becomes available.
|
|
31
|
+
*
|
|
32
|
+
* **Pool file location:** `${tmpdir()}/harper-integration-test-loopback-pool.json`
|
|
33
|
+
* **Lock file location:** `${tmpdir()}/harper-integration-test-loopback-pool.lock`
|
|
34
|
+
*
|
|
35
|
+
* @returns A promise that resolves with an allocated loopback address
|
|
36
|
+
* @throws {LoopbackAddressValidationError} If the allocated address cannot be bound to
|
|
37
|
+
*/
|
|
38
|
+
export declare function getNextAvailableLoopbackAddress(): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Releases a loopback address back to the pool, making it available for other processes.
|
|
41
|
+
*
|
|
42
|
+
* @param address The loopback address to release (e.g., "127.0.0.2")
|
|
43
|
+
* @throws InvalidLoopbackAddressError if the address format is invalid
|
|
44
|
+
*/
|
|
45
|
+
export declare function releaseLoopbackAddress(address: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Releases all loopback addresses assigned to the current process.
|
|
48
|
+
* Useful for cleanup during graceful shutdown.
|
|
49
|
+
*/
|
|
50
|
+
export declare function releaseAllLoopbackAddressesForCurrentProcess(): Promise<void>;
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { open, readFile, stat, unlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { createServer } from 'node:net';
|
|
6
|
+
// Configuration constants
|
|
7
|
+
// The pool starts at 127.0.0.2 by default (rather than 127.0.0.1) to avoid conflicting
|
|
8
|
+
// with other servers commonly bound to localhost. Override via the
|
|
9
|
+
// HARPER_INTEGRATION_TEST_LOOPBACK_POOL_START environment variable.
|
|
10
|
+
const HARPER_LOOPBACK_POOL_START = process.env.HARPER_INTEGRATION_TEST_LOOPBACK_POOL_START
|
|
11
|
+
? parseInt(process.env.HARPER_INTEGRATION_TEST_LOOPBACK_POOL_START, 10)
|
|
12
|
+
: 2;
|
|
13
|
+
if (HARPER_LOOPBACK_POOL_START < 1 || HARPER_LOOPBACK_POOL_START > 255) {
|
|
14
|
+
throw new Error('HARPER_INTEGRATION_TEST_LOOPBACK_POOL_START must be between 1 and 255');
|
|
15
|
+
}
|
|
16
|
+
const HARPER_LOOPBACK_POOL_COUNT = process.env.HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT
|
|
17
|
+
? parseInt(process.env.HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT, 10)
|
|
18
|
+
: 32;
|
|
19
|
+
const HARPER_LOOPBACK_POOL_MAX = 256 - HARPER_LOOPBACK_POOL_START;
|
|
20
|
+
if (HARPER_LOOPBACK_POOL_COUNT < 1 || HARPER_LOOPBACK_POOL_COUNT > HARPER_LOOPBACK_POOL_MAX) {
|
|
21
|
+
throw new Error(`HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT must be between 1 and ${HARPER_LOOPBACK_POOL_MAX}`);
|
|
22
|
+
}
|
|
23
|
+
const HARPER_LOOPBACK_POOL_PATH = join(tmpdir(), 'harper-integration-test-loopback-pool.json');
|
|
24
|
+
const HARPER_LOOPBACK_POOL_LOCK_PATH = join(tmpdir(), 'harper-integration-test-loopback-pool.lock');
|
|
25
|
+
// Constants for timeouts and retries
|
|
26
|
+
const LOCK_STALE_TIMEOUT_MS = 10000;
|
|
27
|
+
const RETRY_DELAY_MS = 1000;
|
|
28
|
+
// Custom error classes
|
|
29
|
+
class LoopbackAddressValidationError extends Error {
|
|
30
|
+
constructor(address, cause) {
|
|
31
|
+
super(`Failed to validate loopback address ${address}. This likely means your system does not have the required loopback addresses configured/enabled. Refer to the Harper Integration Test documentation (integrationTests/README.md) for more information.`);
|
|
32
|
+
this.name = 'LoopbackAddressValidationError';
|
|
33
|
+
if (cause) {
|
|
34
|
+
this.cause = cause;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
class InvalidLoopbackAddressError extends Error {
|
|
39
|
+
constructor(address) {
|
|
40
|
+
super(`Invalid loopback address format: ${address}. Expected format: 127.0.0.X where X is between ${HARPER_LOOPBACK_POOL_START} and ${HARPER_LOOPBACK_POOL_START + HARPER_LOOPBACK_POOL_COUNT - 1}`);
|
|
41
|
+
this.name = 'InvalidLoopbackAddressError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Acquires a file-based lock by creating the lock file. This enables safe concurrent
|
|
46
|
+
* access to the loopback pool across multiple test processes.
|
|
47
|
+
*
|
|
48
|
+
* Uses the 'wx' file flag which atomically fails if the file already exists, providing
|
|
49
|
+
* a simple but effective cross-process mutex. Handles stale locks by removing lock files
|
|
50
|
+
* older than LOCK_STALE_TIMEOUT_MS (10 seconds).
|
|
51
|
+
*
|
|
52
|
+
* @returns A promise that resolves when the lock is acquired
|
|
53
|
+
*/
|
|
54
|
+
async function acquireLock() {
|
|
55
|
+
while (true) {
|
|
56
|
+
try {
|
|
57
|
+
// The 'wx' flag causes the open to fail if the file already exists
|
|
58
|
+
const lockFileHandle = await open(HARPER_LOOPBACK_POOL_LOCK_PATH, 'wx');
|
|
59
|
+
// We have the lock - close the handle as we don't intend to write to it
|
|
60
|
+
await lockFileHandle.close();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// If the lock file already exists, it's either stale or we wait for it to be released
|
|
65
|
+
if (error.code === 'EEXIST') {
|
|
66
|
+
try {
|
|
67
|
+
const lockFileStat = await stat(HARPER_LOOPBACK_POOL_LOCK_PATH);
|
|
68
|
+
// If the lock file is older than the timeout, consider it stale and remove it
|
|
69
|
+
if (Date.now() - lockFileStat.mtimeMs > LOCK_STALE_TIMEOUT_MS) {
|
|
70
|
+
await unlink(HARPER_LOOPBACK_POOL_LOCK_PATH);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Lock file may have been removed by another process, continue
|
|
75
|
+
}
|
|
76
|
+
await sleep(RETRY_DELAY_MS);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Rethrow other errors
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Releases the file-based lock by deleting the lock file.
|
|
86
|
+
*/
|
|
87
|
+
async function releaseLock() {
|
|
88
|
+
try {
|
|
89
|
+
await unlink(HARPER_LOOPBACK_POOL_LOCK_PATH);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Ignore errors if lock file is already gone
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Executes a callback function while holding the lock. Automatically acquires
|
|
97
|
+
* and releases the lock, ensuring the lock is always released even if the callback
|
|
98
|
+
* throws an error.
|
|
99
|
+
*
|
|
100
|
+
* @param callback The async function to execute while holding the lock
|
|
101
|
+
* @returns The result of the callback function
|
|
102
|
+
*/
|
|
103
|
+
async function withLock(callback) {
|
|
104
|
+
await acquireLock();
|
|
105
|
+
try {
|
|
106
|
+
return await callback();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await releaseLock();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Reads the loopback pool from the pool file. The pool is a JSON array where each
|
|
114
|
+
* index represents a loopback address (127.0.0.2, 127.0.0.3, etc.) and the value
|
|
115
|
+
* is either null (available) or a process PID (in use).
|
|
116
|
+
*
|
|
117
|
+
* If the file doesn't exist, creates and returns a new empty pool with all addresses
|
|
118
|
+
* marked as available (null).
|
|
119
|
+
*
|
|
120
|
+
* @returns The loopback pool array
|
|
121
|
+
*/
|
|
122
|
+
async function readPoolFile() {
|
|
123
|
+
try {
|
|
124
|
+
const content = await readFile(HARPER_LOOPBACK_POOL_PATH, 'utf-8');
|
|
125
|
+
return JSON.parse(content);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error.code === 'ENOENT') {
|
|
129
|
+
// If the pool file doesn't exist yet, create it with null entries
|
|
130
|
+
return Array(HARPER_LOOPBACK_POOL_COUNT).fill(null);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Writes the loopback pool to the pool file as JSON.
|
|
137
|
+
*
|
|
138
|
+
* @param pool The loopback pool array to persist
|
|
139
|
+
*/
|
|
140
|
+
async function writePoolFile(pool) {
|
|
141
|
+
await writeFile(HARPER_LOOPBACK_POOL_PATH, JSON.stringify(pool));
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Finds the first available (null) index in the pool. This implements a simple
|
|
145
|
+
* first-available allocation strategy.
|
|
146
|
+
*
|
|
147
|
+
* @param pool The loopback pool array
|
|
148
|
+
* @returns The first available index, or null if the pool is full
|
|
149
|
+
*/
|
|
150
|
+
function findAvailableIndex(pool) {
|
|
151
|
+
for (let i = 0; i < pool.length; i++) {
|
|
152
|
+
if (pool[i] === null) {
|
|
153
|
+
return i;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Validates the format of a loopback address and extracts the pool index.
|
|
160
|
+
*
|
|
161
|
+
* Expects addresses in the format "127.0.0.X" where X is between the pool start
|
|
162
|
+
* (127.0.0.2) and the pool start + pool count - 1. The returned index is 0-based
|
|
163
|
+
* (e.g., "127.0.0.2" returns index 0).
|
|
164
|
+
*
|
|
165
|
+
* @param address The loopback address to parse (e.g., "127.0.0.2")
|
|
166
|
+
* @returns The 0-based pool index for this address
|
|
167
|
+
* @throws {InvalidLoopbackAddressError} If the address format is invalid or out of range
|
|
168
|
+
*/
|
|
169
|
+
function parseLoopbackAddress(address) {
|
|
170
|
+
const parts = address.split('.');
|
|
171
|
+
if (parts.length !== 4 || parts[0] !== '127' || parts[1] !== '0' || parts[2] !== '0') {
|
|
172
|
+
throw new InvalidLoopbackAddressError(address);
|
|
173
|
+
}
|
|
174
|
+
const index = parseInt(parts[3], 10) - HARPER_LOOPBACK_POOL_START;
|
|
175
|
+
if (isNaN(index) || index < 0 || index >= HARPER_LOOPBACK_POOL_COUNT) {
|
|
176
|
+
throw new InvalidLoopbackAddressError(address);
|
|
177
|
+
}
|
|
178
|
+
return index;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Validates that a given loopback address can be bound to by creating a temporary
|
|
182
|
+
* TCP server on that address. This ensures the loopback address is actually configured
|
|
183
|
+
* and available on the system before allocating it to a test process.
|
|
184
|
+
*
|
|
185
|
+
* The server is bound to port 0 (random port) just to verify the address exists,
|
|
186
|
+
* then immediately closed.
|
|
187
|
+
*
|
|
188
|
+
* @param loopbackAddress The loopback IP address to validate (e.g., "127.0.0.2")
|
|
189
|
+
* @returns A promise that resolves with the address if valid
|
|
190
|
+
* @throws An error with the loopbackAddress property if binding fails
|
|
191
|
+
*/
|
|
192
|
+
function validateLoopbackAddress(loopbackAddress) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const server = createServer();
|
|
195
|
+
server.once('error', (error) => {
|
|
196
|
+
const enhancedError = error;
|
|
197
|
+
enhancedError.loopbackAddress = loopbackAddress;
|
|
198
|
+
reject(enhancedError);
|
|
199
|
+
});
|
|
200
|
+
server.listen(0, loopbackAddress, () => {
|
|
201
|
+
server.close(() => {
|
|
202
|
+
resolve(loopbackAddress);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* This method attempts to validate all loopback addresses in the pool by trying to
|
|
209
|
+
* bind to each one. It returns an object containing arrays of successfully bound
|
|
210
|
+
* loopback addresses and those that failed along with their errors.
|
|
211
|
+
*
|
|
212
|
+
* It will check all loopback addresses from 127.0.0.2 to 127.0.0.33 (by default).
|
|
213
|
+
*
|
|
214
|
+
* Use the HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT environment variable to
|
|
215
|
+
* adjust the number of loopback addresses to validate (up to 254).
|
|
216
|
+
*/
|
|
217
|
+
export async function validateLoopbackAddressPool() {
|
|
218
|
+
return Promise.allSettled(Array.from({ length: HARPER_LOOPBACK_POOL_COUNT }, (_, i) => validateLoopbackAddress(`127.0.0.${i + HARPER_LOOPBACK_POOL_START}`))).then((results) => results.reduce((acc, result) => {
|
|
219
|
+
if (result.status === 'fulfilled') {
|
|
220
|
+
acc.successful.push(result.value);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const error = result.reason;
|
|
224
|
+
acc.failed.push({ loopbackAddress: error.loopbackAddress, error });
|
|
225
|
+
}
|
|
226
|
+
return acc;
|
|
227
|
+
}, { successful: [], failed: [] }));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Retrieves the next available loopback address from the pool using a file-based
|
|
231
|
+
* locking mechanism to safely allocate addresses across concurrent test processes.
|
|
232
|
+
*
|
|
233
|
+
* **How it works:**
|
|
234
|
+
* 1. Acquires a file-based lock to prevent race conditions with other processes
|
|
235
|
+
* 2. Reads the pool state (a JSON array of process IDs, with null for available slots)
|
|
236
|
+
* 3. Finds the first available (null) slot and assigns the current process PID to it
|
|
237
|
+
* 4. Writes the updated pool back to disk and releases the lock
|
|
238
|
+
* 5. Validates that the allocated address can actually be bound to
|
|
239
|
+
* 6. Returns the loopback address (e.g., "127.0.0.2")
|
|
240
|
+
*
|
|
241
|
+
* If no addresses are available, waits and retries until one becomes available.
|
|
242
|
+
*
|
|
243
|
+
* **Pool file location:** `${tmpdir()}/harper-integration-test-loopback-pool.json`
|
|
244
|
+
* **Lock file location:** `${tmpdir()}/harper-integration-test-loopback-pool.lock`
|
|
245
|
+
*
|
|
246
|
+
* @returns A promise that resolves with an allocated loopback address
|
|
247
|
+
* @throws {LoopbackAddressValidationError} If the allocated address cannot be bound to
|
|
248
|
+
*/
|
|
249
|
+
export async function getNextAvailableLoopbackAddress() {
|
|
250
|
+
// Each index maps to a different loopback address (index 0 -> 127.0.0.2, index 1 -> 127.0.0.3, etc.)
|
|
251
|
+
// So if the first test process number is 42, it would be assigned to index 0 associated with address 127.0.0.2
|
|
252
|
+
// [42, null, null, ...];
|
|
253
|
+
// Then the next process (call is 43) gets the next available, so index 1 -> 127.0.0.3
|
|
254
|
+
// [42, 43, null, ...];
|
|
255
|
+
// And so on...
|
|
256
|
+
// As processes exit and release their loopback addresses, those addresses become available for new processes to use
|
|
257
|
+
// [42, null, 44, ...];
|
|
258
|
+
// Next process (45) gets index 1 again ->
|
|
259
|
+
// [42, 45, 44, ...];
|
|
260
|
+
// This continues until all loopback addresses are used, at which point new processes will wait until an address becomes available
|
|
261
|
+
// Since multiple processes may be trying to get a loopback address at the same time, we need to implement a simple file-based locking mechanism to prevent race conditions
|
|
262
|
+
while (true) {
|
|
263
|
+
const assignedIndex = await withLock(async () => {
|
|
264
|
+
// Read the pool file
|
|
265
|
+
const loopbackPool = await readPoolFile();
|
|
266
|
+
// Find the first available index
|
|
267
|
+
const index = findAvailableIndex(loopbackPool);
|
|
268
|
+
if (index === null) {
|
|
269
|
+
// No available addresses - remove any dead processes from the pool and wait for one to become available
|
|
270
|
+
removeDeadProcessesFromPool(loopbackPool);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
// Assign the process PID to that index to mark it as used
|
|
274
|
+
loopbackPool[index] = process.pid;
|
|
275
|
+
}
|
|
276
|
+
// Write the updated pool back to the file
|
|
277
|
+
await writePoolFile(loopbackPool);
|
|
278
|
+
return index;
|
|
279
|
+
});
|
|
280
|
+
// If we got an index, validate and return the address
|
|
281
|
+
if (assignedIndex !== null) {
|
|
282
|
+
const loopbackAddress = `127.0.0.${assignedIndex + HARPER_LOOPBACK_POOL_START}`;
|
|
283
|
+
try {
|
|
284
|
+
await validateLoopbackAddress(loopbackAddress);
|
|
285
|
+
return loopbackAddress;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
// Validation failed - throw a proper error instead of breaking
|
|
289
|
+
throw new LoopbackAddressValidationError(loopbackAddress, error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// No available addresses; wait and retry
|
|
293
|
+
await sleep(RETRY_DELAY_MS);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Removes any dead processes from the loopback pool.
|
|
298
|
+
* @param loopbackPool
|
|
299
|
+
*/
|
|
300
|
+
function removeDeadProcessesFromPool(loopbackPool) {
|
|
301
|
+
loopbackPool.forEach((pid, index) => {
|
|
302
|
+
if (pid === null)
|
|
303
|
+
return;
|
|
304
|
+
try {
|
|
305
|
+
process.kill(pid, 0);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
loopbackPool[index] = null;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Releases a loopback address back to the pool, making it available for other processes.
|
|
314
|
+
*
|
|
315
|
+
* @param address The loopback address to release (e.g., "127.0.0.2")
|
|
316
|
+
* @throws InvalidLoopbackAddressError if the address format is invalid
|
|
317
|
+
*/
|
|
318
|
+
export async function releaseLoopbackAddress(address) {
|
|
319
|
+
// Validate and parse the address
|
|
320
|
+
const index = parseLoopbackAddress(address);
|
|
321
|
+
await withLock(async () => {
|
|
322
|
+
// Read the pool file
|
|
323
|
+
const loopbackPool = await readPoolFile();
|
|
324
|
+
// Release the address by setting it to null
|
|
325
|
+
loopbackPool[index] = null;
|
|
326
|
+
// Write the updated pool back to the file
|
|
327
|
+
await writePoolFile(loopbackPool);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Releases all loopback addresses assigned to the current process.
|
|
332
|
+
* Useful for cleanup during graceful shutdown.
|
|
333
|
+
*/
|
|
334
|
+
export async function releaseAllLoopbackAddressesForCurrentProcess() {
|
|
335
|
+
await withLock(async () => {
|
|
336
|
+
// Read the pool file
|
|
337
|
+
const loopbackPool = await readPoolFile();
|
|
338
|
+
// Find and release all addresses assigned to this process
|
|
339
|
+
for (let i = 0; i < loopbackPool.length; i++) {
|
|
340
|
+
if (loopbackPool[i] === process.pid) {
|
|
341
|
+
loopbackPool[i] = null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Write the updated pool back to the file
|
|
345
|
+
await writePoolFile(loopbackPool);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
//# sourceMappingURL=loopbackAddressPool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loopbackAddressPool.js","sourceRoot":"","sources":["../src/loopbackAddressPool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks whether a TCP port can be bound on a given host (i.e. the port is free).
|
|
3
|
+
*
|
|
4
|
+
* Attempts to bind a throwaway server to `host:port`; resolves `true` if the bind
|
|
5
|
+
* succeeds (the server is closed immediately) and `false` if it fails (e.g. the port
|
|
6
|
+
* is still held by another process or socket).
|
|
7
|
+
*
|
|
8
|
+
* @param host The host/address to bind on (e.g. "127.0.0.2")
|
|
9
|
+
* @param port The port to test
|
|
10
|
+
* @returns A promise resolving to `true` if the port is free, `false` otherwise
|
|
11
|
+
*/
|
|
12
|
+
export declare function isPortFree(host: string, port: number): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Polls until every `host:port` in `ports` is free, or `timeoutMs` elapses.
|
|
15
|
+
*
|
|
16
|
+
* Unlike binding to an ephemeral port (which only proves the *address* is usable), this
|
|
17
|
+
* verifies the specific fixed ports a Harper instance binds are actually released, which
|
|
18
|
+
* is what the next test suite needs before it can reuse the address.
|
|
19
|
+
*
|
|
20
|
+
* @param host The host/address the ports are bound on (e.g. "127.0.0.2")
|
|
21
|
+
* @param ports The fixed ports to wait on
|
|
22
|
+
* @param timeoutMs Maximum time to wait for all ports to become free
|
|
23
|
+
* @param pollIntervalMs Delay between polls (default 100ms)
|
|
24
|
+
* @returns `true` if all ports became free within the timeout, `false` if it gave up
|
|
25
|
+
*/
|
|
26
|
+
export declare function waitForPortsFree(host: string, ports: number[], timeoutMs: number, pollIntervalMs?: number): Promise<boolean>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
/**
|
|
4
|
+
* Checks whether a TCP port can be bound on a given host (i.e. the port is free).
|
|
5
|
+
*
|
|
6
|
+
* Attempts to bind a throwaway server to `host:port`; resolves `true` if the bind
|
|
7
|
+
* succeeds (the server is closed immediately) and `false` if it fails (e.g. the port
|
|
8
|
+
* is still held by another process or socket).
|
|
9
|
+
*
|
|
10
|
+
* @param host The host/address to bind on (e.g. "127.0.0.2")
|
|
11
|
+
* @param port The port to test
|
|
12
|
+
* @returns A promise resolving to `true` if the port is free, `false` otherwise
|
|
13
|
+
*/
|
|
14
|
+
export function isPortFree(host, port) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const server = createServer();
|
|
17
|
+
server.once('error', () => resolve(false));
|
|
18
|
+
server.listen(port, host, () => {
|
|
19
|
+
server.close(() => resolve(true));
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Polls until every `host:port` in `ports` is free, or `timeoutMs` elapses.
|
|
25
|
+
*
|
|
26
|
+
* Unlike binding to an ephemeral port (which only proves the *address* is usable), this
|
|
27
|
+
* verifies the specific fixed ports a Harper instance binds are actually released, which
|
|
28
|
+
* is what the next test suite needs before it can reuse the address.
|
|
29
|
+
*
|
|
30
|
+
* @param host The host/address the ports are bound on (e.g. "127.0.0.2")
|
|
31
|
+
* @param ports The fixed ports to wait on
|
|
32
|
+
* @param timeoutMs Maximum time to wait for all ports to become free
|
|
33
|
+
* @param pollIntervalMs Delay between polls (default 100ms)
|
|
34
|
+
* @returns `true` if all ports became free within the timeout, `false` if it gave up
|
|
35
|
+
*/
|
|
36
|
+
export async function waitForPortsFree(host, ports, timeoutMs, pollIntervalMs = 100) {
|
|
37
|
+
const deadline = Date.now() + timeoutMs;
|
|
38
|
+
while (true) {
|
|
39
|
+
const results = await Promise.all(ports.map((port) => isPortFree(host, port)));
|
|
40
|
+
if (results.every(Boolean))
|
|
41
|
+
return true;
|
|
42
|
+
if (Date.now() >= deadline)
|
|
43
|
+
return false;
|
|
44
|
+
await sleep(pollIntervalMs);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=portUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portUtils.js","sourceRoot":"","sources":["../src/portUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,IAAY;IACpD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9B,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3C,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE;YAC9B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,IAAY,EACZ,KAAe,EACf,SAAiB,EACjB,cAAc,GAAG,GAAG;IAEpB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IACxC,OAAO,IAAI,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAC/E,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QACxC,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ;YAAE,OAAO,KAAK,CAAC;QACzC,MAAM,KAAK,CAAC,cAAc,CAAC,CAAC;IAC7B,CAAC;AACF,CAAC"}
|
package/dist/run.d.ts
ADDED
package/dist/run.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { run } from 'node:test';
|
|
3
|
+
import { availableParallelism, tmpdir } from 'node:os';
|
|
4
|
+
import { spec } from 'node:test/reporters';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { validateLoopbackAddressPool } from "./loopbackAddressPool.js";
|
|
7
|
+
import { LOG_DIR_MARKER_PREFIX } from "./harperLifecycle.js";
|
|
8
|
+
import { mkdtemp } from 'node:fs/promises';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
import { readFileSync, rmSync, existsSync } from 'node:fs';
|
|
11
|
+
/**
|
|
12
|
+
* Important! This script should not be required to execute integration tests.
|
|
13
|
+
|
|
14
|
+
* Thus, it should not be responsible for any stateful management or setup/teardown logic.
|
|
15
|
+
* All such logic should be contained within the individual test suites or utility functions.
|
|
16
|
+
* Tests (individuals or multiples) should be executable directly via the Node.js Test Runner CLI and
|
|
17
|
+
* parallelization should still work.
|
|
18
|
+
*
|
|
19
|
+
* The main purpose of this script is to reduce the boilerplate required to run integration tests, or having
|
|
20
|
+
* developers manually specify CLI arguments each time.
|
|
21
|
+
*
|
|
22
|
+
* This script configures and runs the Node.js Test Runner with sensible defaults for Harper integration tests.
|
|
23
|
+
*
|
|
24
|
+
* It supports environment variables to override defaults, allowing flexibility for CI environments or specific use cases.
|
|
25
|
+
*
|
|
26
|
+
* Usage: harper-integration-test-run [options] <glob-pattern> [<glob-pattern> ...]
|
|
27
|
+
*
|
|
28
|
+
* At least one glob pattern positional argument is required.
|
|
29
|
+
*/
|
|
30
|
+
// Imitating the Node.js Test Runner CLI arguments for consistency except we drop the `test-` prefix
|
|
31
|
+
const { values, positionals } = parseArgs({
|
|
32
|
+
options: {
|
|
33
|
+
concurrency: { type: 'string' },
|
|
34
|
+
isolation: { type: 'string' },
|
|
35
|
+
shard: { type: 'string' },
|
|
36
|
+
only: { type: 'boolean' },
|
|
37
|
+
},
|
|
38
|
+
allowPositionals: true,
|
|
39
|
+
});
|
|
40
|
+
if (positionals.length === 0) {
|
|
41
|
+
console.error('Error: At least one glob pattern is required.\n' +
|
|
42
|
+
'Usage: harper-integration-test-run [options] <glob-pattern> [<glob-pattern> ...]\n' +
|
|
43
|
+
'Example: harper-integration-test-run "integrationTests/**/*.test.ts"');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
// https://nodejs.org/docs/latest-v24.x/api/cli.html#--test-concurrency
|
|
47
|
+
const concurrencyOption = process.env.HARPER_INTEGRATION_TEST_CONCURRENCY || values.concurrency;
|
|
48
|
+
const CONCURRENCY = concurrencyOption
|
|
49
|
+
? parseInt(concurrencyOption, 10)
|
|
50
|
+
: Math.max(1, Math.floor(availableParallelism() / 2) + 1);
|
|
51
|
+
// https://nodejs.org/docs/latest-v24.x/api/cli.html#--test-isolationmode
|
|
52
|
+
const ISOLATION = process.env.HARPER_INTEGRATION_TEST_ISOLATION || values.isolation || 'process';
|
|
53
|
+
// https://nodejs.org/docs/latest-v24.x/api/cli.html#--test-shard
|
|
54
|
+
const [SHARD_INDEX, SHARD_TOTAL] = (process.env.HARPER_INTEGRATION_TEST_SHARD || values.shard || '1/1')
|
|
55
|
+
.split('/')
|
|
56
|
+
.map((v) => parseInt(v, 10));
|
|
57
|
+
// https://nodejs.org/docs/latest-v24.x/api/cli.html#--test-only
|
|
58
|
+
const ONLY = parseBoolean(process.env.HARPER_INTEGRATION_TEST_ONLY) ?? values.only ?? false;
|
|
59
|
+
// Number of trailing hdb.log lines to print per failed test. 0 (or any non-positive value) prints the entire log.
|
|
60
|
+
const LOG_TAIL_LINES = (() => {
|
|
61
|
+
const parsed = parseInt(process.env.HARPER_INTEGRATION_TEST_LOG_TAIL_LINES || '', 10);
|
|
62
|
+
return Number.isFinite(parsed) ? parsed : 200;
|
|
63
|
+
})();
|
|
64
|
+
const TEST_FILES = positionals;
|
|
65
|
+
// Loopback Address Check
|
|
66
|
+
if (ISOLATION !== 'none' && CONCURRENCY > 1) {
|
|
67
|
+
const result = await validateLoopbackAddressPool();
|
|
68
|
+
if (result.failed.length > 0) {
|
|
69
|
+
console.error('Failed to bind loopback address pool required for integration tests:');
|
|
70
|
+
for (const failure of result.failed) {
|
|
71
|
+
console.error(`- ${failure.loopbackAddress}: ${failure.error.message}`);
|
|
72
|
+
}
|
|
73
|
+
console.error('Run the setup script to configure loopback addresses:\n' +
|
|
74
|
+
' harper-integration-test-setup-loopback\n' +
|
|
75
|
+
'Or run integration tests sequentially using `--isolation=none` to avoid this requirement.');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let createdTempLogDir;
|
|
80
|
+
if (!process.env.HARPER_INTEGRATION_TEST_LOG_DIR) {
|
|
81
|
+
createdTempLogDir = await mkdtemp(join(tmpdir(), 'harper-integration-test-logs-'));
|
|
82
|
+
process.env.HARPER_INTEGRATION_TEST_LOG_DIR = createdTempLogDir;
|
|
83
|
+
}
|
|
84
|
+
const fileToLogDirs = new Map();
|
|
85
|
+
const failedFiles = new Set();
|
|
86
|
+
const runner = run({
|
|
87
|
+
concurrency: ISOLATION === 'none' ? undefined : CONCURRENCY,
|
|
88
|
+
// @ts-expect-error - ignore until we do better env var / cli arg handling/validation
|
|
89
|
+
isolation: ISOLATION,
|
|
90
|
+
globPatterns: TEST_FILES,
|
|
91
|
+
only: ONLY,
|
|
92
|
+
shard: {
|
|
93
|
+
index: SHARD_INDEX,
|
|
94
|
+
total: SHARD_TOTAL,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
const logDirMarkerPattern = new RegExp(`${LOG_DIR_MARKER_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} (.*)`);
|
|
98
|
+
runner.on('test:stdout', (data) => {
|
|
99
|
+
const match = typeof data.message === 'string' && data.message.match(logDirMarkerPattern);
|
|
100
|
+
if (match && data.file) {
|
|
101
|
+
const logDir = match[1].trim();
|
|
102
|
+
const normalizedFile = resolve(data.file);
|
|
103
|
+
let dirs = fileToLogDirs.get(normalizedFile);
|
|
104
|
+
if (!dirs) {
|
|
105
|
+
dirs = new Set();
|
|
106
|
+
fileToLogDirs.set(normalizedFile, dirs);
|
|
107
|
+
}
|
|
108
|
+
dirs.add(logDir);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
runner.on('test:fail', (data) => {
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
if (data.file) {
|
|
114
|
+
failedFiles.add(resolve(data.file));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
runner.compose(spec).pipe(process.stdout);
|
|
118
|
+
process.on('exit', () => {
|
|
119
|
+
if (failedFiles.size > 0) {
|
|
120
|
+
console.log('\n\n' + '='.repeat(80));
|
|
121
|
+
console.log('--- TEST FAILURES DETECTED: HARPER LOGS ---');
|
|
122
|
+
console.log('='.repeat(80));
|
|
123
|
+
for (const file of failedFiles) {
|
|
124
|
+
const dirs = fileToLogDirs.get(file);
|
|
125
|
+
if (dirs && dirs.size > 0) {
|
|
126
|
+
for (const dir of dirs) {
|
|
127
|
+
const hdbLogPath = join(dir, 'hdb.log');
|
|
128
|
+
if (existsSync(hdbLogPath)) {
|
|
129
|
+
try {
|
|
130
|
+
const content = readFileSync(hdbLogPath, 'utf8');
|
|
131
|
+
let output;
|
|
132
|
+
let tailNote = '';
|
|
133
|
+
if (LOG_TAIL_LINES > 0) {
|
|
134
|
+
const lines = content.split('\n');
|
|
135
|
+
if (lines.length > LOG_TAIL_LINES) {
|
|
136
|
+
output = lines.slice(-LOG_TAIL_LINES).join('\n');
|
|
137
|
+
tailNote = ` (last ${LOG_TAIL_LINES} of ${lines.length} lines; set HARPER_INTEGRATION_TEST_LOG_TAIL_LINES=0 for full log)`;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
output = content;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
output = content;
|
|
145
|
+
}
|
|
146
|
+
console.log(`\n--- Log for instance in ${file}${tailNote} ---`);
|
|
147
|
+
console.log(`Directory: ${dir}`);
|
|
148
|
+
console.log('-'.repeat(80));
|
|
149
|
+
console.log(output);
|
|
150
|
+
console.log('-'.repeat(80));
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
console.error(`Failed to read log file ${hdbLogPath}:`, e);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (createdTempLogDir) {
|
|
161
|
+
try {
|
|
162
|
+
rmSync(createdTempLogDir, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
console.error(`Failed to clean up temporary log directory ${createdTempLogDir}:`, e);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
function parseBoolean(value) {
|
|
170
|
+
if (value === undefined)
|
|
171
|
+
return undefined;
|
|
172
|
+
if (value.toLowerCase() === 'true' || value === '1')
|
|
173
|
+
return true;
|
|
174
|
+
if (value.toLowerCase() === 'false' || value === '0')
|
|
175
|
+
return false;
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=run.js.map
|