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

refactor!: redis 8 compatibility improvements and test infrastructure updates (#2893)

* churn(test): use redislabs/client-libs-test for testing

This  switches our testing infrastructure from redis/redis-stack to
redislabs/client-libs-test Docker image across all packages. This change
also updates the default Docker version from 7.4.0-v1 to 8.0-M04-pre.

* churn(test): verify CONFIG SET / GET compatibility with Redis 8

- Add tests for Redis 8 search configuration settings
- Deprecate Redis Search CONFIG commands in favor of standard CONFIG
- Test read-only config restrictions for Redis 8

* churn(test): handle Redis 8 coordinate precision in GEOPOS

- Update GEOPOS tests to handle increased precision in Redis 8 (17 decimal places vs 14)
- Add precision-aware coordinate comparison helper
- Add comprehensive test suite for coordinate comparison function

* test(search): adapt SUGGET tests for Redis 8 empty results

- Update tests to expect empty array ([]) instead of null for SUGGET variants
- Affects sugGet, sugGetWithPayloads, sugGetWithScores, and sugGetWithScoresWithPayloads

* test(search): support Redis 8 INFO indexes_all field

- Add indexes_all field introduced in Redis 8 to index definition test

* refactor!(search): simplify PROFILE commands to return raw response

- BREAKING CHANGE: FT.PROFILE now returns raw response, letting users implement their own parsing

* test: improve version-specific test coverage

- Add `testWithClientIfVersionWithinRange` method to run tests for specific Redis versions
- Refactor TestUtils to handle version comparisons more accurately
- Update test utilities across Redis modules to run tests against multiple versions, and not against latest only
This commit is contained in:
Bobby I.
2025-02-27 10:56:58 +02:00
committed by GitHub
parent 33cdc00746
commit 69d507a572
28 changed files with 1083 additions and 422 deletions

View File

@@ -19,12 +19,38 @@ import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
interface TestUtilsConfig {
/**
* The name of the Docker image to use for spawning Redis test instances.
* This should be a valid Docker image name that contains a Redis server.
*
* @example 'redislabs/client-libs-test'
*/
dockerImageName: string;
/**
* The command-line argument name used to specify the Redis version.
* This argument can be passed when running tests / GH actions.
*
* @example
* If set to 'redis-version', you can run tests with:
* ```bash
* npm test -- --redis-version="6.2"
* ```
*/
dockerImageVersionArgument: string;
/**
* The default Redis version to use if no version is specified via command-line arguments.
* Can be a specific version number (e.g., '6.2'), 'latest', or 'edge'.
* If not provided, defaults to 'latest'.
*
* @optional
* @default 'latest'
*/
defaultDockerVersion?: string;
}
interface CommonTestOptions {
serverArguments: Array<string>;
minimumDockerVersion?: Array<number>;
@@ -83,22 +109,27 @@ interface Version {
}
export default class TestUtils {
static #parseVersionNumber(version: string): Array<number> {
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`);
}
return value;
});
// Match complete version number patterns
const versionMatch = version.match(/(^|\-)\d+(\.\d+)*($|\-)/);
if (!versionMatch) {
throw new TypeError(`${version} is not a valid redis version`);
}
// Extract just the numbers and dots between first and last dash (or start/end)
const versionNumbers = versionMatch[0].replace(/^\-|\-$/g, '');
return versionNumbers.split('.').map(x => {
const value = Number(x);
if (Number.isNaN(value)) {
throw new TypeError(`${version} is not a valid redis version`);
}
return value;
});
}
static #getVersion(argumentName: string, defaultVersion = 'latest'): Version {
return yargs(hideBin(process.argv))
.option(argumentName, {
@@ -108,7 +139,7 @@ export default class TestUtils {
.coerce(argumentName, (version: string) => {
return {
string: version,
numbers: TestUtils.#parseVersionNumber(version)
numbers: TestUtils.parseVersionNumber(version)
};
})
.demandOption(argumentName)
@@ -118,39 +149,76 @@ export default class TestUtils {
readonly #VERSION_NUMBERS: Array<number>;
readonly #DOCKER_IMAGE: RedisServerDockerConfig;
constructor(config: TestUtilsConfig) {
const { string, numbers } = TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion);
constructor({ string, numbers }: Version, dockerImageName: string) {
this.#VERSION_NUMBERS = numbers;
this.#DOCKER_IMAGE = {
image: config.dockerImageName,
image: dockerImageName,
version: string
};
}
/**
* Creates a new TestUtils instance from a configuration object.
*
* @param config - Configuration object containing Docker image and version settings
* @param config.dockerImageName - The name of the Docker image to use for tests
* @param config.dockerImageVersionArgument - The command-line argument name for specifying Redis version
* @param config.defaultDockerVersion - Optional default Redis version if not specified via arguments
* @returns A new TestUtils instance configured with the provided settings
*/
public static createFromConfig(config: TestUtilsConfig) {
return new TestUtils(
TestUtils.#getVersion(config.dockerImageVersionArgument,
config.defaultDockerVersion), config.dockerImageName);
}
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;
}
}
return this.#VERSION_NUMBERS[lastIndex] >= minimumVersion[lastIndex];
return TestUtils.compareVersions(this.#VERSION_NUMBERS, minimumVersion) >= 0;
}
isVersionGreaterThanHook(minimumVersion: Array<number> | undefined): void {
const isVersionGreaterThan = this.isVersionGreaterThan.bind(this);
const isVersionGreaterThanHook = this.isVersionGreaterThan.bind(this);
const versionNumber = this.#VERSION_NUMBERS.join('.');
const minimumVersionString = minimumVersion?.join('.');
before(function () {
if (!isVersionGreaterThan(minimumVersion)) {
if (!isVersionGreaterThanHook(minimumVersion)) {
console.warn(`TestUtils: Version ${versionNumber} is less than minimum version ${minimumVersionString}, skipping test`);
return this.skip();
}
});
}
isVersionInRange(minVersion: Array<number>, maxVersion: Array<number>): boolean {
return TestUtils.compareVersions(this.#VERSION_NUMBERS, minVersion) >= 0 &&
TestUtils.compareVersions(this.#VERSION_NUMBERS, maxVersion) <= 0
}
/**
* Compares two semantic version arrays and returns:
* -1 if version a is less than version b
* 0 if version a equals version b
* 1 if version a is greater than version b
*
* @param a First version array
* @param b Second version array
* @returns -1 | 0 | 1
*/
static compareVersions(a: Array<number>, b: Array<number>): -1 | 0 | 1 {
const maxLength = Math.max(a.length, b.length);
const paddedA = [...a, ...Array(maxLength - a.length).fill(0)];
const paddedB = [...b, ...Array(maxLength - b.length).fill(0)];
for (let i = 0; i < maxLength; i++) {
if (paddedA[i] > paddedB[i]) return 1;
if (paddedA[i] < paddedB[i]) return -1;
}
return 0;
}
testWithClient<
M extends RedisModules = {},
F extends RedisFunctions = {},
@@ -204,6 +272,27 @@ export default class TestUtils {
});
}
testWithClientIfVersionWithinRange<
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}
>(
range: ([minVersion: Array<number>, maxVersion: Array<number>] | [minVersion: Array<number>, 'LATEST']),
title: string,
fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
options: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>
): void {
if (this.isVersionInRange(range[0], range[1] === 'LATEST' ? [Infinity, Infinity, Infinity] : range[1])) {
return this.testWithClient(`${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 = {},
F extends RedisFunctions = {},