diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index cc052dd5b5..4f752210db 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -89,8 +89,8 @@ describe('Client', () => { && expected?.credentialsProvider?.type === 'async-credentials-provider') { // Compare the actual output of the credentials functions - const resultCreds = await result.credentialsProvider.credentials(); - const expectedCreds = await expected.credentialsProvider.credentials(); + const resultCreds = await result.credentialsProvider?.credentials(); + const expectedCreds = await expected.credentialsProvider?.credentials(); assert.deepEqual(resultCreds, expectedCreds); } else { assert.fail('Credentials provider type mismatch'); diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index a2e52b774b..a9bdc8cc95 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -197,7 +197,6 @@ describe(`test with scripts`, () => { }, GLOBAL.SENTINEL.WITH_SCRIPT) }); - describe(`test with functions`, () => { testUtils.testWithClientSentinel('with function', async sentinel => { await sentinel.functionLoad( @@ -377,12 +376,9 @@ describe(`test with masterPoolSize 2`, () => { }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); }); - -// TODO: Figure out how to modify the test utils -// so it would have fine grained controll over -// sentinel -// it should somehow replicate the `SentinelFramework` object functionallities async function steadyState(frame: SentinelFramework) { + // wait a bit to ensure that sentinels are seeing eachother + await setTimeout(2000) let checkedMaster = false; let checkedReplicas = false; while (!checkedMaster || !checkedReplicas) { @@ -430,7 +426,7 @@ async function steadyState(frame: SentinelFramework) { } } -describe.skip('legacy tests', () => { +describe('legacy tests', () => { const config: RedisSentinelConfig = { sentinelName: "test", numberOfNodes: 3, password: undefined }; const frame = new SentinelFramework(config); let tracer = new Array(); @@ -439,42 +435,30 @@ describe.skip('legacy tests', () => { let longestTestDelta = 0; let last: number; - before(async function () { - this.timeout(15000); - - last = Date.now(); - - function deltaMeasurer() { - const delta = Date.now() - last; - if (delta > longestDelta) { - longestDelta = delta; - } - if (delta > longestTestDelta) { - longestTestDelta = delta; - } - if (!stopMeasuringBlocking) { - last = Date.now(); - setImmediate(deltaMeasurer); - } - } - setImmediate(deltaMeasurer); - await frame.spawnRedisSentinel(); - }); - - after(async function () { - this.timeout(15000); - - stopMeasuringBlocking = true; - - await frame.cleanup(); - }) describe('Sentinel Client', function () { let sentinel: RedisSentinelType | undefined; beforeEach(async function () { - this.timeout(0); - + this.timeout(15000); + + last = Date.now(); + + function deltaMeasurer() { + const delta = Date.now() - last; + if (delta > longestDelta) { + longestDelta = delta; + } + if (delta > longestTestDelta) { + longestTestDelta = delta; + } + if (!stopMeasuringBlocking) { + last = Date.now(); + setImmediate(deltaMeasurer); + } + } + setImmediate(deltaMeasurer); + await frame.spawnRedisSentinel(); await frame.getAllRunning(); await steadyState(frame); longestTestDelta = 0; @@ -522,6 +506,10 @@ describe.skip('legacy tests', () => { await sentinel.destroy(); sentinel = undefined; } + + stopMeasuringBlocking = true; + + await frame.cleanup(); }) it('use', async function () { @@ -863,7 +851,6 @@ describe.skip('legacy tests', () => { it('shutdown sentinel node', async function () { this.timeout(60000); - sentinel = frame.getSentinelClient(); sentinel.setTracer(tracer); sentinel.on("error", () => { }); @@ -1020,7 +1007,7 @@ describe.skip('legacy tests', () => { this.timeout(30000); const csc = new BasicPooledClientSideCache(); - sentinel = frame.getSentinelClient({nodeClientOptions: {RESP: 3}, clientSideCache: csc, masterPoolSize: 5}); + sentinel = frame.getSentinelClient({nodeClientOptions: {RESP: 3 as const}, RESP: 3 as const, clientSideCache: csc, masterPoolSize: 5}); await sentinel.connect(); await sentinel.set('x', 1); diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 86bc5b3178..60c1a59689 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -4,12 +4,13 @@ import { once } from 'node:events'; import { promisify } from 'node:util'; import { exec } from 'node:child_process'; import { RedisSentinelOptions, RedisSentinelType } from './types'; -import RedisClient from '../client'; +import RedisClient, {RedisClientType} from '../client'; import RedisSentinel from '.'; import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; const execAsync = promisify(exec); import RedisSentinelModule from './module' - +import TestUtils from '@redis/test-utils'; +import { DEBUG_MODE_ARGS } from '../test-utils' interface ErrorWithCode extends Error { code: string; } @@ -125,7 +126,6 @@ export interface RedisSentinelConfig { sentinelServerArgument?: Array sentinelName: string; - sentinelQuorum?: number; password?: string; } @@ -151,6 +151,7 @@ export interface SentinelController { } export class SentinelFramework extends DockerBase { + #testUtils: TestUtils; #nodeList: Awaited> = []; /* port -> docker info/client */ #nodeMap: Map>>>; @@ -170,7 +171,11 @@ export class SentinelFramework extends DockerBase { super(); this.config = config; - + this.#testUtils = TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M05-pre' + }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); } @@ -190,7 +195,7 @@ export class SentinelFramework extends DockerBase { const options: RedisSentinelOptions = { ...opts, name: this.config.sentinelName, - sentinelRootNodes: this.#sentinelList.map((sentinel) => { return { host: '127.0.0.1', port: sentinel.docker.port } }), + sentinelRootNodes: this.#sentinelList.map((sentinel) => { return { host: '127.0.0.1', port: sentinel.port } }), passthroughClientErrorEvents: errors } @@ -218,11 +223,11 @@ export class SentinelFramework extends DockerBase { throw new Error("inconsistent state with partial setup"); } - this.#nodeList = await this.spawnRedisSentinelNodes(); - this.#nodeList.map((value) => this.#nodeMap.set(value.docker.port.toString(), value)); + this.#nodeList = await this.spawnRedisSentinelNodes(2); + this.#nodeList.map((value) => this.#nodeMap.set(value.port.toString(), value)); - this.#sentinelList = await this.spawnRedisSentinelSentinels(); - this.#sentinelList.map((value) => this.#sentinelMap.set(value.docker.port.toString(), value)); + this.#sentinelList = await this.spawnRedisSentinelSentinels(this.#nodeList[0].port, 3) + this.#sentinelList.map((value) => this.#sentinelMap.set(value.port.toString(), value)); this.#spawned = true; } @@ -234,11 +239,8 @@ export class SentinelFramework extends DockerBase { return Promise.all( [...this.#nodeMap!.values(), ...this.#sentinelMap!.values()].map( - async ({ docker, client }) => { - if (client.isOpen) { - client.destroy(); - } - this.dockerRemove(docker.dockerId); + async ({ dockerId }) => { + this.dockerRemove(dockerId); } ) ).finally(async () => { @@ -248,112 +250,33 @@ export class SentinelFramework extends DockerBase { }); } - protected async spawnRedisSentinelNodeDocker() { - const imageInfo: RedisServerDockerConfig = this.config.nodeDockerConfig ?? { image: "redis/redis-stack-server", version: "latest" }; - const serverArguments: Array = this.config.nodeServerArguments ?? []; - let environment; - if (this.config.password !== undefined) { - environment = `REDIS_ARGS="{port} --requirepass ${this.config.password}"`; - } else { - environment = 'REDIS_ARGS="{port}"'; - } - - const docker = await this.spawnRedisServerDocker(imageInfo, serverArguments, environment); - const client = await RedisClient.create({ - password: this.config.password, - socket: { - port: docker.port - } - }).on("error", () => { }).connect(); - - return { - docker, - client - }; - } - - protected async spawnRedisSentinelNodes() { - const master = await this.spawnRedisSentinelNodeDocker(); + protected async spawnRedisSentinelNodes(replicasCount: number) { + const master = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) + + const replicas: Array = [] + for (let i = 0; i < replicasCount; i++) { + const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) + replicas.push(replica) - const promises: Array> = []; + const client = RedisClient.create({ + socket: { + port: replica.port + } + }) - for (let i = 0; i < (this.config.numberOfNodes ?? 0) - 1; i++) { - promises.push( - this.spawnRedisSentinelNodeDocker().then(async node => { - if (this.config.password !== undefined) { - await node.client.configSet({'masterauth': this.config.password}) - } - await node.client.replicaOf('127.0.0.1', master.docker.port); - return node; - }) - ); + await client.connect(); + await client.replicaOf("127.0.0.1", master.port); + await client.close(); } return [ master, - ...await Promise.all(promises) - ]; - } - - protected async spawnRedisSentinelSentinelDocker() { - const imageInfo: RedisServerDockerConfig = this.config.sentinelDockerConfig ?? { image: "redis", version: "latest" } - let serverArguments: Array; - if (this.config.password === undefined) { - serverArguments = this.config.sentinelServerArgument ?? - [ - "/bin/bash", - "-c", - "\"touch /tmp/sentinel.conf ; /usr/local/bin/redis-sentinel /tmp/sentinel.conf {port} \"" - ]; - } else { - serverArguments = this.config.sentinelServerArgument ?? - [ - "/bin/bash", - "-c", - `"touch /tmp/sentinel.conf ; /usr/local/bin/redis-sentinel /tmp/sentinel.conf {port} --requirepass ${this.config.password}"` - ]; - } - - const docker = await this.spawnRedisServerDocker(imageInfo, serverArguments); - const client = await RedisClient.create({ - modules: RedisSentinelModule, - password: this.config.password, - socket: { - port: docker.port - } - }).on("error", () => { }).connect(); - - return { - docker, - client - }; + ...replicas + ] } - protected async spawnRedisSentinelSentinels() { - const quorum = this.config.sentinelQuorum?.toString() ?? "2"; - const node = this.#nodeList[0]; - - const promises: Array> = []; - - for (let i = 0; i < (this.config.numberOfSentinels ?? 3); i++) { - promises.push( - this.spawnRedisSentinelSentinelDocker().then(async sentinel => { - await sentinel.client.sentinel.sentinelMonitor(this.config.sentinelName, '127.0.0.1', node.docker.port.toString(), quorum); - const options: Array<{option: RedisArgument, value: RedisArgument}> = []; - options.push({ option: "down-after-milliseconds", value: "100" }); - options.push({ option: "failover-timeout", value: "5000" }); - if (this.config.password !== undefined) { - options.push({ option: "auth-pass", value: this.config.password }); - } - await sentinel.client.sentinel.sentinelSet(this.config.sentinelName, options) - return sentinel; - }) - ); - } - - return [ - ...await Promise.all(promises) - ] + protected async spawnRedisSentinelSentinels(masterPort: number, sentinels: number) { + return this.#testUtils.spawnRedisSentinels({serverArguments: DEBUG_MODE_ARGS}, masterPort, this.config.sentinelName, sentinels) } async getAllRunning() { @@ -384,90 +307,71 @@ export class SentinelFramework extends DockerBase { } async addSentinel() { - const quorum = this.config.sentinelQuorum?.toString() ?? "2"; - const node = this.#nodeList[0]; - const sentinel = await this.spawnRedisSentinelSentinelDocker(); - - await sentinel.client.sentinel.sentinelMonitor(this.config.sentinelName, '127.0.0.1', node.docker.port.toString(), quorum); - const options: Array<{option: RedisArgument, value: RedisArgument}> = []; - options.push({ option: "down-after-milliseconds", value: "100" }); - options.push({ option: "failover-timeout", value: "5000" }); - if (this.config.password !== undefined) { - options.push({ option: "auth-pass", value: this.config.password }); - } - await sentinel.client.sentinel.sentinelSet(this.config.sentinelName, options); - - this.#sentinelList.push(sentinel); - this.#sentinelMap.set(sentinel.docker.port.toString(), sentinel); + const nodes = await this.#testUtils.spawnRedisSentinels({serverArguments: DEBUG_MODE_ARGS}, this.#nodeList[0].port, this.config.sentinelName, 1) + this.#sentinelList.push(nodes[0]); + this.#sentinelMap.set(nodes[0].port.toString(), nodes[0]); } async addNode() { const masterPort = await this.getMasterPort(); - const newNode = await this.spawnRedisSentinelNodeDocker(); + const replica = await this.#testUtils.spawnRedisServer({serverArguments: DEBUG_MODE_ARGS}) - if (this.config.password !== undefined) { - await newNode.client.configSet({'masterauth': this.config.password}) - } - await newNode.client.replicaOf('127.0.0.1', masterPort); + const client = RedisClient.create({ + socket: { + port: replica.port + } + }) + + await client.connect(); + await client.replicaOf("127.0.0.1", masterPort); + await client.close(); + - this.#nodeList.push(newNode); - this.#nodeMap.set(newNode.docker.port.toString(), newNode); + this.#nodeList.push(replica); + this.#nodeMap.set(replica.port.toString(), replica); } async getMaster(tracer?: Array): Promise { - for (const sentinel of this.#sentinelMap!.values()) { - let info; - - try { - if (!sentinel.client.isReady) { - continue; - } - - info = await sentinel.client.sentinel.sentinelMaster(this.config.sentinelName); - if (tracer) { - tracer.push('getMaster: master data returned from sentinel'); - tracer.push(JSON.stringify(info, undefined, '\t')) - } - } catch (err) { - console.log("getMaster: sentinelMaster call failed: " + err); - continue; - } - - const master = this.#nodeMap.get(info.port); - if (master === undefined) { - throw new Error(`couldn't find master node for ${info.port}`); - } - - if (tracer) { - tracer.push(`getMaster: master port is either ${info.port} or ${master.docker.port}`); - } + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const info = await client.sentinel.sentinelMaster(this.config.sentinelName); + await client.close() - if (!master.client.isOpen) { - throw new Error(`Sentinel's expected master node (${info.port}) is now down`); - } + const master = this.#nodeMap.get(info.port); + if (master === undefined) { + throw new Error(`couldn't find master node for ${info.port}`); + } - return info.port; + if (tracer) { + tracer.push(`getMaster: master port is either ${info.port} or ${master.port}`); } - throw new Error("Couldn't get master"); + return info.port; } async getMasterPort(tracer?: Array): Promise { const data = await this.getMaster(tracer) - return this.#nodeMap.get(data!)!.docker.port; + return this.#nodeMap.get(data!)!.port; } getRandomNode() { - return this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)].docker.port.toString(); + return this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)].port.toString(); } async getRandonNonMasterNode(): Promise { const masterPort = await this.getMasterPort(); while (true) { const node = this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)]; - if (node.docker.port != masterPort) { - return node.docker.port.toString(); + if (node.port != masterPort) { + return node.port.toString(); } } } @@ -479,11 +383,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown node: " + id); } - if (node.client.isOpen) { - node.client.destroy(); - } - - return await this.dockerStop(node.docker.dockerId); + return await this.dockerStop(node.dockerId); } async restartNode(id: string) { @@ -492,15 +392,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown node: " + id); } - await this.dockerStart(node.docker.dockerId); - if (!node.client.isOpen) { - node.client = await RedisClient.create({ - password: this.config.password, - socket: { - port: node.docker.port - } - }).on("error", () => { }).connect(); - } + await this.dockerStart(node.dockerId); } async stopSentinel(id: string) { @@ -509,11 +401,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown sentinel: " + id); } - if (sentinel.client.isOpen) { - sentinel.client.destroy(); - } - - return await this.dockerStop(sentinel.docker.dockerId); + return await this.dockerStop(sentinel.dockerId); } async restartSentinel(id: string) { @@ -522,16 +410,7 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown sentinel: " + id); } - await this.dockerStart(sentinel.docker.dockerId); - if (!sentinel.client.isOpen) { - sentinel.client = await RedisClient.create({ - modules: RedisSentinelModule, - password: this.config.password, - socket: { - port: sentinel.docker.port - } - }).on("error", () => { }).connect(); - } + await this.dockerStart(sentinel.dockerId); } getNodePort(id: string) { @@ -540,13 +419,13 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown node: " + id); } - return node.docker.port; + return node.port; } getAllNodesPort() { let ports: Array = []; for (const node of this.#nodeList) { - ports.push(node.docker.port); + ports.push(node.port); } return ports @@ -555,7 +434,7 @@ export class SentinelFramework extends DockerBase { getAllDockerIds() { let ids = new Map(); for (const node of this.#nodeList) { - ids.set(node.docker.dockerId, node.docker.port); + ids.set(node.dockerId, node.port); } return ids; @@ -567,43 +446,67 @@ export class SentinelFramework extends DockerBase { throw new Error("unknown sentinel: " + id); } - return sentinel.docker.port; + return sentinel.port; } getAllSentinelsPort() { let ports: Array = []; for (const sentinel of this.#sentinelList) { - ports.push(sentinel.docker.port); + ports.push(sentinel.port); } return ports } getSetinel(i: number): string { - return this.#sentinelList[i].docker.port.toString(); + return this.#sentinelList[i].port.toString(); } - sentinelSentinels() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelSentinels(this.config.sentinelName); - } - } + async sentinelSentinels() { + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const sentinels = client.sentinel.sentinelSentinels(this.config.sentinelName) + await client.close() + + return sentinels } - sentinelMaster() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelMaster(this.config.sentinelName); - } - } + async sentinelMaster() { + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const master = client.sentinel.sentinelMaster(this.config.sentinelName) + await client.close() + + return master } - sentinelReplicas() { - for (const sentinel of this.#sentinelList) { - if (sentinel.client.isReady) { - return sentinel.client.sentinel.sentinelReplicas(this.config.sentinelName); - } - } + async sentinelReplicas() { + const client = RedisClient.create({ + name: this.config.sentinelName, + socket: { + host: "127.0.0.1", + port: this.#sentinelList[0].port, + }, + modules: RedisSentinelModule, + }); + await client.connect() + const replicas = client.sentinel.sentinelReplicas(this.config.sentinelName) + await client.close() + + return replicas } } \ No newline at end of file diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 63f43ba5e6..19bbafc66e 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -14,7 +14,7 @@ const utils = TestUtils.createFromConfig({ export default utils; -const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? +export const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? ['--enable-debug-command', 'yes'] : []; diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index 3814a80923..47257964f6 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -62,7 +62,7 @@ export interface RedisServerDocker { dockerId: string; } -async function spawnRedisServerDocker( +export async function spawnRedisServerDocker( options: RedisServerDockerOptions, serverArguments: Array): Promise { let port; if (options.mode == "sentinel") { @@ -374,35 +374,16 @@ export async function spawnRedisSentinel( const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix)); for (let i = 0; i < sentinelCount; i++) { - sentinelPromises.push((async () => { - const port = (await portIterator.next()).value; - - let sentinelConfig = `port ${port} -sentinel monitor mymaster 127.0.0.1 ${master.port} 2 -sentinel down-after-milliseconds mymaster 5000 -sentinel failover-timeout mymaster 6000 -`; - if (password !== undefined) { - sentinelConfig += `requirepass ${password}\n`; - sentinelConfig += `sentinel auth-pass mymaster ${password}\n`; - } - - const dir = fs.mkdtempSync(path.join(tmpDir, i.toString())); - fs.writeFile(`${dir}/redis.conf`, sentinelConfig, err => { - if (err) { - console.error("failed to create temporary config file", err); - } - }); - - return await spawnRedisServerDocker( - { - image: dockerConfigs.image, - version: dockerConfigs.version, - mode: "sentinel", - mounts: [`${dir}/redis.conf:/redis/config/node-sentinel-1/redis.conf`], - port: port, - }, serverArguments); - })()); + sentinelPromises.push( + spawnSentinelNode( + dockerConfigs, + serverArguments, + master.port, + "mymaster", + path.join(tmpDir, i.toString()), + password, + ), + ) } const sentinelNodes = await Promise.all(sentinelPromises); @@ -424,3 +405,43 @@ after(() => { }) ); }); + + +export async function spawnSentinelNode( + dockerConfigs: RedisServerDockerOptions, + serverArguments: Array, + masterPort: number, + sentinelName: string, + tmpDir: string, + password?: string, +) { + const port = (await portIterator.next()).value; + + let sentinelConfig = `port ${port} +sentinel monitor ${sentinelName} 127.0.0.1 ${masterPort} 2 +sentinel down-after-milliseconds ${sentinelName} 500 +sentinel failover-timeout ${sentinelName} 1000 +`; + if (password !== undefined) { + sentinelConfig += `requirepass ${password}\n`; + sentinelConfig += `sentinel auth-pass ${sentinelName} ${password}\n`; + } + + const dir = fs.mkdtempSync(tmpDir); + fs.writeFile(`${dir}/redis.conf`, sentinelConfig, err => { + if (err) { + console.error("failed to create temporary config file", err); + } + }); + + return await spawnRedisServerDocker( + { + image: dockerConfigs.image, + version: dockerConfigs.version, + mode: "sentinel", + mounts: [`${dir}/redis.conf:/redis/config/node-sentinel-1/redis.conf`], + port: port, + }, + serverArguments, + ); +} \ No newline at end of file diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index d92c5c9e3d..a41f970e0c 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -19,10 +19,13 @@ import { RedisClusterType } from '@redis/client/index'; import { RedisNode } from '@redis/client/lib/sentinel/types' -import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions } from './dockers'; +import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions, RedisServerDocker, spawnSentinelNode, spawnRedisServerDocker } from './dockers'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; interface TestUtilsConfig { /** @@ -395,19 +398,19 @@ export default class TestUtils { S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {} ->( - range: ([minVersion: Array, maxVersion: Array] | [minVersion: Array, 'LATEST']), - title: string, - fn: (sentinel: RedisSentinelType) => unknown, - options: SentinelTestOptions -): void { - - if (this.isVersionInRange(range[0], range[1] === 'LATEST' ? [Infinity, Infinity, Infinity] : range[1])) { - return this.testWithClientSentinel(`${title} [${range[0].join('.')}] - [${(range[1] === 'LATEST') ? range[1] : range[1].join(".")}] `, fn, options) - } else { - console.warn(`Skipping test ${title} because server version ${this.#VERSION_NUMBERS.join('.')} is not within range ${range[0].join(".")} - ${range[1] !== 'LATEST' ? range[1].join(".") : 'LATEST'}`) + >( + range: ([minVersion: Array, maxVersion: Array] | [minVersion: Array, 'LATEST']), + title: string, + fn: (sentinel: RedisSentinelType) => unknown, + options: SentinelTestOptions + ): void { + + if (this.isVersionInRange(range[0], range[1] === 'LATEST' ? [Infinity, Infinity, Infinity] : range[1])) { + return this.testWithClientSentinel(`${title} [${range[0].join('.')}] - [${(range[1] === 'LATEST') ? range[1] : range[1].join(".")}] `, fn, options) + } else { + console.warn(`Skipping test ${title} because server version ${this.#VERSION_NUMBERS.join('.')} is not within range ${range[0].join(".")} - ${range[1] !== 'LATEST' ? range[1].join(".") : 'LATEST'}`) + } } -} testWithClientPool< M extends RedisModules = {}, @@ -541,4 +544,46 @@ export default class TestUtils { this.testWithClient(`client.${title}`, fn, options.client); this.testWithCluster(`cluster.${title}`, fn, options.cluster); } + + + spawnRedisServer< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + // POLICIES extends CommandPolicies = {} + >( + options: ClientPoolTestOptions + ): Promise { + return spawnRedisServerDocker(this.#DOCKER_IMAGE, options.serverArguments) + } + + async spawnRedisSentinels< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + // POLICIES extends CommandPolicies = {} + >( + options: ClientPoolTestOptions, + masterPort: number, + sentinelName: string, + count: number + ): Promise> { + const sentinels: Array = []; + for (let i = 0; i < count; i++) { + const appPrefix = 'sentinel-config-dir'; + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix)); + + sentinels.push(await spawnSentinelNode(this.#DOCKER_IMAGE, options.serverArguments, masterPort, sentinelName, tmpDir)) + + if (tmpDir) { + fs.rmSync(tmpDir, { recursive: true }); + } + } + + return sentinels + } }