Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 65 additions & 17 deletions tests/e2e/api.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
const path = require('node:path');
const net = require('node:net');
const { rspack } = require('@rspack/core');
const { RspackDevServer: Server } = require('@rspack/dev-server');
const config = require('../fixtures/client-config/rspack.config');
const runBrowser = require('../helpers/run-browser');
const sessionSubscribe = require('../helpers/session-subscribe');
const {
getRandomPorts,
releaseRandomPorts,
} = require('../helpers/get-random-port');
const port = require('../helpers/ports-map').api;

describe('API', () => {
Expand Down Expand Up @@ -337,12 +342,19 @@ describe('API', () => {
});

describe('Server.getFreePort', () => {
let basePort;
let reservedPorts = [];
let dummyServers = [];
let devServerPort;

beforeEach(async () => {
reservedPorts = await getRandomPorts(8, '0.0.0.0');
[basePort] = reservedPorts;
});

afterEach(() => {
process.env.RSPACK_DEV_SERVER_BASE_PORT = undefined;
process.env.RSPACK_DEV_SERVER_PORT_RETRY = undefined;
delete process.env.RSPACK_DEV_SERVER_BASE_PORT;
delete process.env.RSPACK_DEV_SERVER_PORT_RETRY;

return dummyServers
.reduce(
Expand All @@ -359,18 +371,20 @@ describe('API', () => {
)
.then(() => {
dummyServers = [];
releaseRandomPorts(reservedPorts);
reservedPorts = [];
});
});

function createDummyServers(n) {
process.env.RSPACK_DEV_SERVER_BASE_PORT = 60000;
process.env.RSPACK_DEV_SERVER_BASE_PORT = `${basePort}`;

return (Array.isArray(n) ? n : [...new Array(n)]).reduce(
(p, _, i) =>
p.then(
() =>
new Promise((resolve) => {
devServerPort = 60000 + i;
devServerPort = basePort + i;
const compiler = rspack(config);
const server = new Server(
{ port: devServerPort, host: '0.0.0.0' },
Expand Down Expand Up @@ -407,7 +421,7 @@ describe('API', () => {

const freePort = await Server.getFreePort(null);

expect(freePort).toEqual(60000 + retryCount);
expect(freePort).toEqual(basePort + retryCount);

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

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

expect(freePort).toEqual(60000 + retryCount);
expect(freePort).toEqual(basePort + retryCount);

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

Expand Down Expand Up @@ -486,7 +500,7 @@ describe('API', () => {

const freePort = await Server.getFreePort();

expect(freePort).toEqual(60000 + retryCount);
expect(freePort).toEqual(basePort + retryCount);

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

Expand Down Expand Up @@ -525,7 +539,7 @@ describe('API', () => {

const freePort = await Server.getFreePort();

expect(freePort).toEqual(60000 + retryCount);
expect(freePort).toEqual(basePort + retryCount);

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

Expand Down Expand Up @@ -556,15 +570,15 @@ describe('API', () => {
});

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

process.env.RSPACK_DEV_SERVER_PORT_RETRY = 1000;

await createDummyServers(busyPorts);

const freePort = await Server.getFreePort();

expect(freePort).toBeGreaterThan(60005);
expect(freePort).toBeGreaterThan(busyPorts[busyPorts.length - 1]);

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

Expand Down Expand Up @@ -597,16 +611,50 @@ describe('API', () => {
});

it("should throw the error when the port isn't found", async () => {
rs.doMockRequire(
'../../dist/getPort',
() => () => Promise.reject(new Error('busy')),
);
const busyServers = [];
const busyPorts = [65534, 65535];

process.env.RSPACK_DEV_SERVER_PORT_RETRY = 1;
try {
await Promise.all(
busyPorts.map(
(busyPort) =>
new Promise((resolve, reject) => {
const server = net.createServer();

server.unref();
server.on('error', reject);
server.listen(busyPort, '0.0.0.0', () => {
busyServers.push(server);
resolve();
});
}),
),
);

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

await expect(Server.getFreePort()).rejects.toThrowErrorMatchingSnapshot();
await expect(
Server.getFreePort(),
).rejects.toThrowErrorMatchingSnapshot();
} finally {
await Promise.all(
busyServers.map(
(server) =>
new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);

return;
}

resolve();
});
}),
),
);
}
});
});

Expand Down
105 changes: 105 additions & 0 deletions tests/helpers/get-random-port.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
const net = require('node:net');

const minRandomPort = 15000;
const maxRandomPort = 45000;

async function isPortAvailable(port, host = '0.0.0.0') {
try {
const server = net.createServer();

server.unref();

return await new Promise((resolve) => {
server.on('listening', () => {
server.close(() => {
resolve(true);
});
});
server.on('error', () => {
resolve(false);
});
server.listen(port, host);
});
} catch {
return false;
}
}

const portMap = new Map();

function getDefaultPort() {
return Math.ceil(Math.random() * 30000) + minRandomPort;
}

async function findAvailablePort(startPort, count, host) {
let port = Math.max(startPort, minRandomPort);
const maxStartPort = maxRandomPort - count + 1;

while (port <= maxStartPort) {
let isAvailable = true;

for (let index = 0; index < count; index += 1) {
const currentPort = port + index;

if (
portMap.get(currentPort) ||
!(await isPortAvailable(currentPort, host))
) {
isAvailable = false;
break;
}
}

if (isAvailable) {
return port;
}

port += 1;
}

throw new Error('No available ports found');
}

/**
* Get a random port.
* Available port ranges: 1024 ~ 65535
* `10080` is not available on macOS CI, `> 50000` gets "permission denied" on Windows,
* so we use `15000` ~ `45000`.
*/
async function getRandomPort(defaultPort = getDefaultPort(), host) {
const port = await findAvailablePort(defaultPort, 1, host);

portMap.set(port, 1);

return port;
}

async function getRandomPorts(count, host) {
if (count < 1) {
throw new Error('Port count must be greater than 0');
}

const port = await findAvailablePort(getDefaultPort(), count, host);
const ports = [];

for (let index = 0; index < count; index += 1) {
const currentPort = port + index;

portMap.set(currentPort, 1);
ports.push(currentPort);
}

return ports;
}

function releaseRandomPorts(ports = []) {
for (const port of ports) {
portMap.delete(port);
}
}

module.exports = {
getRandomPort,
getRandomPorts,
releaseRandomPorts,
};