From 8b2e1a988565d308120e396fb9bc20d9424a5e11 Mon Sep 17 00:00:00 2001 From: Otavio Jacobi Date: Sun, 27 Oct 2024 09:08:36 -0300 Subject: [PATCH] Add ability to run tests with cluster mode Change-type: patch --- test/00-basic.test.ts | 3 +- test/01-constrain.test.ts | 3 +- test/04-translations.test.ts | 3 +- test/05-request-cancellation.test.ts | 3 +- test/06-webresource.test.ts | 3 +- test/07-permissions.test.ts | 3 +- test/08-tasks.test.ts | 3 +- test/lib/pine-init.ts | 2 + test/lib/test-init.ts | 90 ++++++++++++++++++++++++---- 9 files changed, 93 insertions(+), 20 deletions(-) diff --git a/test/00-basic.test.ts b/test/00-basic.test.ts index 09b6ebe6e..e0b664363 100644 --- a/test/00-basic.test.ts +++ b/test/00-basic.test.ts @@ -2,9 +2,10 @@ import supertest from 'supertest'; import { expect } from 'chai'; const configPath = __dirname + '/fixtures/00-basic/config.js'; import { testInit, testDeInit, testLocalServer } from './lib/test-init'; +import type { ChildProcess } from 'child_process'; describe('00 basic tests', function () { - let pineServer: Awaited>; + let pineServer: ChildProcess; before(async () => { pineServer = await testInit({ configPath }); }); diff --git a/test/01-constrain.test.ts b/test/01-constrain.test.ts index 9e432fdeb..692a36512 100644 --- a/test/01-constrain.test.ts +++ b/test/01-constrain.test.ts @@ -2,9 +2,10 @@ import supertest from 'supertest'; import { expect } from 'chai'; const configPath = __dirname + '/fixtures/01-constrain/config.js'; import { testInit, testDeInit, testLocalServer } from './lib/test-init'; +import type { ChildProcess } from 'child_process'; describe('01 basic constrain tests', function () { - let pineServer: Awaited>; + let pineServer: ChildProcess; before(async () => { pineServer = await testInit({ configPath, deleteDb: true }); }); diff --git a/test/04-translations.test.ts b/test/04-translations.test.ts index 4bdfc3fdd..5be75ed94 100644 --- a/test/04-translations.test.ts +++ b/test/04-translations.test.ts @@ -7,9 +7,10 @@ import type { AnyObject } from 'pinejs-client-core'; import { PineTest } from 'pinejs-client-supertest'; import { assertExists } from './lib/common'; +import type { ChildProcess } from 'child_process'; describe('04 native translation tests', function () { - let pineServer: Awaited>; + let pineServer: ChildProcess; let pineTest: PineTest; let faculty: AnyObject; before(async () => { diff --git a/test/05-request-cancellation.test.ts b/test/05-request-cancellation.test.ts index 1de34fe8e..f6c69955b 100644 --- a/test/05-request-cancellation.test.ts +++ b/test/05-request-cancellation.test.ts @@ -6,6 +6,7 @@ import { testInit, testDeInit, testLocalServer } from './lib/test-init'; import { PineTest } from 'pinejs-client-supertest'; import request from 'request'; import { setTimeout } from 'timers/promises'; +import type { ChildProcess } from 'child_process'; const requestAsync = ( opts: @@ -43,7 +44,7 @@ async function expectLogs(pineTest: PineTest, expectedLogs: string[]) { describe('05 request cancellation tests', function () { let pineTest: PineTest; - let pineServer: Awaited>; + let pineServer: ChildProcess; before(async () => { pineServer = await testInit({ configPath, hooksPath, routesPath }); diff --git a/test/06-webresource.test.ts b/test/06-webresource.test.ts index 3be3cb5f5..98d5c4326 100644 --- a/test/06-webresource.test.ts +++ b/test/06-webresource.test.ts @@ -19,12 +19,13 @@ import { } from '@aws-sdk/client-s3'; import { intVar, requiredVar } from '@balena/env-parsing'; import { assertExists } from './lib/common'; +import type { ChildProcess } from 'child_process'; const pipeline = util.promisify(pipelineRaw); const fs = fsBase.promises; describe('06 webresources tests', function () { - let pineServer: Awaited>; + let pineServer: ChildProcess; const filePath = `${testResourcePath}/avatar-profile.png`; const newFilePath = `${testResourcePath}/other-image.png`; diff --git a/test/07-permissions.test.ts b/test/07-permissions.test.ts index 76dadfc48..080fae445 100644 --- a/test/07-permissions.test.ts +++ b/test/07-permissions.test.ts @@ -4,9 +4,10 @@ const configPath = __dirname + '/fixtures/07-permissions/config.js'; import { testInit, testDeInit, testLocalServer } from './lib/test-init'; import { sbvrUtils, permissions } from '../src/server-glue/module'; import type UserModel from '../src/sbvr-api/user'; +import type { ChildProcess } from 'child_process'; describe('07 permissions tests', function () { - let pineServer: Awaited>; + let pineServer: ChildProcess; let userPineClient: sbvrUtils.PinejsClient; before(async () => { pineServer = await testInit({ diff --git a/test/08-tasks.test.ts b/test/08-tasks.test.ts index a531c01d6..161b13132 100644 --- a/test/08-tasks.test.ts +++ b/test/08-tasks.test.ts @@ -8,6 +8,7 @@ import { tasks as tasksEnv } from '../src/config-loader/env'; import type Model from '../src/tasks/tasks'; import * as cronParser from 'cron-parser'; import { PINE_TEST_SIGNALS } from './lib/common'; +import type { ChildProcess } from 'node:child_process'; const actorId = 1; const fixturesBasePath = __dirname + '/fixtures/08-tasks/'; @@ -63,7 +64,7 @@ async function expectTask( } describe('08 task tests', function () { - let pineServer: Awaited>; + let pineServer: ChildProcess; let pineTest: PineTest; let apikey: string; before(async () => { diff --git a/test/lib/pine-init.ts b/test/lib/pine-init.ts index 8bf4e8e27..e70ef7d14 100644 --- a/test/lib/pine-init.ts +++ b/test/lib/pine-init.ts @@ -11,6 +11,8 @@ export type PineTestOptions = { withLoginRoute?: boolean; deleteDb: boolean; listenPort: number; + clusterMode?: boolean; + clusterNodes?: number; }; export async function init( diff --git a/test/lib/test-init.ts b/test/lib/test-init.ts index 0f7a0ca9b..fb70d39eb 100644 --- a/test/lib/test-init.ts +++ b/test/lib/test-init.ts @@ -5,20 +5,24 @@ import type { PineTestOptions } from './pine-init'; import type { OptionalField } from '../../src/sbvr-api/common-types'; export const listenPortDefault = 1337; export const testLocalServer = `http://localhost:${listenPortDefault}`; +import net, { type AddressInfo } from 'node:net'; -export async function testInit( - options: OptionalField, -): Promise { +async function getAvailableTCPPort(): Promise { + return new Promise((res) => { + const srv = net.createServer(); + srv.listen(0, () => { + const port = (srv.address() as AddressInfo).port; + srv.close(() => { + res(port); + }); + }); + }); +} + +const forkServer = async ( + processArgs: PineTestOptions, +): Promise => { try { - const processArgs: PineTestOptions = { - listenPort: options.listenPort ?? listenPortDefault, - deleteDb: options.deleteDb ?? boolVar('DELETE_DB', false), - configPath: options.configPath, - hooksPath: options.hooksPath, - taskHandlersPath: options.taskHandlersPath, - routesPath: options.routesPath, - withLoginRoute: options.withLoginRoute, - }; const testServer = fork( __dirname + '/pine-in-process.ts', [JSON.stringify(processArgs)], @@ -51,8 +55,68 @@ export async function testInit( console.error(`TestServer wasn't created properly: ${err}`); throw err; } +}; +export async function testInit( + options: OptionalField & { + clusterMode?: false; + clusterNodes?: never; + }, +): Promise; +export async function testInit( + options: OptionalField & { + clusterMode: true; + clusterNodes: number; + }, +): Promise; +export async function testInit( + options: OptionalField, +): Promise { + const processArgs = { + listenPort: options.listenPort ?? listenPortDefault, + deleteDb: options.deleteDb ?? boolVar('DELETE_DB', false), + configPath: options.configPath, + hooksPath: options.hooksPath, + taskHandlersPath: options.taskHandlersPath, + routesPath: options.routesPath, + withLoginRoute: options.withLoginRoute, + clusterMode: options.clusterMode, + clusterNodes: options.clusterNodes, + }; + + if (options.clusterMode) { + if (!options.clusterNodes || options.clusterNodes < 2) { + throw new Error('Cluster mode requires at least 2 nodes'); + } + + const pineServerPromises: Array> = []; + const port = processArgs.listenPort ?? listenPortDefault; + const deleteDb = options.deleteDb ?? boolVar('DELETE_DB', false); + // Await for the first server to be available to avoid several + // process at the same time possibly trying to delete/recreate/read the DB tables + const server = await forkServer({ + ...processArgs, + listenPort: port, + deleteDb, + }); + + for (let i = 1; i < options.clusterNodes - 1; i++) { + const listenPort = await getAvailableTCPPort(); + pineServerPromises.push( + forkServer({ ...processArgs, listenPort, deleteDb: false }), + ); + } + return [server, ...(await Promise.all(pineServerPromises))]; + } + + return forkServer(processArgs); } -export function testDeInit(testServer: ChildProcess) { +export function testDeInit(testServer: ChildProcess | ChildProcess[]) { + if (Array.isArray(testServer)) { + testServer.forEach((server) => { + server?.kill(); + }); + return; + } testServer?.kill(); }