You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-06 02:15:48 +03:00
V5 bringing RESP3, Sentinel and TypeMapping to node-redis
RESP3 Support - Some commands responses in RESP3 aren't stable yet and therefore return an "untyped" ReplyUnion. Sentinel TypeMapping Correctly types Multi commands Note: some API changes to be further documented in v4-to-v5.md
This commit is contained in:
@@ -1,260 +1,256 @@
|
||||
import { createConnection } from 'net';
|
||||
import { once } from 'events';
|
||||
import RedisClient from '@redis/client/dist/lib/client';
|
||||
import { promiseTimeout } from '@redis/client/dist/lib/utils';
|
||||
import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS';
|
||||
import * as path from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { exec } from 'child_process';
|
||||
import { createConnection } from 'node:net';
|
||||
import { once } from 'node:events';
|
||||
import { createClient } from '@redis/client/index';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
// import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS';
|
||||
import { promisify } from 'node:util';
|
||||
import { exec } from 'node:child_process';
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
interface ErrorWithCode extends Error {
|
||||
code: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
async function isPortAvailable(port: number): Promise<boolean> {
|
||||
try {
|
||||
const socket = createConnection({ port });
|
||||
await once(socket, 'connect');
|
||||
socket.end();
|
||||
} catch (err) {
|
||||
if (err instanceof Error && (err as ErrorWithCode).code === 'ECONNREFUSED') {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const socket = createConnection({ port });
|
||||
await once(socket, 'connect');
|
||||
socket.end();
|
||||
} catch (err) {
|
||||
if (err instanceof Error && (err as ErrorWithCode).code === 'ECONNREFUSED') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const portIterator = (async function*(): AsyncIterableIterator<number> {
|
||||
for (let i = 6379; i < 65535; i++) {
|
||||
if (await isPortAvailable(i)) {
|
||||
yield i;
|
||||
}
|
||||
const portIterator = (async function* (): AsyncIterableIterator<number> {
|
||||
for (let i = 6379; i < 65535; i++) {
|
||||
if (await isPortAvailable(i)) {
|
||||
yield i;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('All ports are in use');
|
||||
throw new Error('All ports are in use');
|
||||
})();
|
||||
|
||||
export interface RedisServerDockerConfig {
|
||||
image: string;
|
||||
version: string;
|
||||
image: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface RedisServerDocker {
|
||||
port: number;
|
||||
dockerId: string;
|
||||
port: number;
|
||||
dockerId: string;
|
||||
}
|
||||
|
||||
// ".." cause it'll be in `./dist`
|
||||
const DOCKER_FODLER_PATH = path.join(__dirname, '../docker');
|
||||
|
||||
async function spawnRedisServerDocker({ image, version }: RedisServerDockerConfig, serverArguments: Array<string>): Promise<RedisServerDocker> {
|
||||
const port = (await portIterator.next()).value,
|
||||
{ stdout, stderr } = await execAsync(
|
||||
'docker run -d --network host $(' +
|
||||
`docker build ${DOCKER_FODLER_PATH} -q ` +
|
||||
`--build-arg IMAGE=${image}:${version} ` +
|
||||
`--build-arg REDIS_ARGUMENTS="--save '' --port ${port.toString()} ${serverArguments.join(' ')}"` +
|
||||
')'
|
||||
);
|
||||
const port = (await portIterator.next()).value,
|
||||
{ stdout, stderr } = await execAsync(
|
||||
`docker run -e REDIS_ARGS="--port ${port.toString()} ${serverArguments.join(' ')}" -d --network host ${image}:${version}`
|
||||
);
|
||||
|
||||
if (!stdout) {
|
||||
throw new Error(`docker run error - ${stderr}`);
|
||||
}
|
||||
if (!stdout) {
|
||||
throw new Error(`docker run error - ${stderr}`);
|
||||
}
|
||||
|
||||
while (await isPortAvailable(port)) {
|
||||
await promiseTimeout(50);
|
||||
}
|
||||
while (await isPortAvailable(port)) {
|
||||
await setTimeout(50);
|
||||
}
|
||||
|
||||
return {
|
||||
port,
|
||||
dockerId: stdout.trim()
|
||||
};
|
||||
return {
|
||||
port,
|
||||
dockerId: stdout.trim()
|
||||
};
|
||||
}
|
||||
|
||||
const RUNNING_SERVERS = new Map<Array<string>, ReturnType<typeof spawnRedisServerDocker>>();
|
||||
|
||||
export function spawnRedisServer(dockerConfig: RedisServerDockerConfig, serverArguments: Array<string>): Promise<RedisServerDocker> {
|
||||
const runningServer = RUNNING_SERVERS.get(serverArguments);
|
||||
if (runningServer) {
|
||||
return runningServer;
|
||||
}
|
||||
const runningServer = RUNNING_SERVERS.get(serverArguments);
|
||||
if (runningServer) {
|
||||
return runningServer;
|
||||
}
|
||||
|
||||
const dockerPromise = spawnRedisServerDocker(dockerConfig, serverArguments);
|
||||
RUNNING_SERVERS.set(serverArguments, dockerPromise);
|
||||
return dockerPromise;
|
||||
const dockerPromise = spawnRedisServerDocker(dockerConfig, serverArguments);
|
||||
RUNNING_SERVERS.set(serverArguments, dockerPromise);
|
||||
return dockerPromise;
|
||||
}
|
||||
|
||||
async function dockerRemove(dockerId: string): Promise<void> {
|
||||
const { stderr } = await execAsync(`docker rm -f ${dockerId}`);
|
||||
if (stderr) {
|
||||
throw new Error(`docker rm error - ${stderr}`);
|
||||
}
|
||||
const { stderr } = await execAsync(`docker rm -f ${dockerId}`);
|
||||
if (stderr) {
|
||||
throw new Error(`docker rm error - ${stderr}`);
|
||||
}
|
||||
}
|
||||
|
||||
after(() => {
|
||||
return Promise.all(
|
||||
[...RUNNING_SERVERS.values()].map(async dockerPromise =>
|
||||
await dockerRemove((await dockerPromise).dockerId)
|
||||
)
|
||||
);
|
||||
return Promise.all(
|
||||
[...RUNNING_SERVERS.values()].map(async dockerPromise =>
|
||||
await dockerRemove((await dockerPromise).dockerId)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
export interface RedisClusterDockersConfig extends RedisServerDockerConfig {
|
||||
numberOfMasters?: number;
|
||||
numberOfReplicas?: number;
|
||||
numberOfMasters?: number;
|
||||
numberOfReplicas?: number;
|
||||
}
|
||||
|
||||
async function spawnRedisClusterNodeDockers(
|
||||
dockersConfig: RedisClusterDockersConfig,
|
||||
serverArguments: Array<string>,
|
||||
fromSlot: number,
|
||||
toSlot: number
|
||||
dockersConfig: RedisClusterDockersConfig,
|
||||
serverArguments: Array<string>,
|
||||
fromSlot: number,
|
||||
toSlot: number
|
||||
) {
|
||||
const range: Array<number> = [];
|
||||
for (let i = fromSlot; i < toSlot; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
const range: Array<number> = [];
|
||||
for (let i = fromSlot; i < toSlot; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
const master = await spawnRedisClusterNodeDocker(
|
||||
dockersConfig,
|
||||
serverArguments
|
||||
);
|
||||
const master = await spawnRedisClusterNodeDocker(
|
||||
dockersConfig,
|
||||
serverArguments
|
||||
);
|
||||
|
||||
await master.client.clusterAddSlots(range);
|
||||
await master.client.clusterAddSlots(range);
|
||||
|
||||
if (!dockersConfig.numberOfReplicas) return [master];
|
||||
|
||||
const replicasPromises: Array<ReturnType<typeof spawnRedisClusterNodeDocker>> = [];
|
||||
for (let i = 0; i < (dockersConfig.numberOfReplicas ?? 0); i++) {
|
||||
replicasPromises.push(
|
||||
spawnRedisClusterNodeDocker(dockersConfig, [
|
||||
...serverArguments,
|
||||
'--cluster-enabled',
|
||||
'yes',
|
||||
'--cluster-node-timeout',
|
||||
'5000'
|
||||
]).then(async replica => {
|
||||
await replica.client.clusterMeet('127.0.0.1', master.docker.port);
|
||||
if (!dockersConfig.numberOfReplicas) return [master];
|
||||
|
||||
while ((await replica.client.clusterSlots()).length === 0) {
|
||||
await promiseTimeout(50);
|
||||
}
|
||||
const replicasPromises: Array<ReturnType<typeof spawnRedisClusterNodeDocker>> = [];
|
||||
for (let i = 0; i < (dockersConfig.numberOfReplicas ?? 0); i++) {
|
||||
replicasPromises.push(
|
||||
spawnRedisClusterNodeDocker(dockersConfig, [
|
||||
...serverArguments,
|
||||
'--cluster-enabled',
|
||||
'yes',
|
||||
'--cluster-node-timeout',
|
||||
'5000'
|
||||
]).then(async replica => {
|
||||
await replica.client.clusterMeet('127.0.0.1', master.docker.port);
|
||||
|
||||
await replica.client.clusterReplicate(
|
||||
await master.client.clusterMyId()
|
||||
);
|
||||
while ((await replica.client.clusterSlots()).length === 0) {
|
||||
await setTimeout(50);
|
||||
}
|
||||
|
||||
return replica;
|
||||
})
|
||||
await replica.client.clusterReplicate(
|
||||
await master.client.clusterMyId()
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
master,
|
||||
...await Promise.all(replicasPromises)
|
||||
];
|
||||
return replica;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
master,
|
||||
...await Promise.all(replicasPromises)
|
||||
];
|
||||
}
|
||||
|
||||
async function spawnRedisClusterNodeDocker(
|
||||
dockersConfig: RedisClusterDockersConfig,
|
||||
serverArguments: Array<string>
|
||||
dockersConfig: RedisClusterDockersConfig,
|
||||
serverArguments: Array<string>
|
||||
) {
|
||||
const docker = await spawnRedisServerDocker(dockersConfig, [
|
||||
...serverArguments,
|
||||
'--cluster-enabled',
|
||||
'yes',
|
||||
'--cluster-node-timeout',
|
||||
'5000'
|
||||
]),
|
||||
client = RedisClient.create({
|
||||
socket: {
|
||||
port: docker.port
|
||||
}
|
||||
});
|
||||
const docker = await spawnRedisServerDocker(dockersConfig, [
|
||||
...serverArguments,
|
||||
'--cluster-enabled',
|
||||
'yes',
|
||||
'--cluster-node-timeout',
|
||||
'5000'
|
||||
]),
|
||||
client = createClient({
|
||||
socket: {
|
||||
port: docker.port
|
||||
}
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
await client.connect();
|
||||
|
||||
return {
|
||||
docker,
|
||||
client
|
||||
};
|
||||
return {
|
||||
docker,
|
||||
client
|
||||
};
|
||||
}
|
||||
|
||||
const SLOTS = 16384;
|
||||
|
||||
async function spawnRedisClusterDockers(
|
||||
dockersConfig: RedisClusterDockersConfig,
|
||||
serverArguments: Array<string>
|
||||
dockersConfig: RedisClusterDockersConfig,
|
||||
serverArguments: Array<string>
|
||||
): Promise<Array<RedisServerDocker>> {
|
||||
const numberOfMasters = dockersConfig.numberOfMasters ?? 2,
|
||||
slotsPerNode = Math.floor(SLOTS / numberOfMasters),
|
||||
spawnPromises: Array<ReturnType<typeof spawnRedisClusterNodeDockers>> = [];
|
||||
for (let i = 0; i < numberOfMasters; i++) {
|
||||
const fromSlot = i * slotsPerNode,
|
||||
toSlot = i === numberOfMasters - 1 ? SLOTS : fromSlot + slotsPerNode;
|
||||
spawnPromises.push(
|
||||
spawnRedisClusterNodeDockers(
|
||||
dockersConfig,
|
||||
serverArguments,
|
||||
fromSlot,
|
||||
toSlot
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const nodes = (await Promise.all(spawnPromises)).flat(),
|
||||
meetPromises: Array<Promise<unknown>> = [];
|
||||
for (let i = 1; i < nodes.length; i++) {
|
||||
meetPromises.push(
|
||||
nodes[i].client.clusterMeet('127.0.0.1', nodes[0].docker.port)
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(meetPromises);
|
||||
|
||||
await Promise.all(
|
||||
nodes.map(async ({ client }) => {
|
||||
while (totalNodes(await client.clusterSlots()) !== nodes.length) {
|
||||
await promiseTimeout(50);
|
||||
}
|
||||
|
||||
return client.disconnect();
|
||||
})
|
||||
const numberOfMasters = dockersConfig.numberOfMasters ?? 2,
|
||||
slotsPerNode = Math.floor(SLOTS / numberOfMasters),
|
||||
spawnPromises: Array<ReturnType<typeof spawnRedisClusterNodeDockers>> = [];
|
||||
for (let i = 0; i < numberOfMasters; i++) {
|
||||
const fromSlot = i * slotsPerNode,
|
||||
toSlot = i === numberOfMasters - 1 ? SLOTS : fromSlot + slotsPerNode;
|
||||
spawnPromises.push(
|
||||
spawnRedisClusterNodeDockers(
|
||||
dockersConfig,
|
||||
serverArguments,
|
||||
fromSlot,
|
||||
toSlot
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return nodes.map(({ docker }) => docker);
|
||||
const nodes = (await Promise.all(spawnPromises)).flat(),
|
||||
meetPromises: Array<Promise<unknown>> = [];
|
||||
for (let i = 1; i < nodes.length; i++) {
|
||||
meetPromises.push(
|
||||
nodes[i].client.clusterMeet('127.0.0.1', nodes[0].docker.port)
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(meetPromises);
|
||||
|
||||
await Promise.all(
|
||||
nodes.map(async ({ client }) => {
|
||||
while (
|
||||
totalNodes(await client.clusterSlots()) !== nodes.length ||
|
||||
!(await client.sendCommand<string>(['CLUSTER', 'INFO'])).startsWith('cluster_state:ok') // TODO
|
||||
) {
|
||||
await setTimeout(50);
|
||||
}
|
||||
|
||||
client.destroy();
|
||||
})
|
||||
);
|
||||
|
||||
return nodes.map(({ docker }) => docker);
|
||||
}
|
||||
|
||||
function totalNodes(slots: ClusterSlotsReply) {
|
||||
let total = slots.length;
|
||||
for (const slot of slots) {
|
||||
total += slot.replicas.length;
|
||||
}
|
||||
// TODO: type ClusterSlotsReply
|
||||
function totalNodes(slots: any) {
|
||||
let total = slots.length;
|
||||
for (const slot of slots) {
|
||||
total += slot.replicas.length;
|
||||
}
|
||||
|
||||
return total;
|
||||
return total;
|
||||
}
|
||||
|
||||
const RUNNING_CLUSTERS = new Map<Array<string>, ReturnType<typeof spawnRedisClusterDockers>>();
|
||||
|
||||
export function spawnRedisCluster(dockersConfig: RedisClusterDockersConfig, serverArguments: Array<string>): Promise<Array<RedisServerDocker>> {
|
||||
const runningCluster = RUNNING_CLUSTERS.get(serverArguments);
|
||||
if (runningCluster) {
|
||||
return runningCluster;
|
||||
}
|
||||
const runningCluster = RUNNING_CLUSTERS.get(serverArguments);
|
||||
if (runningCluster) {
|
||||
return runningCluster;
|
||||
}
|
||||
|
||||
const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments);
|
||||
RUNNING_CLUSTERS.set(serverArguments, dockersPromise);
|
||||
return dockersPromise;
|
||||
const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments);
|
||||
RUNNING_CLUSTERS.set(serverArguments, dockersPromise);
|
||||
return dockersPromise;
|
||||
}
|
||||
|
||||
after(() => {
|
||||
return Promise.all(
|
||||
[...RUNNING_CLUSTERS.values()].map(async dockersPromise => {
|
||||
return Promise.all(
|
||||
(await dockersPromise).map(({ dockerId }) => dockerRemove(dockerId))
|
||||
);
|
||||
})
|
||||
);
|
||||
return Promise.all(
|
||||
[...RUNNING_CLUSTERS.values()].map(async dockersPromise => {
|
||||
return Promise.all(
|
||||
(await dockersPromise).map(({ dockerId }) => dockerRemove(dockerId))
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@@ -1,226 +1,339 @@
|
||||
import { RedisModules, RedisFunctions, RedisScripts } from '@redis/client/lib/commands';
|
||||
import RedisClient, { RedisClientOptions, RedisClientType } from '@redis/client/lib/client';
|
||||
import RedisCluster, { RedisClusterOptions, RedisClusterType } from '@redis/client/lib/cluster';
|
||||
import { RedisSocketCommonOptions } from '@redis/client/lib/client/socket';
|
||||
import {
|
||||
RedisModules,
|
||||
RedisFunctions,
|
||||
RedisScripts,
|
||||
RespVersions,
|
||||
TypeMapping,
|
||||
// CommandPolicies,
|
||||
createClient,
|
||||
RedisClientOptions,
|
||||
RedisClientType,
|
||||
RedisPoolOptions,
|
||||
RedisClientPoolType,
|
||||
createClientPool,
|
||||
createCluster,
|
||||
RedisClusterOptions,
|
||||
RedisClusterType
|
||||
} from '@redis/client/index';
|
||||
import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
|
||||
interface TestUtilsConfig {
|
||||
dockerImageName: string;
|
||||
dockerImageVersionArgument: string;
|
||||
defaultDockerVersion?: string;
|
||||
dockerImageName: string;
|
||||
dockerImageVersionArgument: string;
|
||||
defaultDockerVersion?: string;
|
||||
}
|
||||
|
||||
interface CommonTestOptions {
|
||||
minimumDockerVersion?: Array<number>;
|
||||
serverArguments: Array<string>;
|
||||
minimumDockerVersion?: Array<number>;
|
||||
}
|
||||
|
||||
interface ClientTestOptions<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> extends CommonTestOptions {
|
||||
serverArguments: Array<string>;
|
||||
clientOptions?: Partial<Omit<RedisClientOptions<M, F, S>, 'socket'> & { socket: RedisSocketCommonOptions }>;
|
||||
disableClientSetup?: boolean;
|
||||
clientOptions?: Partial<RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>>;
|
||||
disableClientSetup?: boolean;
|
||||
}
|
||||
|
||||
interface ClientPoolTestOptions<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> extends CommonTestOptions {
|
||||
clientOptions?: Partial<RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>>;
|
||||
poolOptions?: RedisPoolOptions;
|
||||
}
|
||||
|
||||
interface ClusterTestOptions<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
// POLICIES extends CommandPolicies
|
||||
> extends CommonTestOptions {
|
||||
serverArguments: Array<string>;
|
||||
clusterConfiguration?: Partial<RedisClusterOptions<M, F, S>>;
|
||||
numberOfMasters?: number;
|
||||
numberOfReplicas?: number;
|
||||
clusterConfiguration?: Partial<RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>>;
|
||||
numberOfMasters?: number;
|
||||
numberOfReplicas?: number;
|
||||
}
|
||||
|
||||
interface AllTestOptions<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
// POLICIES extends CommandPolicies
|
||||
> {
|
||||
client: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>;
|
||||
cluster: ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>;
|
||||
}
|
||||
|
||||
interface Version {
|
||||
string: string;
|
||||
numbers: Array<number>;
|
||||
string: string;
|
||||
numbers: Array<number>;
|
||||
}
|
||||
|
||||
export default class TestUtils {
|
||||
static #parseVersionNumber(version: string): Array<number> {
|
||||
if (version === 'latest' || version === 'edge') return [Infinity];
|
||||
static #parseVersionNumber(version: string): Array<number> {
|
||||
if (version === 'latest' || version === 'edge') return [Infinity];
|
||||
|
||||
const dashIndex = version.indexOf('-');
|
||||
return (dashIndex === -1 ? version : version.substring(0, dashIndex))
|
||||
.split('.')
|
||||
.map(x => {
|
||||
const value = Number(x);
|
||||
if (Number.isNaN(value)) {
|
||||
throw new TypeError(`${version} is not a valid redis version`);
|
||||
}
|
||||
const dashIndex = version.indexOf('-');
|
||||
return (dashIndex === -1 ? version : version.substring(0, dashIndex))
|
||||
.split('.')
|
||||
.map(x => {
|
||||
const value = Number(x);
|
||||
if (Number.isNaN(value)) {
|
||||
throw new TypeError(`${version} is not a valid redis version`);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
static #getVersion(argumentName: string, defaultVersion = 'latest'): Version {
|
||||
return yargs(hideBin(process.argv))
|
||||
.option(argumentName, {
|
||||
type: 'string',
|
||||
default: defaultVersion
|
||||
})
|
||||
.coerce(argumentName, (version: string) => {
|
||||
return {
|
||||
string: version,
|
||||
numbers: TestUtils.#parseVersionNumber(version)
|
||||
};
|
||||
})
|
||||
.demandOption(argumentName)
|
||||
.parseSync()[argumentName];
|
||||
}
|
||||
|
||||
readonly #VERSION_NUMBERS: Array<number>;
|
||||
readonly #DOCKER_IMAGE: RedisServerDockerConfig;
|
||||
|
||||
constructor(config: TestUtilsConfig) {
|
||||
const { string, numbers } = TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion);
|
||||
this.#VERSION_NUMBERS = numbers;
|
||||
this.#DOCKER_IMAGE = {
|
||||
image: config.dockerImageName,
|
||||
version: string
|
||||
static #getVersion(argumentName: string, defaultVersion = 'latest'): Version {
|
||||
return yargs(hideBin(process.argv))
|
||||
.option(argumentName, {
|
||||
type: 'string',
|
||||
default: defaultVersion
|
||||
})
|
||||
.coerce(argumentName, (version: string) => {
|
||||
return {
|
||||
string: version,
|
||||
numbers: TestUtils.#parseVersionNumber(version)
|
||||
};
|
||||
})
|
||||
.demandOption(argumentName)
|
||||
.parseSync()[argumentName];
|
||||
}
|
||||
|
||||
readonly #VERSION_NUMBERS: Array<number>;
|
||||
readonly #DOCKER_IMAGE: RedisServerDockerConfig;
|
||||
|
||||
constructor(config: TestUtilsConfig) {
|
||||
const { string, numbers } = TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion);
|
||||
this.#VERSION_NUMBERS = numbers;
|
||||
this.#DOCKER_IMAGE = {
|
||||
image: config.dockerImageName,
|
||||
version: string
|
||||
};
|
||||
}
|
||||
|
||||
isVersionGreaterThan(minimumVersion: Array<number> | undefined): boolean {
|
||||
if (minimumVersion === undefined) return true;
|
||||
|
||||
const lastIndex = Math.min(this.#VERSION_NUMBERS.length, minimumVersion.length) - 1;
|
||||
for (let i = 0; i < lastIndex; i++) {
|
||||
if (this.#VERSION_NUMBERS[i] > minimumVersion[i]) {
|
||||
return true;
|
||||
} else if (minimumVersion[i] > this.#VERSION_NUMBERS[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
isVersionGreaterThan(minimumVersion: Array<number> | undefined): boolean {
|
||||
if (minimumVersion === undefined) return true;
|
||||
return this.#VERSION_NUMBERS[lastIndex] >= minimumVersion[lastIndex];
|
||||
}
|
||||
|
||||
const lastIndex = Math.min(this.#VERSION_NUMBERS.length, minimumVersion.length) - 1;
|
||||
for (let i = 0; i < lastIndex; i++) {
|
||||
if (this.#VERSION_NUMBERS[i] > minimumVersion[i]) {
|
||||
return true;
|
||||
} else if (minimumVersion[i] > this.#VERSION_NUMBERS[i]) {
|
||||
return false;
|
||||
}
|
||||
isVersionGreaterThanHook(minimumVersion: Array<number> | undefined): void {
|
||||
const isVersionGreaterThan = this.isVersionGreaterThan.bind(this);
|
||||
before(function () {
|
||||
if (!isVersionGreaterThan(minimumVersion)) {
|
||||
return this.skip();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
testWithClient<
|
||||
M extends RedisModules = {},
|
||||
F extends RedisFunctions = {},
|
||||
S extends RedisScripts = {},
|
||||
RESP extends RespVersions = 2,
|
||||
TYPE_MAPPING extends TypeMapping = {}
|
||||
>(
|
||||
title: string,
|
||||
fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
|
||||
options: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>
|
||||
): void {
|
||||
let dockerPromise: ReturnType<typeof spawnRedisServer>;
|
||||
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
|
||||
const dockerImage = this.#DOCKER_IMAGE;
|
||||
before(function () {
|
||||
this.timeout(30000);
|
||||
|
||||
dockerPromise = spawnRedisServer(dockerImage, options.serverArguments);
|
||||
return dockerPromise;
|
||||
});
|
||||
}
|
||||
|
||||
it(title, async function () {
|
||||
if (!dockerPromise) return this.skip();
|
||||
|
||||
const client = createClient({
|
||||
...options.clientOptions,
|
||||
socket: {
|
||||
...options.clientOptions?.socket,
|
||||
// TODO
|
||||
// @ts-ignore
|
||||
port: (await dockerPromise).port
|
||||
}
|
||||
});
|
||||
|
||||
return this.#VERSION_NUMBERS[lastIndex] >= minimumVersion[lastIndex];
|
||||
}
|
||||
if (options.disableClientSetup) {
|
||||
return fn(client);
|
||||
}
|
||||
|
||||
isVersionGreaterThanHook(minimumVersion: Array<number> | undefined): void {
|
||||
const isVersionGreaterThan = this.isVersionGreaterThan.bind(this);
|
||||
before(function () {
|
||||
if (!isVersionGreaterThan(minimumVersion)) {
|
||||
return this.skip();
|
||||
}
|
||||
});
|
||||
}
|
||||
await client.connect();
|
||||
|
||||
testWithClient<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
>(
|
||||
title: string,
|
||||
fn: (client: RedisClientType<M, F, S>) => unknown,
|
||||
options: ClientTestOptions<M, F, S>
|
||||
): void {
|
||||
let dockerPromise: ReturnType<typeof spawnRedisServer>;
|
||||
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
|
||||
const dockerImage = this.#DOCKER_IMAGE;
|
||||
before(function () {
|
||||
this.timeout(30000);
|
||||
|
||||
dockerPromise = spawnRedisServer(dockerImage, options.serverArguments);
|
||||
return dockerPromise;
|
||||
});
|
||||
try {
|
||||
await client.flushAll();
|
||||
await fn(client);
|
||||
} finally {
|
||||
if (client.isOpen) {
|
||||
await client.flushAll();
|
||||
client.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it(title, async function() {
|
||||
if (!dockerPromise) return this.skip();
|
||||
testWithClientPool<
|
||||
M extends RedisModules = {},
|
||||
F extends RedisFunctions = {},
|
||||
S extends RedisScripts = {},
|
||||
RESP extends RespVersions = 2,
|
||||
TYPE_MAPPING extends TypeMapping = {}
|
||||
>(
|
||||
title: string,
|
||||
fn: (client: RedisClientPoolType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
|
||||
options: ClientPoolTestOptions<M, F, S, RESP, TYPE_MAPPING>
|
||||
): void {
|
||||
let dockerPromise: ReturnType<typeof spawnRedisServer>;
|
||||
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
|
||||
const dockerImage = this.#DOCKER_IMAGE;
|
||||
before(function () {
|
||||
this.timeout(30000);
|
||||
|
||||
const client = RedisClient.create({
|
||||
...options?.clientOptions,
|
||||
socket: {
|
||||
...options?.clientOptions?.socket,
|
||||
port: (await dockerPromise).port
|
||||
}
|
||||
});
|
||||
|
||||
if (options.disableClientSetup) {
|
||||
return fn(client);
|
||||
}
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
await client.flushAll();
|
||||
await fn(client);
|
||||
} finally {
|
||||
if (client.isOpen) {
|
||||
await client.flushAll();
|
||||
await client.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
dockerPromise = spawnRedisServer(dockerImage, options.serverArguments);
|
||||
return dockerPromise;
|
||||
});
|
||||
}
|
||||
|
||||
static async #clusterFlushAll<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
>(cluster: RedisClusterType<M, F, S>): Promise<unknown> {
|
||||
return Promise.all(
|
||||
cluster.masters.map(async ({ client }) => {
|
||||
if (client) {
|
||||
await (await client).flushAll();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
it(title, async function () {
|
||||
if (!dockerPromise) return this.skip();
|
||||
|
||||
testWithCluster<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
>(
|
||||
title: string,
|
||||
fn: (cluster: RedisClusterType<M, F, S>) => unknown,
|
||||
options: ClusterTestOptions<M, F, S>
|
||||
): void {
|
||||
let dockersPromise: ReturnType<typeof spawnRedisCluster>;
|
||||
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
|
||||
const dockerImage = this.#DOCKER_IMAGE;
|
||||
before(function () {
|
||||
this.timeout(30000);
|
||||
|
||||
dockersPromise = spawnRedisCluster({
|
||||
...dockerImage,
|
||||
numberOfMasters: options?.numberOfMasters,
|
||||
numberOfReplicas: options?.numberOfReplicas
|
||||
}, options.serverArguments);
|
||||
return dockersPromise;
|
||||
});
|
||||
const pool = createClientPool({
|
||||
...options.clientOptions,
|
||||
socket: {
|
||||
...options.clientOptions?.socket,
|
||||
// TODO
|
||||
// @ts-ignore
|
||||
port: (await dockerPromise).port
|
||||
}
|
||||
}, options.poolOptions);
|
||||
|
||||
it(title, async function () {
|
||||
if (!dockersPromise) return this.skip();
|
||||
await pool.connect();
|
||||
|
||||
const dockers = await dockersPromise,
|
||||
cluster = RedisCluster.create({
|
||||
rootNodes: dockers.map(({ port }) => ({
|
||||
socket: {
|
||||
port
|
||||
}
|
||||
})),
|
||||
minimizeConnections: true,
|
||||
...options.clusterConfiguration
|
||||
});
|
||||
try {
|
||||
await pool.flushAll();
|
||||
await fn(pool);
|
||||
} finally {
|
||||
await pool.flushAll();
|
||||
pool.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await cluster.connect();
|
||||
static async #clusterFlushAll<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
// POLICIES extends CommandPolicies
|
||||
>(cluster: RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>): Promise<unknown> {
|
||||
return Promise.all(
|
||||
cluster.masters.map(async master => {
|
||||
if (master.client) {
|
||||
await (await cluster.nodeClient(master)).flushAll();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await TestUtils.#clusterFlushAll(cluster);
|
||||
await fn(cluster);
|
||||
} finally {
|
||||
await TestUtils.#clusterFlushAll(cluster);
|
||||
await cluster.disconnect();
|
||||
}
|
||||
});
|
||||
testWithCluster<
|
||||
M extends RedisModules = {},
|
||||
F extends RedisFunctions = {},
|
||||
S extends RedisScripts = {},
|
||||
RESP extends RespVersions = 2,
|
||||
TYPE_MAPPING extends TypeMapping = {}
|
||||
// POLICIES extends CommandPolicies = {}
|
||||
>(
|
||||
title: string,
|
||||
fn: (cluster: RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) => unknown,
|
||||
options: ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>
|
||||
): void {
|
||||
let dockersPromise: ReturnType<typeof spawnRedisCluster>;
|
||||
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
|
||||
const dockerImage = this.#DOCKER_IMAGE;
|
||||
before(function () {
|
||||
this.timeout(30000);
|
||||
|
||||
dockersPromise = spawnRedisCluster({
|
||||
...dockerImage,
|
||||
numberOfMasters: options.numberOfMasters,
|
||||
numberOfReplicas: options.numberOfReplicas
|
||||
}, options.serverArguments);
|
||||
return dockersPromise;
|
||||
});
|
||||
}
|
||||
|
||||
it(title, async function () {
|
||||
if (!dockersPromise) return this.skip();
|
||||
|
||||
const dockers = await dockersPromise,
|
||||
cluster = createCluster({
|
||||
rootNodes: dockers.map(({ port }) => ({
|
||||
socket: {
|
||||
port
|
||||
}
|
||||
})),
|
||||
minimizeConnections: true,
|
||||
...options.clusterConfiguration
|
||||
});
|
||||
|
||||
await cluster.connect();
|
||||
|
||||
try {
|
||||
await TestUtils.#clusterFlushAll(cluster);
|
||||
await fn(cluster);
|
||||
} finally {
|
||||
await TestUtils.#clusterFlushAll(cluster);
|
||||
cluster.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
testAll<
|
||||
M extends RedisModules = {},
|
||||
F extends RedisFunctions = {},
|
||||
S extends RedisScripts = {},
|
||||
RESP extends RespVersions = 2,
|
||||
TYPE_MAPPING extends TypeMapping = {}
|
||||
// POLICIES extends CommandPolicies = {}
|
||||
>(
|
||||
title: string,
|
||||
fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING> | RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) => unknown,
|
||||
options: AllTestOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>
|
||||
) {
|
||||
this.testWithClient(`client.${title}`, fn, options.client);
|
||||
this.testWithCluster(`cluster.${title}`, fn, options.cluster);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user