diff --git a/tests/e2e/api.test.js b/tests/e2e/api.test.js index 92f6abe..84a58e2 100644 --- a/tests/e2e/api.test.js +++ b/tests/e2e/api.test.js @@ -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', () => { @@ -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( @@ -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' }, @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -556,7 +570,7 @@ 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; @@ -564,7 +578,7 @@ describe('API', () => { const freePort = await Server.getFreePort(); - expect(freePort).toBeGreaterThan(60005); + expect(freePort).toBeGreaterThan(busyPorts[busyPorts.length - 1]); const { page, browser } = await runBrowser(); @@ -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(); + }); + }), + ), + ); + } }); }); diff --git a/tests/helpers/get-random-port.js b/tests/helpers/get-random-port.js new file mode 100644 index 0000000..bb15cf2 --- /dev/null +++ b/tests/helpers/get-random-port.js @@ -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, +};