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
7 changes: 7 additions & 0 deletions .changelog/20260331183135_master.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: Feature
scope:
- ckeditor5-dev-tests
---

The manual test server now automatically finds a free port at startup. When the preferred port (default 8125) is already in use, the server tries subsequent ports until an available one is found. The `--port` option can be used to set the starting port.
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,44 @@ import combine from 'dom-combiner';
import { logger } from '@ckeditor/ckeditor5-dev-utils';

/**
* Basic HTTP server.
* Basic HTTP server with automatic free port detection.
*
* @param {string} sourcePath Base path where the compiler saved the files.
* @param {number} [port=8125] Port to listen at.
* @param {number} [port=8125] Preferred port to listen at. If the port is already in use,
* the server will automatically try subsequent ports until a free one is found.
* @param {function} [onCreate] A callback called with the reference to the HTTP server when it is up and running.
*/
export default function createManualTestServer( sourcePath, port = 8125, onCreate ) {
return new Promise( resolve => {
const server = http.createServer( ( request, response ) => {
onRequest( sourcePath, request, response );
} ).listen( port );
return new Promise( ( resolve, reject ) => {
tryListenOnPort( sourcePath, port, onCreate, resolve, reject );
} );
}

function tryListenOnPort( sourcePath, port, onCreate, resolve, reject ) {
const log = logger();

const server = http.createServer( ( request, response ) => {
onRequest( sourcePath, request, response );
} );

server.once( 'error', error => {
server.close();

if ( error.code === 'EADDRINUSE' ) {
if ( port >= 65535 ) {
reject( new Error( 'Could not find a free port. All ports from the starting port to 65535 are in use.' ) );

return;
}

log.info( `[Server] Port ${ port } is in use, trying ${ port + 1 }...` );
tryListenOnPort( sourcePath, port + 1, onCreate, resolve, reject );
} else {
reject( error );
}
} );

server.listen( port, () => {
// SIGINT isn't caught on Windows in process. However, `CTRL+C` can be caught
// by `readline` module. After that we can emit SIGINT to the process manually.
if ( process.platform === 'win32' ) {
Expand All @@ -46,7 +72,7 @@ export default function createManualTestServer( sourcePath, port = 8125, onCreat
process.exit();
} );

logger().info( `[Server] Server running at http://localhost:${ port }/` );
log.info( `[Server] Server running at http://localhost:${ port }/` );

if ( onCreate ) {
onCreate( server );
Expand Down
103 changes: 86 additions & 17 deletions packages/ckeditor5-dev-tests/tests/utils/manual-tests/createserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ vi.mock( 'glob' );
vi.mock( 'dom-combiner' );

describe( 'createManualTestServer()', () => {
let loggerStub, server;
let loggerStub, server, servers;

beforeEach( async () => {
const { createServer } = http;

servers = [];
loggerStub = vi.fn();

vi.mocked( logger ).mockReturnValue( {
Expand All @@ -32,6 +33,7 @@ describe( 'createManualTestServer()', () => {

vi.spyOn( http, 'createServer' ).mockImplementation( ( ...theArgs ) => {
server = createServer( ...theArgs );
servers.push( server );

vi.spyOn( server, 'listen' );

Expand All @@ -40,62 +42,129 @@ describe( 'createManualTestServer()', () => {
} );

afterEach( () => {
server.close();
for ( const s of servers ) {
s.close();
}
} );

it( 'should start http server', () => {
createManualTestServer( 'workspace/build/.manual-tests' );
it( 'should start http server', async () => {
createManualTestServer( 'workspace/build/.manual-tests', 49700 );

await vi.waitFor( () => {
expect( loggerStub ).toHaveBeenCalled();
} );

expect( vi.mocked( http ).createServer ).toHaveBeenCalledOnce();
} );

it( 'should listen on given port', () => {
createManualTestServer( 'workspace/build/.manual-tests', 8888 );
it( 'should listen on given port', async () => {
createManualTestServer( 'workspace/build/.manual-tests', 49888 );

await vi.waitFor( () => {
expect( loggerStub ).toHaveBeenCalled();
} );

expect( server ).toEqual( expect.objectContaining( {
listen: expect.any( Function )
} ) );

expect( server.listen ).toHaveBeenCalledExactlyOnceWith( 8888 );
expect( loggerStub ).toHaveBeenCalledExactlyOnceWith( '[Server] Server running at http://localhost:8888/' );
expect( server.listen ).toHaveBeenCalledExactlyOnceWith( 49888, expect.any( Function ) );
expect( loggerStub ).toHaveBeenCalledExactlyOnceWith( '[Server] Server running at http://localhost:49888/' );
} );

it( 'should listen on 8125 port if no specific port was given', () => {
it( 'should listen on 8125 port if no specific port was given', async () => {
createManualTestServer( 'workspace/build/.manual-tests' );

await vi.waitFor( () => {
expect( loggerStub ).toHaveBeenCalledWith(
expect.stringContaining( '[Server] Server running at http://localhost:' )
);
} );

expect( server ).toEqual( expect.objectContaining( {
listen: expect.any( Function )
} ) );

expect( server.listen ).toHaveBeenCalledExactlyOnceWith( 8125 );
expect( loggerStub ).toHaveBeenCalledExactlyOnceWith( '[Server] Server running at http://localhost:8125/' );
// The first listen attempt should always be on the default port 8125, even if the server
// ended up on a different port due to EADDRINUSE retries.
expect( servers[ 0 ].listen ).toHaveBeenCalledWith( 8125, expect.any( Function ) );
} );

it( 'should call the specified callback when the server is running (e.g. to allow running web sockets)', () => {
it( 'should call the specified callback when the server is running (e.g. to allow running web sockets)', async () => {
const spy = vi.fn();

createManualTestServer( 'workspace/build/.manual-tests', 1234, spy );
createManualTestServer( 'workspace/build/.manual-tests', 49234, spy );

await vi.waitFor( () => {
expect( spy ).toHaveBeenCalled();
} );

expect( spy ).toHaveBeenCalledExactlyOnceWith( server );
} );

it( 'should use "readline" to listen to the SIGINT event on Windows', () => {
it( 'should use "readline" to listen to the SIGINT event on Windows', async () => {
const readlineInterface = {
on: vi.fn()
};

vi.mocked( readline ).createInterface.mockReturnValue( readlineInterface );
vi.spyOn( process, 'platform', 'get' ).mockReturnValue( 'win32' );

createManualTestServer( 'workspace/build/.manual-tests' );
createManualTestServer( 'workspace/build/.manual-tests', 49900 );

await vi.waitFor( () => {
expect( vi.mocked( readline ).createInterface ).toHaveBeenCalled();
} );

expect( vi.mocked( readline ).createInterface ).toHaveBeenCalledOnce();
expect( readlineInterface.on ).toHaveBeenCalledExactlyOnceWith( 'SIGINT', expect.any( Function ) );
} );

it( 'should try next port when the requested port is in use', async () => {
// Occupy the port first.
const blockingServer = http.createServer.getMockImplementation()();

await new Promise( resolve => {
blockingServer.listen( 49555, resolve );
} );

createManualTestServer( 'workspace/build/.manual-tests', 49555 );

await vi.waitFor( () => {
expect( loggerStub ).toHaveBeenCalledWith( '[Server] Server running at http://localhost:49556/' );
} );

expect( loggerStub ).toHaveBeenCalledWith( '[Server] Port 49555 is in use, trying 49556...' );

blockingServer.close();
} );

it( 'should reject when a non-EADDRINUSE error occurs', async () => {
const originalCreateServer = http.createServer.getMockImplementation();
const fakeServer = originalCreateServer();

servers.push( fakeServer );
vi.spyOn( fakeServer, 'listen' ).mockImplementation( () => {
// Simulate a non-EADDRINUSE error.
process.nextTick( () => {
fakeServer.emit( 'error', new Error( 'EACCES: permission denied' ) );
} );
} );
vi.spyOn( fakeServer, 'close' ).mockImplementation( () => {} );

vi.mocked( http.createServer ).mockReturnValue( fakeServer );

await expect( createManualTestServer( 'workspace/build/.manual-tests', 49700 ) )
.rejects.toThrow( 'EACCES: permission denied' );
} );

describe( 'request handler', () => {
beforeEach( () => {
createManualTestServer( 'workspace/build/.manual-tests' );
beforeEach( async () => {
createManualTestServer( 'workspace/build/.manual-tests', 49800 );

await vi.waitFor( () => {
expect( loggerStub ).toHaveBeenCalled();
} );
} );

it( 'should handle a request for a favicon (`/favicon.ico`)', () => {
Expand Down