Skip to content

Commit

Permalink
Feat: Scan command implementation (stipsan#334)
Browse files Browse the repository at this point in the history
* scan command implementation

* Chore: Update feature compat table

* rm package-lock

* Chore: Update feature compat table

* more scan tests
  • Loading branch information
DrMegavolt authored and stipsan committed Nov 15, 2017
1 parent 60f1019 commit aae16cd
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 6 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ioredis-mock · [![CircleCI Status](https://img.shields.io/circleci/project/github/stipsan/ioredis-mock.svg?style=flat-square)](https://circleci.com/gh/stipsan/ioredis-mock) [![AppVeyor branch](https://img.shields.io/appveyor/ci/stipsan/ioredis-mock/master.svg?style=flat-square&label=win)](https://ci.appveyor.com/project/stipsan/ioredis-mock) [![npm](https://img.shields.io/npm/dm/ioredis-mock.svg?style=flat-square)](https://npm-stat.com/charts.html?package=ioredis-mock) [![npm version](https://img.shields.io/npm/v/ioredis-mock.svg?style=flat-square)](https://www.npmjs.com/package/ioredis-mock) [![Redis Compatibility: 49%](https://img.shields.io/badge/redis-49%25-yellow.svg?style=flat-square)](compat.md) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release)
# ioredis-mock · [![CircleCI Status](https://img.shields.io/circleci/project/github/stipsan/ioredis-mock.svg?style=flat-square)](https://circleci.com/gh/stipsan/ioredis-mock) [![AppVeyor branch](https://img.shields.io/appveyor/ci/stipsan/ioredis-mock/master.svg?style=flat-square&label=win)](https://ci.appveyor.com/project/stipsan/ioredis-mock) [![npm](https://img.shields.io/npm/dm/ioredis-mock.svg?style=flat-square)](https://npm-stat.com/charts.html?package=ioredis-mock) [![npm version](https://img.shields.io/npm/v/ioredis-mock.svg?style=flat-square)](https://www.npmjs.com/package/ioredis-mock) [![Redis Compatibility: 50%](https://img.shields.io/badge/redis-50%25-yellow.svg?style=flat-square)](compat.md) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release)

This library emulates [ioredis](https://github.com/luin/ioredis) by performing
all operations in-memory. The best way to do integration testing against redis
Expand Down Expand Up @@ -31,11 +31,11 @@ var redis = new Redis({
user_next: '3',
emails: {
'clark@daily.planet': '1',
'bruce@wayne.enterprises': '2'
'bruce@wayne.enterprises': '2',
},
'user:1': { id: '1', username: 'superman', email: 'clark@daily.planet' },
'user:2': { id: '2', username: 'batman', email: 'bruce@wayne.enterprises' }
}
'user:2': { id: '2', username: 'batman', email: 'bruce@wayne.enterprises' },
},
});
// Basically use it just like ioredis
```
Expand Down
4 changes: 2 additions & 2 deletions compat.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Supported commands ![Commands Coverage: 49%](https://img.shields.io/badge/coverage-49%25-yellow.svg)
## Supported commands ![Commands Coverage: 50%](https://img.shields.io/badge/coverage-50%25-yellow.svg)

| redis | ioredis | ioredis-mock |
| --------------------------------------------------------------- | :----------------: | :----------------: |
Expand Down Expand Up @@ -112,7 +112,7 @@
| [rpushx](http://redis.io/commands/RPUSHX) | :white_check_mark: | :white_check_mark: |
| [sadd](http://redis.io/commands/SADD) | :white_check_mark: | :white_check_mark: |
| [save](http://redis.io/commands/SAVE) | :white_check_mark: | :white_check_mark: |
| [scan](http://redis.io/commands/SCAN) | :white_check_mark: | :x: |
| [scan](http://redis.io/commands/SCAN) | :white_check_mark: | :white_check_mark: |
| [scard](http://redis.io/commands/SCARD) | :white_check_mark: | :white_check_mark: |
| [script](http://redis.io/commands/SCRIPT) | :white_check_mark: | :x: |
| [sdiff](http://redis.io/commands/SDIFF) | :white_check_mark: | :white_check_mark: |
Expand Down
1 change: 1 addition & 0 deletions src/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export * from './rpushx';
export * from './sadd';
export * from './save';
export * from './scard';
export * from './scan';
export * from './sdiff';
export * from './set';
export * from './setex';
Expand Down
59 changes: 59 additions & 0 deletions src/commands/scan-command.common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
function pattern(str) {
let string = str.replace(/([+{($^|.\\])/g, '\\$1');
string = string.replace(/(^|[^\\])([*?])/g, '$1.$2');
string = `^${string}$`;

const p = new RegExp(string);
return p.test.bind(p);
}

export function scanHelper(
allKeys,
size,
cursorStart,
opt1Name,
opt1val,
opt2Name,
opt2val
) {
const cursor = parseInt(cursorStart, 10);
if (Number.isNaN(cursor)) throw new Error('Cursor must be integer');

let count = 10;
let matchPattern = null;

const opt1 = (opt1Name || '').toUpperCase();
const opt2 = (opt2Name || '').toUpperCase();
if (opt1 === 'MATCH') {
matchPattern = pattern(opt1val);
if (opt2 === 'COUNT') count = parseInt(opt2val, 10);
else if (opt2) {
throw new Error('BAD Syntax');
}
} else if (opt1 === 'COUNT') {
if (opt2) {
throw new Error('BAD Syntax');
}
count = parseInt(opt1val, 10);
} else if (opt1) {
throw new Error(`Uknown option ${opt1}`);
}

if (Number.isNaN(count)) throw new Error('count must be integer');

let nextCursor = cursor + count;
const keys = allKeys.slice(cursor, nextCursor);

// Apply MATCH filtering _after_ getting number of keys
if (matchPattern) {
let i = 0;
while (i < keys.length)
if (!matchPattern(keys[i])) keys.splice(i, size);
else i += size;
}

// Return 0 when iteration is complete.
if (nextCursor >= allKeys.length) nextCursor = 0;

return [nextCursor, keys];
}
6 changes: 6 additions & 0 deletions src/commands/scan.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { scanHelper } from './scan-command.common';

export function scan(cursor, opt1, opt1val, opt2, opt2val) {
const allKeys = this.data.keys();
return scanHelper(allKeys, 1, cursor, opt1, opt1val, opt2, opt2val);
}
117 changes: 117 additions & 0 deletions test/commands/scan.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import expect from 'expect';
import MockRedis from '../../src';

describe('scan', () => {
it('should return null array if nothing in db', () => {
const redis = new MockRedis();
return redis.scan(0).then(result => {
expect(result[0]).toBe(0);
expect(result[1]).toEqual([]);
});
});

it('should return keys in db', () => {
const redis = new MockRedis({
data: {
foo: 'bar',
test: 'bar',
},
});

return redis.scan(0).then(result => {
expect(result[0]).toBe(0);
expect(result[1]).toEqual(['foo', 'test']);
});
});
it('should return fail if incorrect count', () => {
const redis = new MockRedis();
return redis.scan('asdf').catch(result => {
expect(result).toBeA(Error);
});
});
it('should return fail if incorrect command', () => {
const redis = new MockRedis();
return redis.scan(0, 'ZU').catch(result => {
expect(result).toBeA(Error);
});
});
it('should return fail if incorrect MATCH usage', () => {
const redis = new MockRedis();
return redis.scan(0, 'MATCH', 'sadf', 'ZU').catch(result => {
expect(result).toBeA(Error);
});
});
it('should return fail if incorrect COUNT usage', () => {
const redis = new MockRedis();
return redis.scan(0, 'COUNT', 10, 'ZU').catch(result => {
expect(result).toBeA(Error);
});
});
it('should return fail if incorrect COUNT usage 2', () => {
const redis = new MockRedis();
return redis.scan(0, 'COUNT', 'adsf').catch(result => {
expect(result).toBeA(Error);
});
});
it('should return only mathced keys', () => {
const redis = new MockRedis({
data: {
foo0: 'x',
foo1: 'x',
foo2: 'x',
test0: 'x',
test1: 'x',
},
});

return redis.scan(0, 'MATCH', 'foo*').then(result => {
expect(result[0]).toBe(0);
expect(result[1]).toEqual(['foo0', 'foo1', 'foo2']);
});
});
it('should return only mathced keys and limit by COUNT', () => {
const redis = new MockRedis({
data: {
foo0: 'x',
foo1: 'x',
foo2: 'x',
test0: 'x',
test1: 'x',
},
});

return redis
.scan(0, 'MATCH', 'foo*', 'COUNT', 1)
.then(result => {
expect(result[0]).toBe(1); // more elements left, this is why cursor is not 0
expect(result[1]).toEqual(['foo0']);
return redis.scan(result[0], 'MATCH', 'foo*', 'COUNT', 10);
})
.then(result2 => {
expect(result2[0]).toBe(0);
expect(result2[1]).toEqual(['foo1', 'foo2']);
});
});
it('should return number of keys set by COUNT and continue by cursor', () => {
const redis = new MockRedis({
data: {
foo0: 'x',
foo1: 'x',
test0: 'x',
test1: 'x',
},
});

return redis
.scan(0, 'COUNT', 3)
.then(result => {
expect(result[0]).toBe(3);
expect(result[1]).toEqual(['foo0', 'foo1', 'test0']);
return redis.scan(result[0], 'COUNT', 3);
})
.then(result2 => {
expect(result2[0]).toBe(0);
expect(result2[1]).toEqual(['test1']);
});
});
});

0 comments on commit aae16cd

Please sign in to comment.