redis-mock
Version:
Redis client mock object for unit testing
665 lines (572 loc) • 17.6 kB
JavaScript
/* eslint complexity: "off", no-continue: "off" */
const helpers = require("../helpers.js");
const Item = require("./item.js");
/**
*** NOT IMPLEMENTED ***
ZLEXCOUNT key min max
Count the number of members in a sorted set between a given lexicographical range
ZRANGEBYLEX key min max [LIMIT offset count]
Return a range of members in a sorted set, by lexicographical range
ZREVRANGEBYLEX key max min [LIMIT offset count]
Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.
ZREMRANGEBYLEX key min max
Remove all members in a sorted set between the given lexicographical range
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
Add multiple sorted sets and store the resulting sorted set in a new key
ZSCAN key cursor [MATCH pattern] [COUNT count]
Incrementally iterate sorted sets elements and associated scores
Also: ZUNIONSTORE / ZINTERSTORE is only partially implemented.
*/
const MAX_SCORE_VALUE = 9007199254740992;
const MIN_SCORE_VALUE = -MAX_SCORE_VALUE;
const mockCallback = helpers.noOpCallback;
const validKeyType = (mockInstance, key, callback) => helpers.validKeyType(mockInstance, key, 'zset', callback);
const initKey = (mockInstance, key) => helpers.initKey(mockInstance, key, Item.createSortedSet);
// delimiter for lexicographically sorting by score & member
const rankedDelimiter = '#';
/*
Returns a sorted set of all the score+members for a key
*/
const getRankedList = function(mockInstance, key) {
// returns a ranked list of items (k)
const items = [];
for (const member in mockInstance.storage[key].value) {
const score = parseFloat(mockInstance.storage[key].value[member]);
items.push([score, score + rankedDelimiter + member]);
}
//first sort by score then alphabetically
items.sort(
function (a, b) {
return a[0] - b[0] || a[1].localeCompare(b[1]);
});
//return the concatinated values
return items.map(function (value, index) {
return value[1];
});
};
/*
getRank (zrank & zrevrank)
*/
const getRank = function(mockInstance, key, member, callback, reversed) {
var len = arguments.length;
if (len <= 3) {
return;
}
if (!callback) {
callback = mockCallback;
}
if (!validKeyType(mockInstance, key, callback)) {
return;
}
initKey(mockInstance, key);
member = Item._stringify(member);
var rank = null;
var ranked = getRankedList(mockInstance, key);
// this is for zrevrank
if (reversed) {
ranked.reverse();
}
for (var i=0, parts, s, m; i < ranked.length; i++) {
parts = ranked[i].split(rankedDelimiter);
s = parts[0];
m = parts.slice(1).join(rankedDelimiter);
if (m === member) {
rank = i;
break;
}
}
helpers.callCallback(callback, null, rank);
};
/*
getRange (zrange & zrevrange)
*/
const getRange = function(mockInstance, key, start, stop, withscores, callback, reversed) {
const len = arguments.length;
if (len < 4) {
return;
}
if ('function' === typeof withscores) {
callback = withscores;
withscores = undefined;
}
if (!callback) {
callback = mockCallback;
}
if (!validKeyType(mockInstance, key, callback)) {
return;
}
initKey(mockInstance, key);
let ranked = getRankedList(mockInstance, key);
// this is for zrevrange
if (reversed) {
ranked.reverse();
}
// convert to string so we can test for inclusive range
start = parseInt(String(start), 10);
stop = parseInt(String(stop), 10);
if (start < 0) {
start = ranked.length + start;
}
if (stop < 0) {
stop = ranked.length + stop;
}
// start must be less then stop
if (start > stop) {
return helpers.callCallback(callback, null, []);
}
// console.log(ranked, start, stop + 1);
// make slice inclusive
ranked = ranked.slice(start, stop + 1);
const range = [];
for (let i = 0, parts, s, score, m; i < ranked.length; i++) {
parts = ranked[i].split(rankedDelimiter);
s = parts[0];
score = parseFloat(s);
m = parts.slice(1).join(rankedDelimiter);
range.push(m);
if (withscores && withscores.toLowerCase() === 'withscores') {
range.push(s);
}
}
helpers.callCallback(callback, null, range);
};
/**
getRangeByScore (zrangebyscore & zrevrangebyscore)
**/
const getRangeByScore = function(
mockInstance,
key,
min,
max,
withscores,
limit,
offset,
count,
callback,
reversed) {
var len = arguments.length;
if (len < 4) {
return;
}
if ('function' === typeof withscores) {
callback = withscores;
withscores = undefined;
}
if ('function' === typeof limit) {
callback = limit;
limit = undefined;
}
if (!callback) {
callback = mockCallback;
}
if (!validKeyType(mockInstance, key, callback)) {
return;
}
initKey(mockInstance, key);
var ranked = getRankedList(mockInstance, key);
if (reversed) {
ranked.reverse();
}
// check for infinity flags
if (min.toString() === '-inf') {
min = MIN_SCORE_VALUE;
}
if (max.toString() === '+inf') {
max = MAX_SCORE_VALUE;
}
// handles the reversed case
if (min.toString() === '+inf') {
min = MAX_SCORE_VALUE;
}
if (max.toString() === '-inf') {
max = MIN_SCORE_VALUE;
}
// convert to string so we can test for inclusive range
min = String(min);
max = String(max);
// ranges inclusive?
var minlt = false;
var maxlt = false;
if (min[0] === '(') {
min = min.substring(1);
minlt = true;
}
if (max[0] === '(') {
max = max.substring(1);
maxlt = true;
}
// convert to float
min = parseFloat(min);
max = parseFloat(max);
// console.log('checkpoint', ranked, min, max, withscores, callback, minlt, maxlt);
var range = [],
mintest,
maxtest;
for (var i=0, parts, s, score, m; i < ranked.length; i++) {
parts = ranked[i].split(rankedDelimiter);
s = parts[0];
score = parseFloat(s);
mintest = (minlt) ? (min < score) : (min <= score);
maxtest = (maxlt) ? (score < max) : (score <= max);
// console.log('test', s, score, mintest, maxtest);
if (!mintest || !maxtest) {
continue;
}
m = parts.slice(1).join(rankedDelimiter);
range.push(m);
if (withscores && withscores.toLowerCase() === 'withscores') {
// score as string
range.push(s);
}
}
// console.log('range', range);
// do we need to slice the out put?
if (limit && limit.toLowerCase() === 'limit' && offset && count) {
offset = parseInt(offset, 10);
count = parseInt(count, 10);
// withscores needs to adjust the offset and count
if (withscores && withscores.toLowerCase() === 'withscores') {
offset *= 2;
count *= 2;
}
range = range.slice(offset, offset + count);
}
helpers.callCallback(callback, null, range);
};
// ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
// Add one or more members to a sorted set, or update its score if it already exists
exports.zadd = function(key, flags, scoresAndMembers, callback) {
// init key
initKey(this, key);
// declare opts
const {nx, xx, ch, incr} = flags;
let count = 0;
for (let {score, member} of scoresAndMembers) { // eslint-disable-line prefer-const
const existingScore = this.storage[key].value[member];
const exists = typeof existingScore !== 'undefined';
if ((nx && exists) || (xx && !exists)) {
continue;
}
score = score.toString();
// updating score if memeber doesn't exist
// or if ch = true and score changes
if (!exists || (ch && existingScore != score)) { // eslint-disable-line eqeqeq
count += 1;
}
// do we need to incr (existing score + score)?
if (incr && existingScore) {
score = parseFloat(existingScore) + parseFloat(score);
score = String(score);
}
this.storage[key].value[member] = score;
// only one member is allowed update
// if we have an incr
// this shold behave the same as zincrby
// so return the score instead of the updatedCount;
if (incr) {
count = score;
break;
}
}
helpers.callCallback(callback, null, count);
};
// ZCARD key
// Get the number of members in a sorted set
exports.zcard = function(key, callback) {
const len = arguments.length;
if (len < 1) {
return;
}
if (!callback) {
callback = mockCallback;
}
if (!validKeyType(this, key, callback)) {
return;
}
initKey(this, key);
const count = Object.keys(this.storage[key].value).length;
helpers.callCallback(callback, null, count);
};
// ZCOUNT key min max
// Count the members in a sorted set with scores within the given values
exports.zcount = function(key, min, max, callback) {
const parse = (err, result) => {
if (err) {
return helpers.callCallback(callback, err);
}
helpers.callCallback(callback, null, result.length);
};
this.zrangebyscore(key, min, max, parse);
};
// ZINCRBY key increment member
// Increment the score of a member in a sorted set
exports.zincrby = function(key, increment, member, callback) {
var len = arguments.length;
if (len < 4) {
return;
}
if (!callback) {
callback = mockCallback;
}
if (!validKeyType(this, key, callback)) {
return;
}
initKey(this, key);
member = Item._stringify(member);
var s = this.storage[key].value[member];
var score = parseFloat( s !== undefined ? s : '0');
increment = parseFloat(String(increment));
score += increment;
score = String(score);
this.storage[key].value[member] = score;
helpers.callCallback(callback, null, score);
};
// ZRANGE key start stop [WITHSCORES]
// Return a range of members in a sorted set, by index
exports.zrange = function(key, start, stop, withscores, callback) {
getRange(this, key, start, stop, withscores, callback, false);
};
// ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
// Return a range of members in a sorted set, by score
exports.zrangebyscore = function(
key,
min,
max,
withscores,
limit,
offset,
count,
callback) {
getRangeByScore(
this,
key,
min,
max,
withscores,
limit,
offset,
count,
callback,
false);
};
// ZRANK key member
// Determine the index of a member in a sorted set
exports.zrank = function(key, member, callback) {
getRank(this, key, member, callback, false);
};
// ZREM key member [member ...]
// Remove one or more members from a sorted set
exports.zrem = function(key) {
var len = arguments.length;
if (len <= 2) {
return;
}
var callback = helpers.parseCallback(arguments);
if (!callback) {
callback = mockCallback;
}
if (!validKeyType(this, key, callback)) {
return;
}
initKey(this, key);
// The number of members removed from the sorted set,
// not including non existing members.
var count = 0;
for (var i=1, member; i < len; i++) {
member = arguments[i];
if ('function' === typeof member) {
break;
}
member = Item._stringify(member);
if (this.storage[key].value[member]) {
delete this.storage[key].value[member];
count += 1;
}
}
helpers.callCallback(callback, null, count);
};
// ZREMRANGEBYRANK key start stop
// Remove all members in a sorted set within the given indexes
exports.zremrangebyrank = function(key, start, stop, callback) {
const deleteResults = (err, results) => {
if (err) {
return helpers.callCallback(callback, err);
}
let count = 0;
for (let i = 0, member; i < results.length; i++) {
member = results[i];
if (this.storage[key].value[member]) {
delete this.storage[key].value[member];
count += 1;
}
}
helpers.callCallback(callback, null, count);
};
getRange(this, key, start, stop, deleteResults, false);
};
// ZREMRANGEBYSCORE key min max
// Remove all members in a sorted set within the given scores
exports.zremrangebyscore = function(key, min, max, callback) {
const deleteResults = (err, results) => {
if (err) {
return helpers.callCallback(callback, err);
}
let count = 0;
for (let i = 0, member; i < results.length; i++) {
member = results[i];
if (this.storage[key].value[member]) {
delete this.storage[key].value[member];
count += 1;
}
}
helpers.callCallback(callback, null, count);
};
getRangeByScore(this, key, min, max, deleteResults, false);
};
// ZREVRANGE key start stop [WITHSCORES]
// Return a range of members in a sorted set, by index, with scores ordered from high to low
exports.zrevrange = function(key, start, stop, withscores, callback) {
getRange(this, key, start, stop, withscores, callback, true);
};
// ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
// Return a range of members in a sorted set, by score, with scores ordered from high to low
exports.zrevrangebyscore = function(
key,
max,
min,
withscores,
limit,
offset,
count,
callback) {
getRangeByScore(
this,
key,
min,
max,
withscores,
limit,
offset,
count,
callback,
true);
};
// ZREVRANK key member
// Determine the index of a member in a sorted set, with scores ordered from high to low
exports.zrevrank = function(key, member, callback) {
getRank(this, key, member, callback, true);
};
// ZSCORE key member
// Get the score associated with the given member in a sorted set
exports.zscore = function(key, member, callback) {
const len = arguments.length;
if (len < 2) {
return;
}
if (callback === undefined) {
callback = mockCallback;
}
if (!validKeyType(this, key, callback)) {
return;
}
initKey(this, key);
const score = this.storage[key].value[Item._stringify(member)];
helpers.callCallback(callback, null, (score === undefined ? null : score));
};
// ZUNIONSTORE key argcount member, members...
exports.zunionstore = function(destination, numKeys) {
if (arguments.length < 2) {
return;
}
// Callback function (last arg)
const c = arguments[arguments.length - 1];
const callback = typeof c === 'function' && c || mockCallback;
// Parse arguments
const srcKeys = Array.from(arguments).slice(2, 2 + Number(numKeys));
// Print out warning if bad keys were passed in
if (srcKeys.length === 0) {
console.warn('Warning: No keys passed in to ZUNIONSTORE'); // eslint-disable-line no-console
}
if(srcKeys.some((key) => !key)) {
console.warn('Warning: Undefined or null key(s) provided to ZUNIONSTORE:', srcKeys); // eslint-disable-line no-console
}
let sourcesProcessed = 0;
srcKeys.forEach((srcKey) => {
getRange(this, srcKey, 0, -1, 'withscores', (err, srcVals) => {
let srcItemsProcessed = 0;
// Did we select an empty source?
if (!srcVals || srcVals.length === 0) {
sourcesProcessed++;
// Done with all sources?
if (sourcesProcessed === srcKeys.length) {
initKey(this, destination);
helpers.callCallback(callback, null, Object.keys(this.storage[destination].value).length);
return;
}
}
// Add items one-by-one (because value / score order is flipped on zadd vs. zrange)
for(let i = 0; i < (srcVals.length -1); i = i+2) {
// score member
this.zadd(destination, {}, [{score: srcVals[i+1], member: srcVals[i]}]);
srcItemsProcessed++;
// Done with all items in this source?
if (srcItemsProcessed === srcVals.length / 2) {
sourcesProcessed++;
}
// Done with all sources?
if (sourcesProcessed === srcKeys.length) {
initKey(this, destination);
helpers.callCallback(callback, null, Object.keys(this.storage[destination].value).length);
}
}
});
});
// TODO: Support: [WEIGHTS weight [weight ...]]
// TODO: Support: [AGGREGATE SUM|MIN|MAX]
};
/* Is the provided prop present in all provided objects? */
const allObjsHaveKey = function(prop, objs) {
return objs.every( function(o) {
return !!o[prop];
});
};
/* Sum of the given prop, as found in all the given objects */
var sumPropInObjs = function(prop, objs) {
return objs.reduce( function(sum, o) {
return sum + Number(o[prop] || '0');
}, 0);
};
// ZINTERSTORE key argcount member, members...
exports.zinterstore = function(destination, numKeys) {
if (arguments.length < 2) {
return;
}
// Callback function (last arg)
const c = arguments[arguments.length - 1];
const callback = typeof c === 'function' && c || mockCallback;
// Parse arguments
const srcKeys = Array.from(arguments).slice(2, 2 + Number(numKeys));
// Destination storage
const dest = {}; // Key -> Score mapping
// Source keys storage (filtering out non-existent ones)
const sources = srcKeys
.map((srcKey) => this.storage[srcKey] ? this.storage[srcKey].value : null)
.filter((src) => !!src);
// Compute intersection (inefficiently)
sources.forEach((source) => {
Object.keys(source).forEach((key) => {
if (allObjsHaveKey(key, sources)) {
dest[key] = String(sumPropInObjs(key, sources));
}
});
});
// Store results
initKey(this, destination);
const destValues = Object.keys(dest);
destValues.forEach((value) => {
this.zadd(destination, {}, [{score: dest[value], member: value}]);
});
helpers.callCallback(callback, null, destValues.length);
// TODO: Support: [WEIGHTS weight [weight ...]]
// TODO: Support: [AGGREGATE SUM|MIN|MAX]
};