From 2b8a2e050b59e6464e9b30227ce75221182bb165 Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Wed, 15 Jun 2011 00:11:27 -0700 Subject: [PATCH] Initial commit of fakeredis This is a very rough initial commit. It only supports the list commands, and omits a lot of error conditions. I eventually plan to add all the types, and handle all the various error conditions. I've also added unittests that can optionally run with the real redis module (and a real redis server) to ensure parity between the real redis client and fake redis. --- COPYING | 24 ++++++ fakeredis.py | 104 +++++++++++++++++++++++++ requirements.txt | 2 + setup.py | 22 ++++++ test_fakeredis.py | 191 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 343 insertions(+) create mode 100644 COPYING create mode 100644 fakeredis.py create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 test_fakeredis.py diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..6723b13 --- /dev/null +++ b/COPYING @@ -0,0 +1,24 @@ +Copyright (c) 2011 James Saryerwinnie +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/fakeredis.py b/fakeredis.py new file mode 100644 index 0000000..8f39e1b --- /dev/null +++ b/fakeredis.py @@ -0,0 +1,104 @@ +import redis + + +class FakeRedis(object): + def __init__(self): + self._db = {} + + def flushdb(self): + pass + + def get(self, name): + return self._db.get(name) + + def set(self, name, value): + self._db[name] = value + + def lpush(self, name, value): + self._db.setdefault(name, []).insert(0, value) + + def lrange(self, name, start, end): + if end == -1: + end = None + else: + end += 1 + return self._db.get(name, [])[start:end] + + def llen(self, name): + return len(self._db.get(name, [])) + + def lrem(self, name, value, count=0): + a_list = self._db.get(name, []) + found = [] + for i, el in enumerate(a_list): + if el == value: + found.append(i) + if count > 0: + indices_to_remove = found[:count] + elif count < 0: + indices_to_remove = found[count:] + else: + indices_to_remove = found + # Iterating in reverse order to ensure the indices + # remain valid during deletion. + for index in reversed(indices_to_remove): + del a_list[index] + return len(indices_to_remove) + + def rpush(self, name, value): + self._db.setdefault(name, []).append(value) + + def lpop(self, name): + try: + return self._db.get(name, []).pop(0) + except IndexError: + return None + + def lset(self, name, index, value): + try: + self._db.get(name, [])[index] = value + except IndexError: + raise redis.ResponseError("index out of range") + + def rpushx(self, name, value): + try: + self._db[name].append(value) + except KeyError: + return + + def ltrim(self, name, start, end): + raise NotImplementedError() + + def lindex(self, name, index): + try: + return self._db.get(name, [])[index] + except IndexError: + return None + + def lpushx(self, name, value): + try: + self._db[name].insert(0, value) + except KeyError: + return + + def rpop(self, name): + try: + return self._db.get(name, []).pop() + except IndexError: + return None + + def linsert(self, name, where, refvalue, value): + index = self._db.get(name, []).index(refvalue) + self._db.get(name, []).insert(index, value) + + def rpoplpush(self, src, dst): + self._db.get(dst, []).insert(0, self._db.get(src).pop()) + + def blpop(self, keys, timeout=0): + raise NotImplementedError() + + def brpop(self, keys, timeout=0): + raise NotImplementedError() + + def brpoplpush(self, src, dst, timeout=0): + raise NotImplementedError() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4ac3aac --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +nose==1.0.0 +redis==2.4.5 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..aa2efb7 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +from setuptools import setup, find_packages + +setup( + name='fakeredis', + version='0.1', + description="Fake implementation of redis API for testing purposes.", + license='BSD', + author='James Saryerwinnie', + author_email='jlsnpi@gmail.com', + modules=['fakeredis'], + entry_points={ + 'console_scripts': ['lmsh = labmanager.shell:main'], + }, + classifiers=[ + 'Development Status :: 3 - Alpha' + 'License :: OSI Approved :: BSD License', + ], + install_requires=[ + 'redis', + ] +) + diff --git a/test_fakeredis.py b/test_fakeredis.py new file mode 100644 index 0000000..174fceb --- /dev/null +++ b/test_fakeredis.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python + +import unittest +import fakeredis +import redis + + +class TestFakeRedis(unittest.TestCase): + def setUp(self): + self.redis = self.create_redis() + + def tearDown(self): + self.redis.flushdb() + + def create_redis(self): + return fakeredis.FakeRedis() + + def test_set_then_get(self): + self.redis.set('foo', 'bar') + self.assertEqual(self.redis.get('foo'), 'bar') + + def test_get_does_not_exist(self): + self.assertEqual(self.redis.get('foo'), None) + + ## Tests for the list type. + + def test_lpush_then_lrange_all(self): + self.redis.lpush('foo', 'bar') + self.redis.lpush('foo', 'baz') + self.assertEqual(self.redis.lrange('foo', 0, -1), ['baz', 'bar']) + + def test_lpush_then_lrange_portion(self): + self.redis.lpush('foo', 'one') + self.redis.lpush('foo', 'two') + self.redis.lpush('foo', 'three') + self.redis.lpush('foo', 'four') + self.assertEqual(self.redis.lrange('foo', 0, 2), + ['four', 'three', 'two']) + self.assertEqual(self.redis.lrange('foo', 0, 3), + ['four', 'three', 'two', 'one']) + + def test_lpush_key_does_not_exist(self): + self.assertEqual(self.redis.lrange('foo', 0, -1), []) + + def test_llen(self): + self.redis.lpush('foo', 'one') + self.redis.lpush('foo', 'two') + self.redis.lpush('foo', 'three') + self.assertEqual(self.redis.llen('foo'), 3) + + def test_llen_no_exist(self): + self.assertEqual(self.redis.llen('foo'), 0) + + def test_lrem_postitive_count(self): + self.redis.lpush('foo', 'same') + self.redis.lpush('foo', 'same') + self.redis.lpush('foo', 'different') + self.redis.lrem('foo', 'same', 2) + self.assertEqual(self.redis.lrange('foo', 0, -1), ['different']) + + def test_lrem_negative_count(self): + self.redis.lpush('foo', 'removeme') + self.redis.lpush('foo', 'three') + self.redis.lpush('foo', 'two') + self.redis.lpush('foo', 'one') + self.redis.lpush('foo', 'removeme') + self.redis.lrem('foo', 'removeme', -1) + # Should remove it from the end of the list, + # leaving the 'removeme' from the front of the list alone. + self.assertEqual(self.redis.lrange('foo', 0, -1), + ['removeme', 'one', 'two', 'three']) + + def test_lrem_zero_count(self): + self.redis.lpush('foo', 'one') + self.redis.lpush('foo', 'one') + self.redis.lpush('foo', 'one') + self.redis.lrem('foo', 'one', 0) + self.assertEqual(self.redis.lrange('foo', 0, -1), []) + + def test_lrem_default_value(self): + self.redis.lpush('foo', 'one') + self.redis.lpush('foo', 'one') + self.redis.lpush('foo', 'one') + self.redis.lrem('foo', 'one') + self.assertEqual(self.redis.lrange('foo', 0, -1), []) + + def test_lrem_does_not_exist(self): + self.redis.lpush('foo', 'one') + self.redis.lrem('foo', 'one') + # These should be noops. + self.redis.lrem('foo', 'one', -2) + self.redis.lrem('foo', 'one', 2) + + def test_lrem_return_value(self): + self.redis.lpush('foo', 'one') + count = self.redis.lrem('foo', 'one') + self.assertEqual(count, 1) + self.assertEqual(self.redis.lrem('foo', 'one'), 0) + + def test_rpush(self): + self.redis.rpush('foo', 'one') + self.redis.rpush('foo', 'two') + self.redis.rpush('foo', 'three') + self.assertEqual(self.redis.lrange('foo', 0, -1), + ['one', 'two', 'three']) + + def test_lpop(self): + self.redis.rpush('foo', 'one') + self.redis.rpush('foo', 'two') + self.redis.rpush('foo', 'three') + self.assertEqual(self.redis.lpop('foo'), 'one') + self.assertEqual(self.redis.lpop('foo'), 'two') + self.assertEqual(self.redis.lpop('foo'), 'three') + + def test_lpop_empty_list(self): + self.redis.rpush('foo', 'one') + self.redis.lpop('foo') + self.assertEqual(self.redis.lpop('foo'), None) + # Verify what happens if we try to pop from a key + # we've never seen before. + self.assertEqual(self.redis.lpop('noexists'), None) + + def test_lset(self): + self.redis.rpush('foo', 'one') + self.redis.rpush('foo', 'two') + self.redis.rpush('foo', 'three') + self.redis.lset('foo', 0, 'four') + self.redis.lset('foo', -2, 'five') + self.assertEqual(self.redis.lrange('foo', 0, -1), + ['four', 'five', 'three']) + + def test_lset_index_out_of_range(self): + self.redis.rpush('foo', 'one') + with self.assertRaises(redis.ResponseError): + self.redis.lset('foo', 3, 'three') + + def test_rpushx(self): + self.redis.rpush('foo', 'one') + self.redis.rpushx('foo', 'two') + self.redis.rpushx('bar', 'three') + self.assertEqual(self.redis.lrange('foo', 0, -1), ['one', 'two']) + self.assertEqual(self.redis.lrange('bar', 0, -1), []) + + def test_lindex(self): + self.redis.rpush('foo', 'one') + self.redis.rpush('foo', 'two') + self.assertEqual(self.redis.lindex('foo', 0), 'one') + self.assertEqual(self.redis.lindex('foo', 4), None) + self.assertEqual(self.redis.lindex('bar', 4), None) + + def test_lpushx(self): + self.redis.lpush('foo', 'two') + self.redis.lpushx('foo', 'one') + self.redis.lpushx('bar', 'one') + self.assertEqual(self.redis.lrange('foo', 0, -1), ['one', 'two']) + self.assertEqual(self.redis.lrange('bar', 0, -1), []) + + def test_rpop(self): + self.redis.rpush('foo', 'one') + self.redis.rpush('foo', 'two') + self.assertEqual(self.redis.rpop('foo'), 'two') + self.assertEqual(self.redis.rpop('foo'), 'one') + self.assertEqual(self.redis.rpop('foo'), None) + + def test_linsert(self): + self.redis.rpush('foo', 'hello') + self.redis.rpush('foo', 'world') + self.redis.linsert('foo', 'before', 'world', 'there') + self.assertEqual(self.redis.lrange('foo', 0, -1), + ['hello', 'there', 'world']) + + def test_rpoplpush(self): + self.redis.rpush('foo', 'one') + self.redis.rpush('foo', 'two') + self.redis.rpush('bar', 'one') + + self.redis.rpoplpush('foo', 'bar') + self.assertEqual(self.redis.lrange('foo', 0, -1), ['one']) + self.assertEqual(self.redis.lrange('bar', 0, -1), ['two', 'one']) + + +class TestRealRedis(TestFakeRedis): + integration = True + + def create_redis(self): + # Using db=10 in the hopes that it's not commonly used. + return redis.Redis('localhost', port=6379, db=10) + + +if __name__ == '__main__': + unittest.main()