Skip to content

Commit 4b71590

Browse files
authored
test(api): use random ports for getFreePort e2e tests (#179)
1 parent 3c68518 commit 4b71590

2 files changed

Lines changed: 170 additions & 17 deletions

File tree

tests/e2e/api.test.js

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
const path = require('node:path');
2+
const net = require('node:net');
23
const { rspack } = require('@rspack/core');
34
const { RspackDevServer: Server } = require('@rspack/dev-server');
45
const config = require('../fixtures/client-config/rspack.config');
56
const runBrowser = require('../helpers/run-browser');
67
const sessionSubscribe = require('../helpers/session-subscribe');
8+
const {
9+
getRandomPorts,
10+
releaseRandomPorts,
11+
} = require('../helpers/get-random-port');
712
const port = require('../helpers/ports-map').api;
813

914
describe('API', () => {
@@ -337,12 +342,19 @@ describe('API', () => {
337342
});
338343

339344
describe('Server.getFreePort', () => {
345+
let basePort;
346+
let reservedPorts = [];
340347
let dummyServers = [];
341348
let devServerPort;
342349

350+
beforeEach(async () => {
351+
reservedPorts = await getRandomPorts(8, '0.0.0.0');
352+
[basePort] = reservedPorts;
353+
});
354+
343355
afterEach(() => {
344-
process.env.RSPACK_DEV_SERVER_BASE_PORT = undefined;
345-
process.env.RSPACK_DEV_SERVER_PORT_RETRY = undefined;
356+
delete process.env.RSPACK_DEV_SERVER_BASE_PORT;
357+
delete process.env.RSPACK_DEV_SERVER_PORT_RETRY;
346358

347359
return dummyServers
348360
.reduce(
@@ -359,18 +371,20 @@ describe('API', () => {
359371
)
360372
.then(() => {
361373
dummyServers = [];
374+
releaseRandomPorts(reservedPorts);
375+
reservedPorts = [];
362376
});
363377
});
364378

365379
function createDummyServers(n) {
366-
process.env.RSPACK_DEV_SERVER_BASE_PORT = 60000;
380+
process.env.RSPACK_DEV_SERVER_BASE_PORT = `${basePort}`;
367381

368382
return (Array.isArray(n) ? n : [...new Array(n)]).reduce(
369383
(p, _, i) =>
370384
p.then(
371385
() =>
372386
new Promise((resolve) => {
373-
devServerPort = 60000 + i;
387+
devServerPort = basePort + i;
374388
const compiler = rspack(config);
375389
const server = new Server(
376390
{ port: devServerPort, host: '0.0.0.0' },
@@ -407,7 +421,7 @@ describe('API', () => {
407421

408422
const freePort = await Server.getFreePort(null);
409423

410-
expect(freePort).toEqual(60000 + retryCount);
424+
expect(freePort).toEqual(basePort + retryCount);
411425

412426
const { page, browser } = await runBrowser();
413427

@@ -447,7 +461,7 @@ describe('API', () => {
447461
// eslint-disable-next-line no-undefined
448462
const freePort = await Server.getFreePort(undefined);
449463

450-
expect(freePort).toEqual(60000 + retryCount);
464+
expect(freePort).toEqual(basePort + retryCount);
451465

452466
const { page, browser } = await runBrowser();
453467

@@ -486,7 +500,7 @@ describe('API', () => {
486500

487501
const freePort = await Server.getFreePort();
488502

489-
expect(freePort).toEqual(60000 + retryCount);
503+
expect(freePort).toEqual(basePort + retryCount);
490504

491505
const { page, browser } = await runBrowser();
492506

@@ -525,7 +539,7 @@ describe('API', () => {
525539

526540
const freePort = await Server.getFreePort();
527541

528-
expect(freePort).toEqual(60000 + retryCount);
542+
expect(freePort).toEqual(basePort + retryCount);
529543

530544
const { page, browser } = await runBrowser();
531545

@@ -556,15 +570,15 @@ describe('API', () => {
556570
});
557571

558572
it('should retry finding the port when serial ports are busy', async () => {
559-
const busyPorts = [60000, 60001, 60002, 60003, 60004, 60005];
573+
const busyPorts = reservedPorts.slice(0, 6);
560574

561575
process.env.RSPACK_DEV_SERVER_PORT_RETRY = 1000;
562576

563577
await createDummyServers(busyPorts);
564578

565579
const freePort = await Server.getFreePort();
566580

567-
expect(freePort).toBeGreaterThan(60005);
581+
expect(freePort).toBeGreaterThan(busyPorts[busyPorts.length - 1]);
568582

569583
const { page, browser } = await runBrowser();
570584

@@ -597,16 +611,50 @@ describe('API', () => {
597611
});
598612

599613
it("should throw the error when the port isn't found", async () => {
600-
rs.doMockRequire(
601-
'../../dist/getPort',
602-
() => () => Promise.reject(new Error('busy')),
603-
);
614+
const busyServers = [];
615+
const busyPorts = [65534, 65535];
604616

605-
process.env.RSPACK_DEV_SERVER_PORT_RETRY = 1;
617+
try {
618+
await Promise.all(
619+
busyPorts.map(
620+
(busyPort) =>
621+
new Promise((resolve, reject) => {
622+
const server = net.createServer();
623+
624+
server.unref();
625+
server.on('error', reject);
626+
server.listen(busyPort, '0.0.0.0', () => {
627+
busyServers.push(server);
628+
resolve();
629+
});
630+
}),
631+
),
632+
);
606633

607-
const { RspackDevServer: Server } = require('@rspack/dev-server');
634+
process.env.RSPACK_DEV_SERVER_BASE_PORT = '65534';
635+
process.env.RSPACK_DEV_SERVER_PORT_RETRY = 0;
608636

609-
await expect(Server.getFreePort()).rejects.toThrowErrorMatchingSnapshot();
637+
await expect(
638+
Server.getFreePort(),
639+
).rejects.toThrowErrorMatchingSnapshot();
640+
} finally {
641+
await Promise.all(
642+
busyServers.map(
643+
(server) =>
644+
new Promise((resolve, reject) => {
645+
server.close((error) => {
646+
if (error) {
647+
reject(error);
648+
649+
return;
650+
}
651+
652+
resolve();
653+
});
654+
}),
655+
),
656+
);
657+
}
610658
});
611659
});
612660

tests/helpers/get-random-port.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
const net = require('node:net');
2+
3+
const minRandomPort = 15000;
4+
const maxRandomPort = 45000;
5+
6+
async function isPortAvailable(port, host = '0.0.0.0') {
7+
try {
8+
const server = net.createServer();
9+
10+
server.unref();
11+
12+
return await new Promise((resolve) => {
13+
server.on('listening', () => {
14+
server.close(() => {
15+
resolve(true);
16+
});
17+
});
18+
server.on('error', () => {
19+
resolve(false);
20+
});
21+
server.listen(port, host);
22+
});
23+
} catch {
24+
return false;
25+
}
26+
}
27+
28+
const portMap = new Map();
29+
30+
function getDefaultPort() {
31+
return Math.ceil(Math.random() * 30000) + minRandomPort;
32+
}
33+
34+
async function findAvailablePort(startPort, count, host) {
35+
let port = Math.max(startPort, minRandomPort);
36+
const maxStartPort = maxRandomPort - count + 1;
37+
38+
while (port <= maxStartPort) {
39+
let isAvailable = true;
40+
41+
for (let index = 0; index < count; index += 1) {
42+
const currentPort = port + index;
43+
44+
if (
45+
portMap.get(currentPort) ||
46+
!(await isPortAvailable(currentPort, host))
47+
) {
48+
isAvailable = false;
49+
break;
50+
}
51+
}
52+
53+
if (isAvailable) {
54+
return port;
55+
}
56+
57+
port += 1;
58+
}
59+
60+
throw new Error('No available ports found');
61+
}
62+
63+
/**
64+
* Get a random port.
65+
* Available port ranges: 1024 ~ 65535
66+
* `10080` is not available on macOS CI, `> 50000` gets "permission denied" on Windows,
67+
* so we use `15000` ~ `45000`.
68+
*/
69+
async function getRandomPort(defaultPort = getDefaultPort(), host) {
70+
const port = await findAvailablePort(defaultPort, 1, host);
71+
72+
portMap.set(port, 1);
73+
74+
return port;
75+
}
76+
77+
async function getRandomPorts(count, host) {
78+
if (count < 1) {
79+
throw new Error('Port count must be greater than 0');
80+
}
81+
82+
const port = await findAvailablePort(getDefaultPort(), count, host);
83+
const ports = [];
84+
85+
for (let index = 0; index < count; index += 1) {
86+
const currentPort = port + index;
87+
88+
portMap.set(currentPort, 1);
89+
ports.push(currentPort);
90+
}
91+
92+
return ports;
93+
}
94+
95+
function releaseRandomPorts(ports = []) {
96+
for (const port of ports) {
97+
portMap.delete(port);
98+
}
99+
}
100+
101+
module.exports = {
102+
getRandomPort,
103+
getRandomPorts,
104+
releaseRandomPorts,
105+
};

0 commit comments

Comments
 (0)