1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-06 02:15:48 +03:00

use dockers for tests, use npm workspaces, add rejson & redisearch modules, fix some bugs

This commit is contained in:
leibale
2021-11-08 19:21:15 -05:00
parent ecbd5b6968
commit 3eb99dbe83
689 changed files with 5321 additions and 1712 deletions

View File

@@ -0,0 +1,222 @@
import { createConnection } from 'net';
import { once } from 'events';
import { RedisModules, RedisScripts } from '@redis/client/lib/commands';
import RedisClient, { RedisClientType } from '@redis/client/lib/client';
import { promiseTimeout } from '@redis/client/lib/utils';
import path from 'path';
import { promisify } from 'util';
import { exec } from 'child_process';
const execAsync = promisify(exec);
interface ErrorWithCode extends Error {
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;
}
}
return false;
}
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');
})();
export interface RedisServerDockerConfig {
image: string;
version: Array<number>;
}
export interface RedisServerDocker {
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.join('.')} ` +
`--build-arg REDIS_ARGUMENTS="--save --port ${port.toString()} ${serverArguments.join(' ')}"` +
')'
);
if (!stdout) {
throw new Error(`docker run error - ${stderr}`);
}
while (await isPortAvailable(port)) {
await promiseTimeout(500);
}
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 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}`);
}
}
after(() => {
return Promise.all(
[...RUNNING_SERVERS.values()].map(async dockerPromise =>
await dockerRemove((await dockerPromise).dockerId)
)
);
});
export interface RedisClusterDockersConfig extends RedisServerDockerConfig {
numberOfNodes?: number;
}
async function spawnRedisClusterNodeDocker(
dockersConfig: RedisClusterDockersConfig,
serverArguments: Array<string>,
fromSlot: number,
toSlot: number,
waitForState: boolean,
meetPort?: number
): Promise<RedisServerDocker> {
const docker = await spawnRedisServerDocker(dockersConfig, [
...serverArguments,
'--cluster-enabled',
'yes',
'--cluster-node-timeout',
'5000'
]),
client = RedisClient.create({
socket: {
port: docker.port
}
});
await client.connect();
try {
const range = [];
for (let i = fromSlot; i < toSlot; i++) {
range.push(i);
}
const promises: Array<Promise<unknown>> = [client.clusterAddSlots(range)];
if (meetPort) {
promises.push(client.clusterMeet('127.0.0.1', meetPort));
}
if (waitForState) {
promises.push(waitForClusterState(client));
}
await Promise.all(promises);
return docker;
} finally {
await client.disconnect();
}
}
async function waitForClusterState<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>): Promise<void> {
while ((await client.clusterInfo()).state !== 'ok') {
await promiseTimeout(500);
}
}
const SLOTS = 16384;
async function spawnRedisClusterDockers(dockersConfig: RedisClusterDockersConfig, serverArguments: Array<string>): Promise<Array<RedisServerDocker>> {
const numberOfNodes = dockersConfig.numberOfNodes ?? 3,
slotsPerNode = Math.floor(SLOTS / numberOfNodes),
dockers: Array<RedisServerDocker> = [];
for (let i = 0; i < numberOfNodes; i++) {
const fromSlot = i * slotsPerNode,
[ toSlot, waitForState ] = i === numberOfNodes - 1 ? [SLOTS, true] : [fromSlot + slotsPerNode, false];
dockers.push(
await spawnRedisClusterNodeDocker(
dockersConfig,
serverArguments,
fromSlot,
toSlot,
waitForState,
i === 0 ? undefined : dockers[i - 1].port
)
);
}
const client = RedisClient.create({
socket: {
port: dockers[0].port
}
});
await client.connect();
try {
while ((await client.clusterInfo()).state !== 'ok') {
await promiseTimeout(500);
}
} finally {
await client.disconnect();
}
return dockers;
}
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 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))
);
})
);
});

View File

@@ -0,0 +1,177 @@
import { RedisModules, 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 { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
interface TestUtilsConfig<M extends RedisModules, S extends RedisScripts> {
dockerImageName: string;
dockerImageVersionArgument: string;
defaultDockerVersion: string;
defaultClientOptions?: Partial<RedisClientOptions<M, S>>;
defaultClusterOptions?: Partial<RedisClusterOptions<M, S>>;
}
interface CommonTestOptions {
minimumDockerVersion?: Array<number>;
}
interface ClientTestOptions<M extends RedisModules, S extends RedisScripts> extends CommonTestOptions {
serverArguments: Array<string>;
clientOptions?: Partial<RedisClientOptions<M, S>>;
disableClientSetup?: boolean;
}
interface ClusterTestOptions<M extends RedisModules, S extends RedisScripts> extends CommonTestOptions {
serverArguments: Array<string>;
clusterConfiguration?: Partial<RedisClusterOptions<M, S>>;
}
export default class TestUtils<M extends RedisModules, S extends RedisScripts> {
static #getVersion(argumentName: string, defaultVersion: string): Array<number> {
return yargs(hideBin(process.argv))
.option(argumentName, {
type: 'string',
default: defaultVersion
})
.coerce(argumentName, (arg: string) => {
return arg.split('.').map(x => {
const value = Number(x);
if (Number.isNaN(value)) {
throw new TypeError(`${arg} is not a valid redis version`);
}
return value;
});
})
.demandOption(argumentName)
.parseSync()[argumentName];
}
readonly #DOCKER_IMAGE: RedisServerDockerConfig;
constructor(config: TestUtilsConfig<M, S>) {
this.#DOCKER_IMAGE = {
image: config.dockerImageName,
version: TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion)
};
}
isVersionGreaterThan(minimumVersion: Array<number> | undefined): boolean {
if (minimumVersion === undefined) return true;
const lastIndex = Math.min(this.#DOCKER_IMAGE.version.length, minimumVersion.length) - 1;
for (let i = 0; i < lastIndex; i++) {
if (this.#DOCKER_IMAGE.version[i] > minimumVersion[i]) {
return true;
} else if (minimumVersion[i] > this.#DOCKER_IMAGE.version[i]) {
return false;
}
}
return this.#DOCKER_IMAGE.version[lastIndex] >= minimumVersion[lastIndex];
}
isVersionGreaterThanHook(minimumVersion: Array<number> | undefined): void {
const isVersionGreaterThan = this.isVersionGreaterThan.bind(this);
before(function () {
if (!isVersionGreaterThan(minimumVersion)) {
return this.skip();
}
});
}
testWithClient<M extends RedisModules, S extends RedisScripts>(
title: string,
fn: (client: RedisClientType<M, S>) => Promise<unknown>,
options: ClientTestOptions<M, 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;
});
}
it(title, async function() {
if (!dockerPromise) return this.skip();
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();
}
}
});
}
static async #clusterFlushAll<M extends RedisModules, S extends RedisScripts>(cluster: RedisClusterType<M, S>): Promise<void> {
await Promise.all(
cluster.getMasters().map(({ client }) => client.flushAll())
);
}
testWithCluster<M extends RedisModules, S extends RedisScripts>(
title: string,
fn: (cluster: RedisClusterType<M, S>) => Promise<void>,
options: ClusterTestOptions<M, 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, options.serverArguments);
return dockersPromise;
});
}
it(title, async function () {
if (!dockersPromise) return this.skip();
const dockers = await dockersPromise,
cluster = RedisCluster.create({
...options.clusterConfiguration,
rootNodes: dockers.map(({ port }) => ({
socket: {
port
}
}))
});
await cluster.connect();
try {
await TestUtils.#clusterFlushAll(cluster);
await fn(cluster);
} finally {
await TestUtils.#clusterFlushAll(cluster);
await cluster.disconnect();
}
});
}
}