You've already forked node-redis
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:
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -21,8 +21,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: ['18', '20', '22']
|
||||
redis-version: ['6.2.6-v17', '7.2.0-v13', '7.4.0-v1']
|
||||
node-version: [ '18', '20', '22' ]
|
||||
redis-version: [ 'rs-7.2.0-v13', 'rs-7.4.0-v1', '8.0-M04-pre' ]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
@@ -5,6 +5,7 @@
|
||||
"./packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"test-single": "TS_NODE_PROJECT='./packages/test-utils/tsconfig.json' mocha --require ts-node/register/transpile-only ",
|
||||
"test": "npm run test -ws --if-present",
|
||||
"build": "tsc --build",
|
||||
"documentation": "typedoc --out ./documentation",
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import TestUtils from '@redis/test-utils';
|
||||
import RedisBloomModules from '.';
|
||||
|
||||
export default new TestUtils({
|
||||
dockerImageName: 'redis/redis-stack',
|
||||
dockerImageVersionArgument: 'redisbloom-version',
|
||||
defaultDockerVersion: '7.4.0-v1'
|
||||
export default TestUtils.createFromConfig({
|
||||
dockerImageName: 'redislabs/client-libs-test',
|
||||
dockerImageVersionArgument: 'redis-version',
|
||||
defaultDockerVersion: '8.0-M04-pre'
|
||||
});
|
||||
|
||||
export const GLOBAL = {
|
||||
|
@@ -19,7 +19,6 @@ describe('CONFIG GET', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
testUtils.testWithClient('client.configGet', async client => {
|
||||
const config = await client.configGet('*');
|
||||
@@ -29,4 +28,33 @@ describe('CONFIG GET', () => {
|
||||
assert.equal(typeof value, 'string');
|
||||
}
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('client.configSet.getSearchConfigSettingTest | Redis >= 8', async client => {
|
||||
assert.ok(
|
||||
await client.configGet('search-timeout'),
|
||||
'OK'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('client.configSet.getTSConfigSettingTest | Redis >= 8', async client => {
|
||||
assert.ok(
|
||||
await client.configGet('ts-retention-policy'),
|
||||
'OK'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('client.configSet.getBFConfigSettingTest | Redis >= 8', async client => {
|
||||
assert.ok(
|
||||
await client.configGet('bf-error-rate'),
|
||||
'OK'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('client.configSet.getCFConfigSettingTest | Redis >= 8', async client => {
|
||||
assert.ok(
|
||||
await client.configGet('cf-initial-size'),
|
||||
'OK'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
});
|
||||
|
@@ -30,4 +30,13 @@ describe('CONFIG SET', () => {
|
||||
'OK'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('client.configSet.setReadOnlySearchConfigTest | Redis >= 8',
|
||||
async client => {
|
||||
assert.rejects(
|
||||
client.configSet('search-max-doctablesize', '0'),
|
||||
new Error('ERR CONFIG SET failed (possibly related to argument \'search-max-doctablesize\') - can\'t set immutable config')
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { strict as assert, fail } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import GEOPOS from './GEOPOS';
|
||||
import { parseArgs } from './generic-transformers';
|
||||
@@ -41,12 +41,126 @@ describe('GEOPOS', () => {
|
||||
...coordinates
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
await client.geoPos('key', 'member'),
|
||||
[coordinates]
|
||||
);
|
||||
const result = await client.geoPos('key', 'member');
|
||||
|
||||
/**
|
||||
* - Redis < 8: Returns coordinates with 14 decimal places (e.g., "-122.06429868936539")
|
||||
* - Redis 8+: Returns coordinates with 17 decimal places (e.g., "-122.06429868936538696")
|
||||
*
|
||||
*/
|
||||
const PRECISION = 13; // Number of decimal places to compare
|
||||
|
||||
if (result && result.length === 1 && result[0] != null) {
|
||||
const { longitude, latitude } = result[0];
|
||||
|
||||
assert.ok(
|
||||
compareWithPrecision(longitude, coordinates.longitude, PRECISION),
|
||||
`Longitude mismatch: ${longitude} vs ${coordinates.longitude}`
|
||||
);
|
||||
assert.ok(
|
||||
compareWithPrecision(latitude, coordinates.latitude, PRECISION),
|
||||
`Latitude mismatch: ${latitude} vs ${coordinates.latitude}`
|
||||
);
|
||||
|
||||
} else {
|
||||
assert.fail('Expected a valid result');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareWithPrecision', () => {
|
||||
it('should match exact same numbers', () => {
|
||||
assert.strictEqual(
|
||||
compareWithPrecision('123.456789', '123.456789', 6),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should match when actual has more precision than needed', () => {
|
||||
assert.strictEqual(
|
||||
compareWithPrecision('123.456789123456', '123.456789', 6),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should match when expected has more precision than needed', () => {
|
||||
assert.strictEqual(
|
||||
compareWithPrecision('123.456789', '123.456789123456', 6),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail when decimals differ within precision', () => {
|
||||
assert.strictEqual(
|
||||
compareWithPrecision('123.456689', '123.456789', 6),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle negative numbers', () => {
|
||||
assert.strictEqual(
|
||||
compareWithPrecision('-122.06429868936538', '-122.06429868936539', 13),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail when integer parts differ', () => {
|
||||
assert.strictEqual(
|
||||
compareWithPrecision('124.456789', '123.456789', 6),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle zero decimal places', () => {
|
||||
assert.strictEqual(
|
||||
compareWithPrecision('123.456789', '123.456789', 0),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle numbers without decimal points', () => {
|
||||
assert.strictEqual(
|
||||
compareWithPrecision('123', '123', 6),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle one number without decimal point', () => {
|
||||
assert.strictEqual(
|
||||
compareWithPrecision('123', '123.000', 3),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should match Redis coordinates with different precision', () => {
|
||||
assert.strictEqual(
|
||||
compareWithPrecision(
|
||||
'-122.06429868936538696',
|
||||
'-122.06429868936539',
|
||||
13
|
||||
),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should match Redis latitude with different precision', () => {
|
||||
assert.strictEqual(
|
||||
compareWithPrecision(
|
||||
'37.37749628831998194',
|
||||
'37.37749628831998',
|
||||
14
|
||||
),
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
export const compareWithPrecision = (actual: string, expected: string, decimals: number): boolean => {
|
||||
return Math.abs(Number(actual) - Number(expected)) < Math.pow(10, -decimals);
|
||||
};
|
||||
|
@@ -5,10 +5,10 @@ import { CredentialsProvider } from './authx';
|
||||
import { Command } from './RESP/types';
|
||||
import { BasicCommandParser } from './client/parser';
|
||||
|
||||
const utils = new TestUtils({
|
||||
dockerImageName: 'redis/redis-stack',
|
||||
const utils = TestUtils.createFromConfig({
|
||||
dockerImageName: 'redislabs/client-libs-test',
|
||||
dockerImageVersionArgument: 'redis-version',
|
||||
defaultDockerVersion: '7.4.0-v1'
|
||||
defaultDockerVersion: '8.0-M04-pre'
|
||||
});
|
||||
|
||||
export default utils;
|
||||
|
@@ -3,10 +3,10 @@ import { IdentityProvider, StreamingCredentialsProvider, TokenManager, TokenResp
|
||||
import TestUtils from '@redis/test-utils';
|
||||
import { EntraidCredentialsProvider } from './entraid-credentials-provider';
|
||||
|
||||
export const testUtils = new TestUtils({
|
||||
dockerImageName: 'redis/redis-stack',
|
||||
export const testUtils = TestUtils.createFromConfig({
|
||||
dockerImageName: 'redislabs/client-libs-test',
|
||||
dockerImageVersionArgument: 'redis-version',
|
||||
defaultDockerVersion: '7.4.0-v1'
|
||||
defaultDockerVersion: '8.0-M04-pre'
|
||||
});
|
||||
|
||||
const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ?
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import TestUtils from '@redis/test-utils';
|
||||
import RedisGraph from '.';
|
||||
|
||||
export default new TestUtils({
|
||||
dockerImageName: 'redis/redis-stack',
|
||||
dockerImageVersionArgument: 'redisgraph-version',
|
||||
defaultDockerVersion: '7.4.0-v1'
|
||||
|
||||
export default TestUtils.createFromConfig({
|
||||
dockerImageName: 'redislabs/client-libs-test',
|
||||
dockerImageVersionArgument: 'redis-version',
|
||||
defaultDockerVersion: '8.0-M04-pre'
|
||||
});
|
||||
|
||||
export const GLOBAL = {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import TestUtils from '@redis/test-utils';
|
||||
import RedisJSON from '.';
|
||||
|
||||
export default new TestUtils({
|
||||
dockerImageName: 'redis/redis-stack',
|
||||
dockerImageVersionArgument: 'redisgraph-version',
|
||||
defaultDockerVersion: '7.4.0-v1'
|
||||
export default TestUtils.createFromConfig({
|
||||
dockerImageName: 'redislabs/client-libs-test',
|
||||
dockerImageVersionArgument: 'redis-version',
|
||||
defaultDockerVersion: '8.0-M04-pre'
|
||||
});
|
||||
|
||||
export const GLOBAL = {
|
||||
|
@@ -17,4 +17,40 @@ describe('FT.CONFIG SET', () => {
|
||||
'OK'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'setSearchConfigGloballyTest', async client => {
|
||||
|
||||
const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
|
||||
assert.equal(await client.configSet('search-default-dialect', '3'),
|
||||
'OK', 'CONFIG SET should return OK');
|
||||
|
||||
assert.deepEqual(
|
||||
normalizeObject(await client.configGet('search-default-dialect')),
|
||||
{ 'search-default-dialect': '3' },
|
||||
'CONFIG GET should return 3'
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
normalizeObject(await client.ft.configGet('DEFAULT_DIALECT')),
|
||||
{ 'DEFAULT_DIALECT': '3' },
|
||||
'FT.CONFIG GET should return 3'
|
||||
);
|
||||
|
||||
const ftConfigSetResult = await client.ft.configSet('DEFAULT_DIALECT', '2');
|
||||
assert.equal(normalizeObject(ftConfigSetResult), 'OK', 'FT.CONFIG SET should return OK');
|
||||
|
||||
assert.deepEqual(
|
||||
normalizeObject(await client.ft.configGet('DEFAULT_DIALECT')),
|
||||
{ 'DEFAULT_DIALECT': '2' },
|
||||
'FT.CONFIG GET should return 2'
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
normalizeObject(await client.configGet('search-default-dialect')),
|
||||
{ 'search-default-dialect': '2' },
|
||||
'CONFIG GET should return 22'
|
||||
);
|
||||
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
});
|
||||
|
@@ -5,105 +5,305 @@ import { SCHEMA_FIELD_TYPE } from './CREATE';
|
||||
import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
|
||||
|
||||
describe('INFO', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
parseArgs(INFO, 'index'),
|
||||
['FT.INFO', 'index']
|
||||
);
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
parseArgs(INFO, 'index'),
|
||||
['FT.INFO', 'index']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'client.ft.info', async client => {
|
||||
|
||||
await client.ft.create('index', {
|
||||
field: SCHEMA_FIELD_TYPE.TEXT
|
||||
});
|
||||
const ret = await client.ft.info('index');
|
||||
// effectively testing that stopwords_list is not in ret
|
||||
assert.deepEqual(
|
||||
ret,
|
||||
{
|
||||
index_name: 'index',
|
||||
index_options: [],
|
||||
index_definition: Object.create(null, {
|
||||
|
||||
testUtils.testWithClient('client.ft.info', async client => {
|
||||
await client.ft.create('index', {
|
||||
field: SCHEMA_FIELD_TYPE.TEXT
|
||||
});
|
||||
const ret = await client.ft.info('index');
|
||||
// effectively testing that stopwords_list is not in ret
|
||||
assert.deepEqual(
|
||||
ret,
|
||||
{
|
||||
index_name: 'index',
|
||||
index_options: [],
|
||||
index_definition: Object.create(null, {
|
||||
default_score: {
|
||||
value: '1',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
key_type: {
|
||||
value: 'HASH',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
prefixes: {
|
||||
value: [''],
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
}
|
||||
}),
|
||||
attributes: [Object.create(null, {
|
||||
identifier: {
|
||||
value: 'field',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
attribute: {
|
||||
value: 'field',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
type: {
|
||||
value: 'TEXT',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
WEIGHT: {
|
||||
value: '1',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
}
|
||||
})],
|
||||
num_docs: 0,
|
||||
max_doc_id: 0,
|
||||
num_terms: 0,
|
||||
num_records: 0,
|
||||
inverted_sz_mb: 0,
|
||||
vector_index_sz_mb: 0,
|
||||
total_inverted_index_blocks: 0,
|
||||
offset_vectors_sz_mb: 0,
|
||||
doc_table_size_mb: 0,
|
||||
sortable_values_size_mb: 0,
|
||||
key_table_size_mb: 0,
|
||||
records_per_doc_avg: NaN,
|
||||
bytes_per_record_avg: NaN,
|
||||
cleaning: 0,
|
||||
offsets_per_term_avg: NaN,
|
||||
offset_bits_per_record_avg: NaN,
|
||||
geoshapes_sz_mb: 0,
|
||||
hash_indexing_failures: 0,
|
||||
indexing: 0,
|
||||
percent_indexed: 1,
|
||||
number_of_uses: 1,
|
||||
tag_overhead_sz_mb: 0,
|
||||
text_overhead_sz_mb: 0,
|
||||
total_index_memory_sz_mb: 0,
|
||||
total_indexing_time: 0,
|
||||
gc_stats: {
|
||||
bytes_collected: 0,
|
||||
total_ms_run: 0,
|
||||
total_cycles: 0,
|
||||
average_cycle_time_ms: NaN,
|
||||
last_run_time_ms: 0,
|
||||
gc_numeric_trees_missed: 0,
|
||||
gc_blocks_denied: 0
|
||||
},
|
||||
cursor_stats: {
|
||||
global_idle: 0,
|
||||
global_total: 0,
|
||||
index_capacity: 128,
|
||||
index_total: 0
|
||||
},
|
||||
}
|
||||
);
|
||||
indexes_all: {
|
||||
value: 'false',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
default_score: {
|
||||
value: '1',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
key_type: {
|
||||
value: 'HASH',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
prefixes: {
|
||||
value: [''],
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
}
|
||||
}),
|
||||
attributes: [Object.create(null, {
|
||||
identifier: {
|
||||
value: 'field',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
attribute: {
|
||||
value: 'field',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
type: {
|
||||
value: 'TEXT',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
WEIGHT: {
|
||||
value: '1',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
}
|
||||
})],
|
||||
num_docs: 0,
|
||||
max_doc_id: 0,
|
||||
num_terms: 0,
|
||||
num_records: 0,
|
||||
inverted_sz_mb: 0,
|
||||
vector_index_sz_mb: 0,
|
||||
total_inverted_index_blocks: 0,
|
||||
offset_vectors_sz_mb: 0,
|
||||
doc_table_size_mb: 0,
|
||||
sortable_values_size_mb: 0,
|
||||
key_table_size_mb: 0,
|
||||
records_per_doc_avg: NaN,
|
||||
bytes_per_record_avg: NaN,
|
||||
cleaning: 0,
|
||||
offsets_per_term_avg: NaN,
|
||||
offset_bits_per_record_avg: NaN,
|
||||
geoshapes_sz_mb: 0,
|
||||
hash_indexing_failures: 0,
|
||||
indexing: 0,
|
||||
percent_indexed: 1,
|
||||
number_of_uses: 1,
|
||||
tag_overhead_sz_mb: 0,
|
||||
text_overhead_sz_mb: 0,
|
||||
total_index_memory_sz_mb: 0,
|
||||
total_indexing_time: 0,
|
||||
gc_stats: {
|
||||
bytes_collected: 0,
|
||||
total_ms_run: 0,
|
||||
total_cycles: 0,
|
||||
average_cycle_time_ms: NaN,
|
||||
last_run_time_ms: 0,
|
||||
gc_numeric_trees_missed: 0,
|
||||
gc_blocks_denied: 0
|
||||
},
|
||||
cursor_stats: {
|
||||
global_idle: 0,
|
||||
global_total: 0,
|
||||
index_capacity: 128,
|
||||
index_total: 0
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[7, 4, 2], [7, 4, 2]], 'client.ft.info', async client => {
|
||||
|
||||
await client.ft.create('index', {
|
||||
field: SCHEMA_FIELD_TYPE.TEXT
|
||||
});
|
||||
const ret = await client.ft.info('index');
|
||||
// effectively testing that stopwords_list is not in ret
|
||||
assert.deepEqual(
|
||||
ret,
|
||||
{
|
||||
index_name: 'index',
|
||||
index_options: [],
|
||||
index_definition: Object.create(null, {
|
||||
default_score: {
|
||||
value: '1',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
key_type: {
|
||||
value: 'HASH',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
prefixes: {
|
||||
value: [''],
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
}
|
||||
}),
|
||||
attributes: [Object.create(null, {
|
||||
identifier: {
|
||||
value: 'field',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
attribute: {
|
||||
value: 'field',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
type: {
|
||||
value: 'TEXT',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
WEIGHT: {
|
||||
value: '1',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
}
|
||||
})],
|
||||
num_docs: 0,
|
||||
max_doc_id: 0,
|
||||
num_terms: 0,
|
||||
num_records: 0,
|
||||
inverted_sz_mb: 0,
|
||||
vector_index_sz_mb: 0,
|
||||
total_inverted_index_blocks: 0,
|
||||
offset_vectors_sz_mb: 0,
|
||||
doc_table_size_mb: 0,
|
||||
sortable_values_size_mb: 0,
|
||||
key_table_size_mb: 0,
|
||||
records_per_doc_avg: NaN,
|
||||
bytes_per_record_avg: NaN,
|
||||
cleaning: 0,
|
||||
offsets_per_term_avg: NaN,
|
||||
offset_bits_per_record_avg: NaN,
|
||||
geoshapes_sz_mb: 0,
|
||||
hash_indexing_failures: 0,
|
||||
indexing: 0,
|
||||
percent_indexed: 1,
|
||||
number_of_uses: 1,
|
||||
tag_overhead_sz_mb: 0,
|
||||
text_overhead_sz_mb: 0,
|
||||
total_index_memory_sz_mb: 0,
|
||||
total_indexing_time: 0,
|
||||
gc_stats: {
|
||||
bytes_collected: 0,
|
||||
total_ms_run: 0,
|
||||
total_cycles: 0,
|
||||
average_cycle_time_ms: NaN,
|
||||
last_run_time_ms: 0,
|
||||
gc_numeric_trees_missed: 0,
|
||||
gc_blocks_denied: 0
|
||||
},
|
||||
cursor_stats: {
|
||||
global_idle: 0,
|
||||
global_total: 0,
|
||||
index_capacity: 128,
|
||||
index_total: 0
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[7, 2, 0], [7, 2, 0]], 'client.ft.info', async client => {
|
||||
|
||||
await client.ft.create('index', {
|
||||
field: SCHEMA_FIELD_TYPE.TEXT
|
||||
});
|
||||
const ret = await client.ft.info('index');
|
||||
// effectively testing that stopwords_list is not in ret
|
||||
assert.deepEqual(
|
||||
ret,
|
||||
{
|
||||
index_name: 'index',
|
||||
index_options: [],
|
||||
index_definition: Object.create(null, {
|
||||
default_score: {
|
||||
value: '1',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
key_type: {
|
||||
value: 'HASH',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
prefixes: {
|
||||
value: [''],
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
}
|
||||
}),
|
||||
attributes: [Object.create(null, {
|
||||
identifier: {
|
||||
value: 'field',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
attribute: {
|
||||
value: 'field',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
type: {
|
||||
value: 'TEXT',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
},
|
||||
WEIGHT: {
|
||||
value: '1',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
}
|
||||
})],
|
||||
num_docs: "0",
|
||||
max_doc_id: "0",
|
||||
num_terms: "0",
|
||||
num_records: "0",
|
||||
inverted_sz_mb: 0,
|
||||
vector_index_sz_mb: 0,
|
||||
total_inverted_index_blocks: "0",
|
||||
offset_vectors_sz_mb: 0,
|
||||
doc_table_size_mb: 0,
|
||||
sortable_values_size_mb: 0,
|
||||
key_table_size_mb: 0,
|
||||
records_per_doc_avg: NaN,
|
||||
bytes_per_record_avg: NaN,
|
||||
cleaning: 0,
|
||||
offsets_per_term_avg: NaN,
|
||||
offset_bits_per_record_avg: NaN,
|
||||
geoshapes_sz_mb: 0,
|
||||
hash_indexing_failures: "0",
|
||||
indexing: "0",
|
||||
percent_indexed: 1,
|
||||
number_of_uses: 1,
|
||||
tag_overhead_sz_mb: 0,
|
||||
text_overhead_sz_mb: 0,
|
||||
total_index_memory_sz_mb: 0,
|
||||
total_indexing_time: 0,
|
||||
gc_stats: {
|
||||
bytes_collected: 0,
|
||||
total_ms_run: 0,
|
||||
total_cycles: 0,
|
||||
average_cycle_time_ms: NaN,
|
||||
last_run_time_ms: 0,
|
||||
gc_numeric_trees_missed: 0,
|
||||
gc_blocks_denied: 0
|
||||
},
|
||||
cursor_stats: {
|
||||
global_idle: 0,
|
||||
global_total: 0,
|
||||
index_capacity: 128,
|
||||
index_total: 0
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
@@ -7,43 +7,106 @@ import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
|
||||
import { DEFAULT_DIALECT } from '../dialect/default';
|
||||
|
||||
describe('PROFILE AGGREGATE', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('without options', () => {
|
||||
assert.deepEqual(
|
||||
parseArgs(PROFILE_AGGREGATE, 'index', 'query'),
|
||||
['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT]
|
||||
);
|
||||
});
|
||||
|
||||
it('with options', () => {
|
||||
assert.deepEqual(
|
||||
parseArgs(PROFILE_AGGREGATE, 'index', 'query', {
|
||||
LIMITED: true,
|
||||
VERBATIM: true,
|
||||
STEPS: [{
|
||||
type: FT_AGGREGATE_STEPS.SORTBY,
|
||||
BY: '@by'
|
||||
}]
|
||||
}),
|
||||
['FT.PROFILE', 'index', 'AGGREGATE', 'LIMITED', 'QUERY', 'query',
|
||||
'VERBATIM', 'SORTBY', '1', '@by', 'DIALECT', DEFAULT_DIALECT]
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('without options', () => {
|
||||
assert.deepEqual(
|
||||
parseArgs(PROFILE_AGGREGATE, 'index', 'query'),
|
||||
['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT]
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.ft.search', async client => {
|
||||
await Promise.all([
|
||||
client.ft.create('index', {
|
||||
field: SCHEMA_FIELD_TYPE.NUMERIC
|
||||
}),
|
||||
client.hSet('1', 'field', '1'),
|
||||
client.hSet('2', 'field', '2')
|
||||
]);
|
||||
|
||||
const res = await client.ft.profileAggregate('index', '*');
|
||||
assert.deepEqual('None', res.profile.warning);
|
||||
assert.ok(typeof res.profile.iteratorsProfile.counter === 'number');
|
||||
assert.ok(typeof res.profile.parsingTime === 'string');
|
||||
assert.ok(res.results.total == 1);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
it('with options', () => {
|
||||
assert.deepEqual(
|
||||
parseArgs(PROFILE_AGGREGATE, 'index', 'query', {
|
||||
LIMITED: true,
|
||||
VERBATIM: true,
|
||||
STEPS: [{
|
||||
type: FT_AGGREGATE_STEPS.SORTBY,
|
||||
BY: '@by'
|
||||
}]
|
||||
}),
|
||||
['FT.PROFILE', 'index', 'AGGREGATE', 'LIMITED', 'QUERY', 'query',
|
||||
'VERBATIM', 'SORTBY', '1', '@by', 'DIALECT', DEFAULT_DIALECT]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'client.ft.search', async client => {
|
||||
await Promise.all([
|
||||
client.ft.create('index', {
|
||||
field: SCHEMA_FIELD_TYPE.NUMERIC
|
||||
}),
|
||||
client.hSet('1', 'field', '1'),
|
||||
client.hSet('2', 'field', '2')
|
||||
]);
|
||||
|
||||
|
||||
const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
|
||||
const res = await client.ft.profileAggregate('index', '*');
|
||||
|
||||
const normalizedRes = normalizeObject(res);
|
||||
assert.equal(normalizedRes.results.total, 1);
|
||||
|
||||
assert.ok(normalizedRes.profile[0] === 'Shards');
|
||||
assert.ok(Array.isArray(normalizedRes.profile[1]));
|
||||
assert.ok(normalizedRes.profile[2] === 'Coordinator');
|
||||
assert.ok(Array.isArray(normalizedRes.profile[3]));
|
||||
|
||||
const shardProfile = normalizedRes.profile[1][0];
|
||||
assert.ok(shardProfile.includes('Total profile time'));
|
||||
assert.ok(shardProfile.includes('Parsing time'));
|
||||
assert.ok(shardProfile.includes('Pipeline creation time'));
|
||||
assert.ok(shardProfile.includes('Warning'));
|
||||
assert.ok(shardProfile.includes('Iterators profile'));
|
||||
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[7, 2, 0], [7, 4, 0]], 'client.ft.search', async client => {
|
||||
await Promise.all([
|
||||
client.ft.create('index', {
|
||||
field: SCHEMA_FIELD_TYPE.NUMERIC
|
||||
}),
|
||||
client.hSet('1', 'field', '1'),
|
||||
client.hSet('2', 'field', '2')
|
||||
]);
|
||||
|
||||
const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
|
||||
const res = await client.ft.profileAggregate('index', '*');
|
||||
const normalizedRes = normalizeObject(res);
|
||||
assert.equal(normalizedRes.results.total, 1);
|
||||
|
||||
assert.ok(Array.isArray(normalizedRes.profile));
|
||||
assert.equal(normalizedRes.profile[0][0], 'Total profile time');
|
||||
assert.equal(normalizedRes.profile[1][0], 'Parsing time');
|
||||
assert.equal(normalizedRes.profile[2][0], 'Pipeline creation time');
|
||||
assert.equal(normalizedRes.profile[3][0], 'Warning');
|
||||
assert.equal(normalizedRes.profile[4][0], 'Iterators profile');
|
||||
assert.equal(normalizedRes.profile[5][0], 'Result processors profile');
|
||||
|
||||
const iteratorsProfile = normalizedRes.profile[4][1];
|
||||
assert.equal(iteratorsProfile[0], 'Type');
|
||||
assert.equal(iteratorsProfile[1], 'WILDCARD');
|
||||
assert.equal(iteratorsProfile[2], 'Time');
|
||||
assert.equal(iteratorsProfile[4], 'Counter');
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], '[RESP3] client.ft.search', async client => {
|
||||
await Promise.all([
|
||||
client.ft.create('index', {
|
||||
field: SCHEMA_FIELD_TYPE.NUMERIC
|
||||
}),
|
||||
client.hSet('1', 'field', '1'),
|
||||
client.hSet('2', 'field', '2')
|
||||
]);
|
||||
|
||||
|
||||
const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
|
||||
const res = await client.ft.profileAggregate('index', '*');
|
||||
|
||||
const normalizedRes = normalizeObject(res);
|
||||
assert.equal(normalizedRes.Results.total_results, 1);
|
||||
assert.ok(normalizedRes.Profile.Shards);
|
||||
|
||||
}, GLOBAL.SERVERS.OPEN_3)
|
||||
|
||||
});
|
||||
|
@@ -1,37 +1,35 @@
|
||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||
import { Command, ReplyUnion } from "@redis/client/dist/lib/RESP/types";
|
||||
import AGGREGATE, { AggregateRawReply, FtAggregateOptions, parseAggregateOptions } from "./AGGREGATE";
|
||||
import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from "./PROFILE_SEARCH";
|
||||
import { Command, ReplyUnion, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
|
||||
import AGGREGATE, { AggregateRawReply, FtAggregateOptions, parseAggregateOptions } from './AGGREGATE';
|
||||
import { ProfileOptions, ProfileRawReplyResp2, ProfileReplyResp2, } from './PROFILE_SEARCH';
|
||||
|
||||
export default {
|
||||
NOT_KEYED_COMMAND: true,
|
||||
IS_READ_ONLY: true,
|
||||
parseCommand(
|
||||
parser: CommandParser,
|
||||
index: string,
|
||||
query: string,
|
||||
options?: ProfileOptions & FtAggregateOptions
|
||||
) {
|
||||
parser.push('FT.PROFILE', index, 'AGGREGATE');
|
||||
|
||||
if (options?.LIMITED) {
|
||||
parser.push('LIMITED');
|
||||
IS_READ_ONLY: true,
|
||||
parseCommand(
|
||||
parser: CommandParser,
|
||||
index: string,
|
||||
query: string,
|
||||
options?: ProfileOptions & FtAggregateOptions
|
||||
) {
|
||||
parser.push('FT.PROFILE', index, 'AGGREGATE');
|
||||
|
||||
if (options?.LIMITED) {
|
||||
parser.push('LIMITED');
|
||||
}
|
||||
|
||||
parser.push('QUERY', query);
|
||||
|
||||
parseAggregateOptions(parser, options)
|
||||
},
|
||||
transformReply: {
|
||||
2: (reply: UnwrapReply<ProfileRawReplyResp2<AggregateRawReply>>): ProfileReplyResp2 => {
|
||||
return {
|
||||
results: AGGREGATE.transformReply[2](reply[0]),
|
||||
profile: reply[1]
|
||||
}
|
||||
|
||||
parser.push('QUERY', query);
|
||||
|
||||
parseAggregateOptions(parser, options)
|
||||
},
|
||||
transformReply: {
|
||||
2: (reply: ProfileAggeregateRawReply): ProfileReply => {
|
||||
return {
|
||||
results: AGGREGATE.transformReply[2](reply[0]),
|
||||
profile: transformProfile(reply[1])
|
||||
}
|
||||
},
|
||||
3: undefined as unknown as () => ReplyUnion
|
||||
},
|
||||
unstableResp3: true
|
||||
} as const satisfies Command;
|
||||
|
||||
type ProfileAggeregateRawReply = ProfileRawReply<AggregateRawReply>;
|
||||
3: (reply: ReplyUnion): ReplyUnion => reply
|
||||
},
|
||||
unstableResp3: true
|
||||
} as const satisfies Command;
|
||||
|
@@ -6,39 +6,90 @@ import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
|
||||
import { DEFAULT_DIALECT } from '../dialect/default';
|
||||
|
||||
describe('PROFILE SEARCH', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('without options', () => {
|
||||
assert.deepEqual(
|
||||
parseArgs(PROFILE_SEARCH, 'index', 'query'),
|
||||
['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT]
|
||||
);
|
||||
});
|
||||
|
||||
it('with options', () => {
|
||||
assert.deepEqual(
|
||||
parseArgs(PROFILE_SEARCH, 'index', 'query', {
|
||||
LIMITED: true,
|
||||
VERBATIM: true,
|
||||
INKEYS: 'key'
|
||||
}),
|
||||
['FT.PROFILE', 'index', 'SEARCH', 'LIMITED', 'QUERY', 'query',
|
||||
'VERBATIM', 'INKEYS', '1', 'key', 'DIALECT', DEFAULT_DIALECT]
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('without options', () => {
|
||||
assert.deepEqual(
|
||||
parseArgs(PROFILE_SEARCH, 'index', 'query'),
|
||||
['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT]
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.ft.search', async client => {
|
||||
await Promise.all([
|
||||
client.ft.create('index', {
|
||||
field: SCHEMA_FIELD_TYPE.NUMERIC
|
||||
}),
|
||||
client.hSet('1', 'field', '1')
|
||||
]);
|
||||
|
||||
const res = await client.ft.profileSearch('index', '*');
|
||||
assert.strictEqual('None', res.profile.warning);
|
||||
assert.ok(typeof res.profile.iteratorsProfile.counter === 'number');
|
||||
assert.ok(typeof res.profile.parsingTime === 'string');
|
||||
assert.ok(res.results.total == 1);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
it('with options', () => {
|
||||
assert.deepEqual(
|
||||
parseArgs(PROFILE_SEARCH, 'index', 'query', {
|
||||
LIMITED: true,
|
||||
VERBATIM: true,
|
||||
INKEYS: 'key'
|
||||
}),
|
||||
['FT.PROFILE', 'index', 'SEARCH', 'LIMITED', 'QUERY', 'query',
|
||||
'VERBATIM', 'INKEYS', '1', 'key', 'DIALECT', DEFAULT_DIALECT]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'client.ft.search', async client => {
|
||||
await Promise.all([
|
||||
client.ft.create('index', {
|
||||
field: SCHEMA_FIELD_TYPE.NUMERIC
|
||||
}),
|
||||
client.hSet('1', 'field', '1')
|
||||
]);
|
||||
|
||||
const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
const res = await client.ft.profileSearch('index', '*');
|
||||
|
||||
const normalizedRes = normalizeObject(res);
|
||||
assert.equal(normalizedRes.results.total, 1);
|
||||
|
||||
assert.ok(normalizedRes.profile[0] === 'Shards');
|
||||
assert.ok(Array.isArray(normalizedRes.profile[1]));
|
||||
assert.ok(normalizedRes.profile[2] === 'Coordinator');
|
||||
assert.ok(Array.isArray(normalizedRes.profile[3]));
|
||||
|
||||
const shardProfile = normalizedRes.profile[1][0];
|
||||
assert.ok(shardProfile.includes('Total profile time'));
|
||||
assert.ok(shardProfile.includes('Parsing time'));
|
||||
assert.ok(shardProfile.includes('Pipeline creation time'));
|
||||
assert.ok(shardProfile.includes('Warning'));
|
||||
assert.ok(shardProfile.includes('Iterators profile'));
|
||||
;
|
||||
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[7, 2, 0], [7, 4, 0]], 'client.ft.search', async client => {
|
||||
await Promise.all([
|
||||
client.ft.create('index', {
|
||||
field: SCHEMA_FIELD_TYPE.NUMERIC
|
||||
}),
|
||||
client.hSet('1', 'field', '1')
|
||||
]);
|
||||
|
||||
const normalizeObject = obj => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
const res = await client.ft.profileSearch('index', '*');
|
||||
|
||||
const normalizedRes = normalizeObject(res);
|
||||
assert.equal(normalizedRes.results.total, 1);
|
||||
|
||||
assert.ok(Array.isArray(normalizedRes.profile));
|
||||
assert.equal(normalizedRes.profile[0][0], 'Total profile time');
|
||||
assert.equal(normalizedRes.profile[1][0], 'Parsing time');
|
||||
assert.equal(normalizedRes.profile[2][0], 'Pipeline creation time');
|
||||
assert.equal(normalizedRes.profile[3][0], 'Warning');
|
||||
assert.equal(normalizedRes.profile[4][0], 'Iterators profile');
|
||||
assert.equal(normalizedRes.profile[5][0], 'Result processors profile');
|
||||
|
||||
const iteratorsProfile = normalizedRes.profile[4][1];
|
||||
assert.equal(iteratorsProfile[0], 'Type');
|
||||
assert.equal(iteratorsProfile[1], 'WILDCARD');
|
||||
assert.equal(iteratorsProfile[2], 'Time');
|
||||
assert.equal(iteratorsProfile[4], 'Counter');
|
||||
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
});
|
||||
|
@@ -1,23 +1,19 @@
|
||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||
import { Command, RedisArgument, ReplyUnion } from "@redis/client/dist/lib/RESP/types";
|
||||
import { AggregateReply } from "./AGGREGATE";
|
||||
import SEARCH, { FtSearchOptions, SearchRawReply, SearchReply, parseSearchOptions } from "./SEARCH";
|
||||
import { ArrayReply, Command, RedisArgument, ReplyUnion, TuplesReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
|
||||
import { AggregateReply } from './AGGREGATE';
|
||||
import SEARCH, { FtSearchOptions, SearchRawReply, SearchReply, parseSearchOptions } from './SEARCH';
|
||||
|
||||
export type ProfileRawReply<T> = [
|
||||
results: T,
|
||||
profile: [
|
||||
_: string,
|
||||
TotalProfileTime: string,
|
||||
_: string,
|
||||
ParsingTime: string,
|
||||
_: string,
|
||||
PipelineCreationTime: string,
|
||||
_: string,
|
||||
IteratorsProfile: Array<any>
|
||||
]
|
||||
];
|
||||
export type ProfileRawReplyResp2<T> = TuplesReply<[
|
||||
T,
|
||||
ArrayReply<ReplyUnion>
|
||||
]>;
|
||||
|
||||
type ProfileSearchRawReply = ProfileRawReply<SearchRawReply>;
|
||||
type ProfileSearchResponseResp2 = ProfileRawReplyResp2<SearchRawReply>;
|
||||
|
||||
export interface ProfileReplyResp2 {
|
||||
results: SearchReply | AggregateReply;
|
||||
profile: ReplyUnion;
|
||||
}
|
||||
|
||||
export interface ProfileOptions {
|
||||
LIMITED?: true;
|
||||
@@ -43,108 +39,13 @@ export default {
|
||||
parseSearchOptions(parser, options);
|
||||
},
|
||||
transformReply: {
|
||||
2: (reply: ProfileSearchRawReply, withoutDocuments: boolean): ProfileReply => {
|
||||
2: (reply: UnwrapReply<ProfileSearchResponseResp2>): ProfileReplyResp2 => {
|
||||
return {
|
||||
results: SEARCH.transformReply[2](reply[0]),
|
||||
profile: transformProfile(reply[1])
|
||||
}
|
||||
profile: reply[1]
|
||||
};
|
||||
},
|
||||
3: undefined as unknown as () => ReplyUnion
|
||||
3: (reply: ReplyUnion): ReplyUnion => reply
|
||||
},
|
||||
unstableResp3: true
|
||||
} as const satisfies Command;
|
||||
|
||||
export interface ProfileReply {
|
||||
results: SearchReply | AggregateReply;
|
||||
profile: ProfileData;
|
||||
}
|
||||
|
||||
interface ChildIterator {
|
||||
type?: string,
|
||||
counter?: number,
|
||||
term?: string,
|
||||
size?: number,
|
||||
time?: string,
|
||||
childIterators?: Array<ChildIterator>
|
||||
}
|
||||
|
||||
interface IteratorsProfile {
|
||||
type?: string,
|
||||
counter?: number,
|
||||
queryType?: string,
|
||||
time?: string,
|
||||
childIterators?: Array<ChildIterator>
|
||||
}
|
||||
|
||||
interface ProfileData {
|
||||
totalProfileTime: string,
|
||||
parsingTime: string,
|
||||
pipelineCreationTime: string,
|
||||
warning: string,
|
||||
iteratorsProfile: IteratorsProfile
|
||||
}
|
||||
|
||||
export function transformProfile(reply: Array<any>): ProfileData{
|
||||
return {
|
||||
totalProfileTime: reply[0][1],
|
||||
parsingTime: reply[1][1],
|
||||
pipelineCreationTime: reply[2][1],
|
||||
warning: reply[3][1] ? reply[3][1] : 'None',
|
||||
iteratorsProfile: transformIterators(reply[4][1])
|
||||
};
|
||||
}
|
||||
|
||||
function transformIterators(IteratorsProfile: Array<any>): IteratorsProfile {
|
||||
var res: IteratorsProfile = {};
|
||||
for (let i = 0; i < IteratorsProfile.length; i += 2) {
|
||||
const value = IteratorsProfile[i+1];
|
||||
switch (IteratorsProfile[i]) {
|
||||
case 'Type':
|
||||
res.type = value;
|
||||
break;
|
||||
case 'Counter':
|
||||
res.counter = value;
|
||||
break;
|
||||
case 'Time':
|
||||
res.time = value;
|
||||
break;
|
||||
case 'Query type':
|
||||
res.queryType = value;
|
||||
break;
|
||||
case 'Child iterators':
|
||||
res.childIterators = value.map(transformChildIterators);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function transformChildIterators(IteratorsProfile: Array<any>): ChildIterator {
|
||||
var res: ChildIterator = {};
|
||||
for (let i = 1; i < IteratorsProfile.length; i += 2) {
|
||||
const value = IteratorsProfile[i+1];
|
||||
switch (IteratorsProfile[i]) {
|
||||
case 'Type':
|
||||
res.type = value;
|
||||
break;
|
||||
case 'Counter':
|
||||
res.counter = value;
|
||||
break;
|
||||
case 'Time':
|
||||
res.time = value;
|
||||
break;
|
||||
case 'Size':
|
||||
res.size = value;
|
||||
break;
|
||||
case 'Term':
|
||||
res.term = value;
|
||||
break;
|
||||
case 'Child iterators':
|
||||
res.childIterators = value.map(transformChildIterators);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
@@ -28,12 +28,22 @@ describe('FT.SUGGET', () => {
|
||||
});
|
||||
|
||||
describe('client.ft.sugGet', () => {
|
||||
testUtils.testWithClient('null', async client => {
|
||||
assert.equal(
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => {
|
||||
assert.deepStrictEqual(
|
||||
await client.ft.sugGet('key', 'prefix'),
|
||||
[]
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[6, 2, 0], [7, 4, 0]], 'null', async client => {
|
||||
assert.deepStrictEqual(
|
||||
await client.ft.sugGet('key', 'prefix'),
|
||||
null
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
}, GLOBAL.SERVERS.OPEN)
|
||||
|
||||
testUtils.testWithClient('with suggestions', async client => {
|
||||
const [, reply] = await Promise.all([
|
||||
|
@@ -11,14 +11,21 @@ describe('FT.SUGGET WITHPAYLOADS', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('client.ft.sugGetWithPayloads', () => {
|
||||
testUtils.testWithClient('null', async client => {
|
||||
assert.equal(
|
||||
await client.ft.sugGetWithPayloads('key', 'prefix'),
|
||||
null
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => {
|
||||
assert.deepStrictEqual(
|
||||
await client.ft.sugGetWithPayloads('key', 'prefix'),
|
||||
[]
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[6], [7, 4, 0]], 'null', async client => {
|
||||
assert.deepStrictEqual(
|
||||
await client.ft.sugGetWithPayloads('key', 'prefix'),
|
||||
null
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
describe('with suggestions', () => {
|
||||
testUtils.testWithClient('with suggestions', async client => {
|
||||
const [, reply] = await Promise.all([
|
||||
client.ft.sugAdd('key', 'string', 1, {
|
||||
|
@@ -12,14 +12,15 @@ describe('FT.SUGGET WITHSCORES', () => {
|
||||
});
|
||||
|
||||
describe('client.ft.sugGetWithScores', () => {
|
||||
testUtils.testWithClient('null', async client => {
|
||||
assert.equal(
|
||||
|
||||
testUtils.testWithClientIfVersionWithinRange([[8],'LATEST'], 'null', async client => {
|
||||
assert.deepStrictEqual(
|
||||
await client.ft.sugGetWithScores('key', 'prefix'),
|
||||
null
|
||||
[]
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('with suggestions', async client => {
|
||||
testUtils.testWithClientIfVersionWithinRange([[8],'LATEST'],'with suggestions', async client => {
|
||||
const [, reply] = await Promise.all([
|
||||
client.ft.sugAdd('key', 'string', 1),
|
||||
client.ft.sugGetWithScores('key', 's')
|
||||
|
@@ -12,14 +12,14 @@ describe('FT.SUGGET WITHSCORES WITHPAYLOADS', () => {
|
||||
});
|
||||
|
||||
describe('client.ft.sugGetWithScoresWithPayloads', () => {
|
||||
testUtils.testWithClient('null', async client => {
|
||||
assert.equal(
|
||||
testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => {
|
||||
assert.deepStrictEqual(
|
||||
await client.ft.sugGetWithScoresWithPayloads('key', 'prefix'),
|
||||
null
|
||||
[]
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('with suggestions', async client => {
|
||||
testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'with suggestions', async client => {
|
||||
const [, reply] = await Promise.all([
|
||||
client.ft.sugAdd('key', 'string', 1, {
|
||||
PAYLOAD: 'payload'
|
||||
|
@@ -48,9 +48,21 @@ export default {
|
||||
aliasDel: ALIASDEL,
|
||||
ALIASUPDATE,
|
||||
aliasUpdate: ALIASUPDATE,
|
||||
/**
|
||||
* @deprecated Redis >=8 uses the standard CONFIG command
|
||||
*/
|
||||
CONFIG_GET,
|
||||
/**
|
||||
* @deprecated Redis >=8 uses the standard CONFIG command
|
||||
*/
|
||||
configGet: CONFIG_GET,
|
||||
/**
|
||||
* @deprecated Redis >=8 uses the standard CONFIG command
|
||||
*/
|
||||
CONFIG_SET,
|
||||
/**
|
||||
* @deprecated Redis >=8 uses the standard CONFIG command
|
||||
*/
|
||||
configSet: CONFIG_SET,
|
||||
CREATE,
|
||||
create: CREATE,
|
||||
|
@@ -1,21 +1,32 @@
|
||||
import TestUtils from '@redis/test-utils';
|
||||
import RediSearch from '.';
|
||||
import { RespVersions } from '@redis/client';
|
||||
|
||||
export default new TestUtils({
|
||||
dockerImageName: 'redis/redis-stack',
|
||||
dockerImageVersionArgument: 'redisearch-version',
|
||||
defaultDockerVersion: '7.4.0-v1'
|
||||
export default TestUtils.createFromConfig({
|
||||
dockerImageName: 'redislabs/client-libs-test',
|
||||
dockerImageVersionArgument: 'redis-version',
|
||||
defaultDockerVersion: '8.0-M04-pre'
|
||||
});
|
||||
|
||||
export const GLOBAL = {
|
||||
SERVERS: {
|
||||
OPEN: {
|
||||
serverArguments: [],
|
||||
clientOptions: {
|
||||
modules: {
|
||||
ft: RediSearch
|
||||
}
|
||||
}
|
||||
SERVERS: {
|
||||
OPEN: {
|
||||
serverArguments: [],
|
||||
clientOptions: {
|
||||
modules: {
|
||||
ft: RediSearch
|
||||
}
|
||||
}
|
||||
},
|
||||
OPEN_3: {
|
||||
serverArguments: [],
|
||||
clientOptions: {
|
||||
RESP: 3 as RespVersions,
|
||||
unstableResp3:true,
|
||||
modules: {
|
||||
ft: RediSearch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -9,7 +9,8 @@
|
||||
"!dist/tsconfig.tsbuildinfo"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'"
|
||||
"test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'",
|
||||
"test-sourcemap": "mocha -r ts-node/register/transpile-only './lib/**/*.spec.ts'"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.0.0-next.6"
|
||||
|
@@ -4,9 +4,11 @@ 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 { execFile as execFileCallback } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { exec } from 'node:child_process';
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const execAsync = promisify(execFileCallback);
|
||||
|
||||
interface ErrorWithCode extends Error {
|
||||
code: string;
|
||||
@@ -46,11 +48,29 @@ export interface RedisServerDocker {
|
||||
dockerId: string;
|
||||
}
|
||||
|
||||
async function spawnRedisServerDocker({ image, version }: RedisServerDockerConfig, serverArguments: Array<string>): Promise<RedisServerDocker> {
|
||||
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}`
|
||||
);
|
||||
async function spawnRedisServerDocker({
|
||||
image,
|
||||
version
|
||||
}: RedisServerDockerConfig, serverArguments: Array<string>): Promise<RedisServerDocker> {
|
||||
const port = (await portIterator.next()).value;
|
||||
const portStr = port.toString();
|
||||
|
||||
const dockerArgs = [
|
||||
'run',
|
||||
'-e', `PORT=${portStr}`,
|
||||
'-d',
|
||||
'--network', 'host',
|
||||
`${image}:${version}`,
|
||||
'--port', portStr
|
||||
];
|
||||
|
||||
if (serverArguments.length > 0) {
|
||||
dockerArgs.push(...serverArguments);
|
||||
}
|
||||
|
||||
console.log(`[Docker] Spawning Redis container - Image: ${image}:${version}, Port: ${port}`);
|
||||
|
||||
const { stdout, stderr } = await execAsync('docker', dockerArgs);
|
||||
|
||||
if (!stdout) {
|
||||
throw new Error(`docker run error - ${stderr}`);
|
||||
@@ -65,7 +85,6 @@ async function spawnRedisServerDocker({ image, version }: RedisServerDockerConfi
|
||||
dockerId: stdout.trim()
|
||||
};
|
||||
}
|
||||
|
||||
const RUNNING_SERVERS = new Map<Array<string>, ReturnType<typeof spawnRedisServerDocker>>();
|
||||
|
||||
export function spawnRedisServer(dockerConfig: RedisServerDockerConfig, serverArguments: Array<string>): Promise<RedisServerDocker> {
|
||||
@@ -80,7 +99,7 @@ export function spawnRedisServer(dockerConfig: RedisServerDockerConfig, serverAr
|
||||
}
|
||||
|
||||
async function dockerRemove(dockerId: string): Promise<void> {
|
||||
const { stderr } = await execAsync(`docker rm -f ${dockerId}`);
|
||||
const { stderr } = await execAsync('docker', ['rm', '-f', dockerId]);
|
||||
if (stderr) {
|
||||
throw new Error(`docker rm error - ${stderr}`);
|
||||
}
|
||||
@@ -132,15 +151,15 @@ async function spawnRedisClusterNodeDockers(
|
||||
'5000'
|
||||
], clientConfig).then(async replica => {
|
||||
|
||||
const requirePassIndex = serverArguments.findIndex((x)=>x==='--requirepass');
|
||||
if(requirePassIndex!==-1) {
|
||||
const password = serverArguments[requirePassIndex+1];
|
||||
await replica.client.configSet({'masterauth': password})
|
||||
const requirePassIndex = serverArguments.findIndex((x) => x === '--requirepass');
|
||||
if (requirePassIndex !== -1) {
|
||||
const password = serverArguments[requirePassIndex + 1];
|
||||
await replica.client.configSet({ 'masterauth': password })
|
||||
}
|
||||
await replica.client.clusterMeet('127.0.0.1', master.docker.port);
|
||||
|
||||
while ((await replica.client.clusterSlots()).length === 0) {
|
||||
await setTimeout(50);
|
||||
await setTimeout(25);
|
||||
}
|
||||
|
||||
await replica.client.clusterReplicate(
|
||||
@@ -224,7 +243,7 @@ async function spawnRedisClusterDockers(
|
||||
while (
|
||||
totalNodes(await client.clusterSlots()) !== nodes.length ||
|
||||
!(await client.sendCommand<string>(['CLUSTER', 'INFO'])).startsWith('cluster_state:ok') // TODO
|
||||
) {
|
||||
) {
|
||||
await setTimeout(50);
|
||||
}
|
||||
|
||||
@@ -257,7 +276,7 @@ export function spawnRedisCluster(
|
||||
return runningCluster;
|
||||
}
|
||||
|
||||
const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments,clientConfig);
|
||||
const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments, clientConfig);
|
||||
|
||||
RUNNING_CLUSTERS.set(serverArguments, dockersPromise);
|
||||
return dockersPromise;
|
||||
|
106
packages/test-utils/lib/index.spec.ts
Normal file
106
packages/test-utils/lib/index.spec.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
import TestUtils from './index';
|
||||
|
||||
describe('TestUtils', () => {
|
||||
describe('parseVersionNumber', () => {
|
||||
it('should handle special versions', () => {
|
||||
assert.deepStrictEqual(TestUtils.parseVersionNumber('latest'), [Infinity]);
|
||||
assert.deepStrictEqual(TestUtils.parseVersionNumber('edge'), [Infinity]);
|
||||
});
|
||||
|
||||
it('should parse simple version numbers', () => {
|
||||
assert.deepStrictEqual(TestUtils.parseVersionNumber('7.4.0'), [7, 4, 0]);
|
||||
});
|
||||
|
||||
it('should handle versions with multiple dashes and prefixes', () => {
|
||||
assert.deepStrictEqual(TestUtils.parseVersionNumber('rs-7.4.0-v2'), [7, 4, 0]);
|
||||
assert.deepStrictEqual(TestUtils.parseVersionNumber('rs-7.4.0'), [7, 4, 0]);
|
||||
assert.deepStrictEqual(TestUtils.parseVersionNumber('7.4.0-v2'), [7, 4, 0]);
|
||||
});
|
||||
|
||||
it('should handle various version number formats', () => {
|
||||
assert.deepStrictEqual(TestUtils.parseVersionNumber('10.5'), [10, 5]);
|
||||
assert.deepStrictEqual(TestUtils.parseVersionNumber('8.0.0'), [8, 0, 0]);
|
||||
assert.deepStrictEqual(TestUtils.parseVersionNumber('rs-6.2.4-v1'), [6, 2, 4]);
|
||||
});
|
||||
|
||||
it('should throw TypeError for invalid version strings', () => {
|
||||
['', 'invalid', 'rs-', 'v2', 'rs-invalid-v2'].forEach(version => {
|
||||
assert.throws(
|
||||
() => TestUtils.parseVersionNumber(version),
|
||||
TypeError,
|
||||
`Expected TypeError for version string: ${version}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('Version Comparison', () => {
|
||||
it('should correctly compare versions', () => {
|
||||
const tests: [Array<number>, Array<number>, -1 | 0 | 1][] = [
|
||||
[[1, 0, 0], [1, 0, 0], 0],
|
||||
[[2, 0, 0], [1, 9, 9], 1],
|
||||
[[1, 9, 9], [2, 0, 0], -1],
|
||||
[[1, 2, 3], [1, 2], 1],
|
||||
[[1, 2], [1, 2, 3], -1],
|
||||
[[1, 2, 0], [1, 2, 1], -1],
|
||||
[[1], [1, 0, 0], 0],
|
||||
[[2], [1, 9, 9], 1],
|
||||
];
|
||||
|
||||
tests.forEach(([a, b, expected]) => {
|
||||
|
||||
assert.equal(
|
||||
TestUtils.compareVersions(a, b),
|
||||
expected,
|
||||
`Failed comparing ${a.join('.')} with ${b.join('.')}: expected ${expected}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly compare versions', () => {
|
||||
const tests: [Array<number>, Array<number>, -1 | 0 | 1][] = [
|
||||
[[1, 0, 0], [1, 0, 0], 0],
|
||||
[[2, 0, 0], [1, 9, 9], 1],
|
||||
[[1, 9, 9], [2, 0, 0], -1],
|
||||
[[1, 2, 3], [1, 2], 1],
|
||||
[[1, 2], [1, 2, 3], -1],
|
||||
[[1, 2, 0], [1, 2, 1], -1],
|
||||
[[1], [1, 0, 0], 0],
|
||||
[[2], [1, 9, 9], 1],
|
||||
];
|
||||
|
||||
tests.forEach(([a, b, expected]) => {
|
||||
|
||||
assert.equal(
|
||||
TestUtils.compareVersions(a, b),
|
||||
expected,
|
||||
`Failed comparing ${a.join('.')} with ${b.join('.')}: expected ${expected}`
|
||||
);
|
||||
});
|
||||
})
|
||||
it('isVersionInRange should work correctly', () => {
|
||||
const tests: [Array<number>, Array<number>, Array<number>, boolean][] = [
|
||||
[[7, 0, 0], [7, 0, 0], [7, 0, 0], true],
|
||||
[[7, 0, 1], [7, 0, 0], [7, 0, 2], true],
|
||||
[[7, 0, 0], [7, 0, 1], [7, 0, 2], false],
|
||||
[[7, 0, 3], [7, 0, 1], [7, 0, 2], false],
|
||||
[[7], [6, 0, 0], [8, 0, 0], true],
|
||||
[[7, 1, 1], [7, 1, 0], [7, 1, 2], true],
|
||||
[[6, 0, 0], [7, 0, 0], [8, 0, 0], false],
|
||||
[[9, 0, 0], [7, 0, 0], [8, 0, 0], false]
|
||||
];
|
||||
|
||||
tests.forEach(([version, min, max, expected]) => {
|
||||
const testUtils = new TestUtils({ string: version.join('.'), numbers: version }, "test")
|
||||
assert.equal(
|
||||
testUtils.isVersionInRange(min, max),
|
||||
expected,
|
||||
`Failed checking if ${version.join('.')} is between ${min.join('.')} and ${max.join('.')}: expected ${expected}`
|
||||
);
|
||||
});
|
||||
})
|
||||
});
|
@@ -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 = {},
|
||||
|
@@ -3,6 +3,9 @@
|
||||
"private": true,
|
||||
"main": "./dist/lib/index.js",
|
||||
"types": "./dist/lib/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "*"
|
||||
},
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import TestUtils from '@redis/test-utils';
|
||||
import TimeSeries from '.';
|
||||
|
||||
export default new TestUtils({
|
||||
dockerImageName: 'redis/redis-stack',
|
||||
dockerImageVersionArgument: 'timeseries-version',
|
||||
defaultDockerVersion: '7.4.0-v1'
|
||||
export default TestUtils.createFromConfig({
|
||||
dockerImageName: 'redislabs/client-libs-test',
|
||||
dockerImageVersionArgument: 'redis-version',
|
||||
defaultDockerVersion: '8.0-M04-pre'
|
||||
});
|
||||
|
||||
export const GLOBAL = {
|
||||
|
Reference in New Issue
Block a user