aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Sebastiaan Zeeff <[email protected]>2020-09-19 21:48:41 +0200
committerGravatar Sebastiaan Zeeff <[email protected]>2020-09-19 22:14:48 +0200
commitde4a8d960f9f845467efb470e17a0e9685df1bdc (patch)
tree2a981dcd0c23e83b39ca37f512fb53ddeb6e370d
parentUse async-rediscache package for our redis caches (diff)
Remove vestigial RedisCache class definition
As we're now using the `async-rediscache` package, it's no longer necessary to keep the `RedisCache` defined in `bot.utils.redis_cache` around. I've removed the file containing it and the tests written for it. Signed-off-by: Sebastiaan Zeeff <[email protected]>
-rw-r--r--bot/utils/__init__.py3
-rw-r--r--bot/utils/redis_cache.py414
-rw-r--r--tests/bot/utils/test_redis_cache.py265
3 files changed, 1 insertions, 681 deletions
diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py
index 3e93fcb06..60170a88f 100644
--- a/bot/utils/__init__.py
+++ b/bot/utils/__init__.py
@@ -1,5 +1,4 @@
from bot.utils.helpers import CogABCMeta, find_nth_occurrence, pad_base64
-from bot.utils.redis_cache import RedisCache
from bot.utils.services import send_to_paste_service
-__all__ = ['RedisCache', 'CogABCMeta', 'find_nth_occurrence', 'pad_base64', 'send_to_paste_service']
+__all__ = ['CogABCMeta', 'find_nth_occurrence', 'pad_base64', 'send_to_paste_service']
diff --git a/bot/utils/redis_cache.py b/bot/utils/redis_cache.py
deleted file mode 100644
index 52b689b49..000000000
--- a/bot/utils/redis_cache.py
+++ /dev/null
@@ -1,414 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-import logging
-from functools import partialmethod
-from typing import Any, Dict, ItemsView, Optional, Tuple, Union
-
-from bot.bot import Bot
-
-log = logging.getLogger(__name__)
-
-# Type aliases
-RedisKeyType = Union[str, int]
-RedisValueType = Union[str, int, float, bool]
-RedisKeyOrValue = Union[RedisKeyType, RedisValueType]
-
-# Prefix tuples
-_PrefixTuple = Tuple[Tuple[str, Any], ...]
-_VALUE_PREFIXES = (
- ("f|", float),
- ("i|", int),
- ("s|", str),
- ("b|", bool),
-)
-_KEY_PREFIXES = (
- ("i|", int),
- ("s|", str),
-)
-
-
-class NoBotInstanceError(RuntimeError):
- """Raised when RedisCache is created without an available bot instance on the owner class."""
-
-
-class NoNamespaceError(RuntimeError):
- """Raised when RedisCache has no namespace, for example if it is not assigned to a class attribute."""
-
-
-class NoParentInstanceError(RuntimeError):
- """Raised when the parent instance is available, for example if called by accessing the parent class directly."""
-
-
-class RedisCache:
- """
- A simplified interface for a Redis connection.
-
- We implement several convenient methods that are fairly similar to have a dict
- behaves, and should be familiar to Python users. The biggest difference is that
- all the public methods in this class are coroutines, and must be awaited.
-
- Because of limitations in Redis, this cache will only accept strings and integers for keys,
- and strings, integers, floats and booleans for values.
-
- Please note that this class MUST be created as a class attribute, and that that class
- must also contain an attribute with an instance of our Bot. See `__get__` and `__set_name__`
- for more information about how this works.
-
- Simple example for how to use this:
-
- class SomeCog(Cog):
- # To initialize a valid RedisCache, just add it as a class attribute here.
- # Do not add it to the __init__ method or anywhere else, it MUST be a class
- # attribute. Do not pass any parameters.
- cache = RedisCache()
-
- async def my_method(self):
-
- # Now we're ready to use the RedisCache.
- # One thing to note here is that this will not work unless
- # we access self.cache through an _instance_ of this class.
- #
- # For example, attempting to use SomeCog.cache will _not_ work,
- # you _must_ instantiate the class first and use that instance.
- #
- # Now we can store some stuff in the cache just by doing this.
- # This data will persist through restarts!
- await self.cache.set("key", "value")
-
- # To get the data, simply do this.
- value = await self.cache.get("key")
-
- # Other methods work more or less like a dictionary.
- # Checking if something is in the cache
- await self.cache.contains("key")
-
- # iterating the cache
- async for key, value in self.cache.items():
- print(value)
-
- # We can even iterate in a comprehension!
- consumed = [value async for key, value in self.cache.items()]
- """
-
- _namespaces = []
-
- def __init__(self) -> None:
- """Initialize the RedisCache."""
- self._namespace = None
- self.bot = None
- self._increment_lock = None
-
- def _set_namespace(self, namespace: str) -> None:
- """Try to set the namespace, but do not permit collisions."""
- log.trace(f"RedisCache setting namespace to {namespace}")
- self._namespaces.append(namespace)
- self._namespace = namespace
-
- @staticmethod
- def _to_typestring(key_or_value: RedisKeyOrValue, prefixes: _PrefixTuple) -> str:
- """Turn a valid Redis type into a typestring."""
- for prefix, _type in prefixes:
- # Convert bools into integers before storing them.
- if type(key_or_value) is bool:
- bool_int = int(key_or_value)
- return f"{prefix}{bool_int}"
-
- # isinstance is a bad idea here, because isintance(False, int) == True.
- if type(key_or_value) is _type:
- return f"{prefix}{key_or_value}"
-
- raise TypeError(f"RedisCache._to_typestring only supports the following: {prefixes}.")
-
- @staticmethod
- def _from_typestring(key_or_value: Union[bytes, str], prefixes: _PrefixTuple) -> RedisKeyOrValue:
- """Deserialize a typestring into a valid Redis type."""
- # Stuff that comes out of Redis will be bytestrings, so let's decode those.
- if isinstance(key_or_value, bytes):
- key_or_value = key_or_value.decode('utf-8')
-
- # Now we convert our unicode string back into the type it originally was.
- for prefix, _type in prefixes:
- if key_or_value.startswith(prefix):
-
- # For booleans, we need special handling because bool("False") is True.
- if prefix == "b|":
- value = key_or_value[len(prefix):]
- return bool(int(value))
-
- # Otherwise we can just convert normally.
- return _type(key_or_value[len(prefix):])
- raise TypeError(f"RedisCache._from_typestring only supports the following: {prefixes}.")
-
- # Add some nice partials to call our generic typestring converters.
- # These are basically methods that will fill in some of the parameters for you, so that
- # any call to _key_to_typestring will be like calling _to_typestring with the two parameters
- # at `prefixes` and `types_string` pre-filled.
- #
- # See https://docs.python.org/3/library/functools.html#functools.partialmethod
- _key_to_typestring = partialmethod(_to_typestring, prefixes=_KEY_PREFIXES)
- _value_to_typestring = partialmethod(_to_typestring, prefixes=_VALUE_PREFIXES)
- _key_from_typestring = partialmethod(_from_typestring, prefixes=_KEY_PREFIXES)
- _value_from_typestring = partialmethod(_from_typestring, prefixes=_VALUE_PREFIXES)
-
- def _dict_from_typestring(self, dictionary: Dict) -> Dict:
- """Turns all contents of a dict into valid Redis types."""
- return {self._key_from_typestring(key): self._value_from_typestring(value) for key, value in dictionary.items()}
-
- def _dict_to_typestring(self, dictionary: Dict) -> Dict:
- """Turns all contents of a dict into typestrings."""
- return {self._key_to_typestring(key): self._value_to_typestring(value) for key, value in dictionary.items()}
-
- async def _validate_cache(self) -> None:
- """Validate that the RedisCache is ready to be used."""
- if self._namespace is None:
- error_message = (
- "Critical error: RedisCache has no namespace. "
- "This object must be initialized as a class attribute."
- )
- log.error(error_message)
- raise NoNamespaceError(error_message)
-
- if self.bot is None:
- error_message = (
- "Critical error: RedisCache has no `Bot` instance. "
- "This happens when the class RedisCache was created in doesn't "
- "have a Bot instance. Please make sure that you're instantiating "
- "the RedisCache inside a class that has a Bot instance attribute."
- )
- log.error(error_message)
- raise NoBotInstanceError(error_message)
-
- if not self.bot.redis_closed:
- await self.bot.redis_ready.wait()
-
- def __set_name__(self, owner: Any, attribute_name: str) -> None:
- """
- Set the namespace to Class.attribute_name.
-
- Called automatically when this class is constructed inside a class as an attribute.
-
- This class MUST be created as a class attribute in a class, otherwise it will raise
- exceptions whenever a method is used. This is because it uses this method to create
- a namespace like `MyCog.my_class_attribute` which is used as a hash name when we store
- stuff in Redis, to prevent collisions.
- """
- self._set_namespace(f"{owner.__name__}.{attribute_name}")
-
- def __get__(self, instance: RedisCache, owner: Any) -> RedisCache:
- """
- This is called if the RedisCache is a class attribute, and is accessed.
-
- The class this object is instantiated in must contain an attribute with an
- instance of Bot. This is because Bot contains our redis_session, which is
- the mechanism by which we will communicate with the Redis server.
-
- Any attempt to use RedisCache in a class that does not have a Bot instance
- will fail. It is mostly intended to be used inside of a Cog, although theoretically
- it should work in any class that has a Bot instance.
- """
- if self.bot:
- return self
-
- if self._namespace is None:
- error_message = "RedisCache must be a class attribute."
- log.error(error_message)
- raise NoNamespaceError(error_message)
-
- if instance is None:
- error_message = (
- "You must access the RedisCache instance through the cog instance "
- "before accessing it using the cog's class object."
- )
- log.error(error_message)
- raise NoParentInstanceError(error_message)
-
- for attribute in vars(instance).values():
- if isinstance(attribute, Bot):
- self.bot = attribute
- return self
- else:
- error_message = (
- "Critical error: RedisCache has no `Bot` instance. "
- "This happens when the class RedisCache was created in doesn't "
- "have a Bot instance. Please make sure that you're instantiating "
- "the RedisCache inside a class that has a Bot instance attribute."
- )
- log.error(error_message)
- raise NoBotInstanceError(error_message)
-
- def __repr__(self) -> str:
- """Return a beautiful representation of this object instance."""
- return f"RedisCache(namespace={self._namespace!r})"
-
- async def set(self, key: RedisKeyType, value: RedisValueType) -> None:
- """Store an item in the Redis cache."""
- await self._validate_cache()
-
- # Convert to a typestring and then set it
- key = self._key_to_typestring(key)
- value = self._value_to_typestring(value)
-
- log.trace(f"Setting {key} to {value}.")
- await self.bot.redis_session.hset(self._namespace, key, value)
-
- async def get(self, key: RedisKeyType, default: Optional[RedisValueType] = None) -> Optional[RedisValueType]:
- """Get an item from the Redis cache."""
- await self._validate_cache()
- key = self._key_to_typestring(key)
-
- log.trace(f"Attempting to retrieve {key}.")
- value = await self.bot.redis_session.hget(self._namespace, key)
-
- if value is None:
- log.trace(f"Value not found, returning default value {default}")
- return default
- else:
- value = self._value_from_typestring(value)
- log.trace(f"Value found, returning value {value}")
- return value
-
- async def delete(self, key: RedisKeyType) -> None:
- """
- Delete an item from the Redis cache.
-
- If we try to delete a key that does not exist, it will simply be ignored.
-
- See https://redis.io/commands/hdel for more info on how this works.
- """
- await self._validate_cache()
- key = self._key_to_typestring(key)
-
- log.trace(f"Attempting to delete {key}.")
- return await self.bot.redis_session.hdel(self._namespace, key)
-
- async def contains(self, key: RedisKeyType) -> bool:
- """
- Check if a key exists in the Redis cache.
-
- Return True if the key exists, otherwise False.
- """
- await self._validate_cache()
- key = self._key_to_typestring(key)
- exists = await self.bot.redis_session.hexists(self._namespace, key)
-
- log.trace(f"Testing if {key} exists in the RedisCache - Result is {exists}")
- return exists
-
- async def items(self) -> ItemsView:
- """
- Fetch all the key/value pairs in the cache.
-
- Returns a normal ItemsView, like you would get from dict.items().
-
- Keep in mind that these items are just a _copy_ of the data in the
- RedisCache - any changes you make to them will not be reflected
- into the RedisCache itself. If you want to change these, you need
- to make a .set call.
-
- Example:
- items = await my_cache.items()
- for key, value in items:
- # Iterate like a normal dictionary
- """
- await self._validate_cache()
- items = self._dict_from_typestring(
- await self.bot.redis_session.hgetall(self._namespace)
- ).items()
-
- log.trace(f"Retrieving all key/value pairs from cache, total of {len(items)} items.")
- return items
-
- async def length(self) -> int:
- """Return the number of items in the Redis cache."""
- await self._validate_cache()
- number_of_items = await self.bot.redis_session.hlen(self._namespace)
- log.trace(f"Returning length. Result is {number_of_items}.")
- return number_of_items
-
- async def to_dict(self) -> Dict:
- """Convert to dict and return."""
- return {key: value for key, value in await self.items()}
-
- async def clear(self) -> None:
- """Deletes the entire hash from the Redis cache."""
- await self._validate_cache()
- log.trace("Clearing the cache of all key/value pairs.")
- await self.bot.redis_session.delete(self._namespace)
-
- async def pop(self, key: RedisKeyType, default: Optional[RedisValueType] = None) -> RedisValueType:
- """Get the item, remove it from the cache, and provide a default if not found."""
- log.trace(f"Attempting to pop {key}.")
- value = await self.get(key, default)
-
- log.trace(
- f"Attempting to delete item with key '{key}' from the cache. "
- "If this key doesn't exist, nothing will happen."
- )
- await self.delete(key)
-
- return value
-
- async def update(self, items: Dict[RedisKeyType, RedisValueType]) -> None:
- """
- Update the Redis cache with multiple values.
-
- This works exactly like dict.update from a normal dictionary. You pass
- a dictionary with one or more key/value pairs into this method. If the keys
- do not exist in the RedisCache, they are created. If they do exist, the values
- are updated with the new ones from `items`.
-
- Please note that keys and the values in the `items` dictionary
- must consist of valid RedisKeyTypes and RedisValueTypes.
- """
- await self._validate_cache()
- log.trace(f"Updating the cache with the following items:\n{items}")
- await self.bot.redis_session.hmset_dict(self._namespace, self._dict_to_typestring(items))
-
- async def increment(self, key: RedisKeyType, amount: Optional[int, float] = 1) -> None:
- """
- Increment the value by `amount`.
-
- This works for both floats and ints, but will raise a TypeError
- if you try to do it for any other type of value.
-
- This also supports negative amounts, although it would provide better
- readability to use .decrement() for that.
- """
- log.trace(f"Attempting to increment/decrement the value with the key {key} by {amount}.")
-
- # We initialize the lock here, because we need to ensure we get it
- # running on the same loop as the calling coroutine.
- #
- # If we initialized the lock in the __init__, the loop that the coroutine this method
- # would be called from might not exist yet, and so the lock would be on a different
- # loop, which would raise RuntimeErrors.
- if self._increment_lock is None:
- self._increment_lock = asyncio.Lock()
-
- # Since this has several API calls, we need a lock to prevent race conditions
- async with self._increment_lock:
- value = await self.get(key)
-
- # Can't increment a non-existing value
- if value is None:
- error_message = "The provided key does not exist!"
- log.error(error_message)
- raise KeyError(error_message)
-
- # If it does exist, and it's an int or a float, increment and set it.
- if isinstance(value, int) or isinstance(value, float):
- value += amount
- await self.set(key, value)
- else:
- error_message = "You may only increment or decrement values that are integers or floats."
- log.error(error_message)
- raise TypeError(error_message)
-
- async def decrement(self, key: RedisKeyType, amount: Optional[int, float] = 1) -> None:
- """
- Decrement the value by `amount`.
-
- Basically just does the opposite of .increment.
- """
- await self.increment(key, -amount)
diff --git a/tests/bot/utils/test_redis_cache.py b/tests/bot/utils/test_redis_cache.py
deleted file mode 100644
index a2f0fe55d..000000000
--- a/tests/bot/utils/test_redis_cache.py
+++ /dev/null
@@ -1,265 +0,0 @@
-import asyncio
-import unittest
-
-import fakeredis.aioredis
-
-from bot.utils import RedisCache
-from bot.utils.redis_cache import NoBotInstanceError, NoNamespaceError, NoParentInstanceError
-from tests import helpers
-
-
-class RedisCacheTests(unittest.IsolatedAsyncioTestCase):
- """Tests the RedisCache class from utils.redis_dict.py."""
-
- async def asyncSetUp(self): # noqa: N802
- """Sets up the objects that only have to be initialized once."""
- self.bot = helpers.MockBot()
- self.bot.redis_session = await fakeredis.aioredis.create_redis_pool()
-
- # Okay, so this is necessary so that we can create a clean new
- # class for every test method, and we want that because it will
- # ensure we get a fresh loop, which is necessary for test_increment_lock
- # to be able to pass.
- class DummyCog:
- """A dummy cog, for dummies."""
-
- redis = RedisCache()
-
- def __init__(self, bot: helpers.MockBot):
- self.bot = bot
-
- self.cog = DummyCog(self.bot)
-
- await self.cog.redis.clear()
-
- def test_class_attribute_namespace(self):
- """Test that RedisDict creates a namespace automatically for class attributes."""
- self.assertEqual(self.cog.redis._namespace, "DummyCog.redis")
-
- async def test_class_attribute_required(self):
- """Test that errors are raised when not assigned as a class attribute."""
- bad_cache = RedisCache()
- self.assertIs(bad_cache._namespace, None)
-
- with self.assertRaises(RuntimeError):
- await bad_cache.set("test", "me_up_deadman")
-
- async def test_set_get_item(self):
- """Test that users can set and get items from the RedisDict."""
- test_cases = (
- ('favorite_fruit', 'melon'),
- ('favorite_number', 86),
- ('favorite_fraction', 86.54),
- ('favorite_boolean', False),
- ('other_boolean', True),
- )
-
- # Test that we can get and set different types.
- for test in test_cases:
- await self.cog.redis.set(*test)
- self.assertEqual(await self.cog.redis.get(test[0]), test[1])
-
- # Test that .get allows a default value
- self.assertEqual(await self.cog.redis.get('favorite_nothing', "bearclaw"), "bearclaw")
-
- async def test_set_item_type(self):
- """Test that .set rejects keys and values that are not permitted."""
- fruits = ["lemon", "melon", "apple"]
-
- with self.assertRaises(TypeError):
- await self.cog.redis.set(fruits, "nice")
-
- with self.assertRaises(TypeError):
- await self.cog.redis.set(4.23, "nice")
-
- async def test_delete_item(self):
- """Test that .delete allows us to delete stuff from the RedisCache."""
- # Add an item and verify that it gets added
- await self.cog.redis.set("internet", "firetruck")
- self.assertEqual(await self.cog.redis.get("internet"), "firetruck")
-
- # Delete that item and verify that it gets deleted
- await self.cog.redis.delete("internet")
- self.assertIs(await self.cog.redis.get("internet"), None)
-
- async def test_contains(self):
- """Test that we can check membership with .contains."""
- await self.cog.redis.set('favorite_country', "Burkina Faso")
-
- self.assertIs(await self.cog.redis.contains('favorite_country'), True)
- self.assertIs(await self.cog.redis.contains('favorite_dentist'), False)
-
- async def test_items(self):
- """Test that the RedisDict can be iterated."""
- # Set up our test cases in the Redis cache
- test_cases = [
- ('favorite_turtle', 'Donatello'),
- ('second_favorite_turtle', 'Leonardo'),
- ('third_favorite_turtle', 'Raphael'),
- ]
- for key, value in test_cases:
- await self.cog.redis.set(key, value)
-
- # Consume the AsyncIterator into a regular list, easier to compare that way.
- redis_items = [item for item in await self.cog.redis.items()]
-
- # These sequences are probably in the same order now, but probably
- # isn't good enough for tests. Let's not rely on .hgetall always
- # returning things in sequence, and just sort both lists to be safe.
- redis_items = sorted(redis_items)
- test_cases = sorted(test_cases)
-
- # If these are equal now, everything works fine.
- self.assertSequenceEqual(test_cases, redis_items)
-
- async def test_length(self):
- """Test that we can get the correct .length from the RedisDict."""
- await self.cog.redis.set('one', 1)
- await self.cog.redis.set('two', 2)
- await self.cog.redis.set('three', 3)
- self.assertEqual(await self.cog.redis.length(), 3)
-
- await self.cog.redis.set('four', 4)
- self.assertEqual(await self.cog.redis.length(), 4)
-
- async def test_to_dict(self):
- """Test that the .to_dict method returns a workable dictionary copy."""
- copy = await self.cog.redis.to_dict()
- local_copy = {key: value for key, value in await self.cog.redis.items()}
- self.assertIs(type(copy), dict)
- self.assertDictEqual(copy, local_copy)
-
- async def test_clear(self):
- """Test that the .clear method removes the entire hash."""
- await self.cog.redis.set('teddy', 'with me')
- await self.cog.redis.set('in my dreams', 'you have a weird hat')
- self.assertEqual(await self.cog.redis.length(), 2)
-
- await self.cog.redis.clear()
- self.assertEqual(await self.cog.redis.length(), 0)
-
- async def test_pop(self):
- """Test that we can .pop an item from the RedisDict."""
- await self.cog.redis.set('john', 'was afraid')
-
- self.assertEqual(await self.cog.redis.pop('john'), 'was afraid')
- self.assertEqual(await self.cog.redis.pop('pete', 'breakneck'), 'breakneck')
- self.assertEqual(await self.cog.redis.length(), 0)
-
- async def test_update(self):
- """Test that we can .update the RedisDict with multiple items."""
- await self.cog.redis.set("reckfried", "lona")
- await self.cog.redis.set("bel air", "prince")
- await self.cog.redis.update({
- "reckfried": "jona",
- "mega": "hungry, though",
- })
-
- result = {
- "reckfried": "jona",
- "bel air": "prince",
- "mega": "hungry, though",
- }
- self.assertDictEqual(await self.cog.redis.to_dict(), result)
-
- def test_typestring_conversion(self):
- """Test the typestring-related helper functions."""
- conversion_tests = (
- (12, "i|12"),
- (12.4, "f|12.4"),
- ("cowabunga", "s|cowabunga"),
- )
-
- # Test conversion to typestring
- for _input, expected in conversion_tests:
- self.assertEqual(self.cog.redis._value_to_typestring(_input), expected)
-
- # Test conversion from typestrings
- for _input, expected in conversion_tests:
- self.assertEqual(self.cog.redis._value_from_typestring(expected), _input)
-
- # Test that exceptions are raised on invalid input
- with self.assertRaises(TypeError):
- self.cog.redis._value_to_typestring(["internet"])
- self.cog.redis._value_from_typestring("o|firedog")
-
- async def test_increment_decrement(self):
- """Test .increment and .decrement methods."""
- await self.cog.redis.set("entropic", 5)
- await self.cog.redis.set("disentropic", 12.5)
-
- # Test default increment
- await self.cog.redis.increment("entropic")
- self.assertEqual(await self.cog.redis.get("entropic"), 6)
-
- # Test default decrement
- await self.cog.redis.decrement("entropic")
- self.assertEqual(await self.cog.redis.get("entropic"), 5)
-
- # Test float increment with float
- await self.cog.redis.increment("disentropic", 2.0)
- self.assertEqual(await self.cog.redis.get("disentropic"), 14.5)
-
- # Test float increment with int
- await self.cog.redis.increment("disentropic", 2)
- self.assertEqual(await self.cog.redis.get("disentropic"), 16.5)
-
- # Test negative increments, because why not.
- await self.cog.redis.increment("entropic", -5)
- self.assertEqual(await self.cog.redis.get("entropic"), 0)
-
- # Negative decrements? Sure.
- await self.cog.redis.decrement("entropic", -5)
- self.assertEqual(await self.cog.redis.get("entropic"), 5)
-
- # What about if we use a negative float to decrement an int?
- # This should convert the type into a float.
- await self.cog.redis.decrement("entropic", -2.5)
- self.assertEqual(await self.cog.redis.get("entropic"), 7.5)
-
- # Let's test that they raise the right errors
- with self.assertRaises(KeyError):
- await self.cog.redis.increment("doesn't_exist!")
-
- await self.cog.redis.set("stringthing", "stringthing")
- with self.assertRaises(TypeError):
- await self.cog.redis.increment("stringthing")
-
- async def test_increment_lock(self):
- """Test that we can't produce a race condition in .increment."""
- await self.cog.redis.set("test_key", 0)
- tasks = []
-
- # Increment this a lot in different tasks
- for _ in range(100):
- task = asyncio.create_task(
- self.cog.redis.increment("test_key", 1)
- )
- tasks.append(task)
- await asyncio.gather(*tasks)
-
- # Confirm that the value has been incremented the exact right number of times.
- value = await self.cog.redis.get("test_key")
- self.assertEqual(value, 100)
-
- async def test_exceptions_raised(self):
- """Testing that the various RuntimeErrors are reachable."""
- class MyCog:
- cache = RedisCache()
-
- def __init__(self):
- self.other_cache = RedisCache()
-
- cog = MyCog()
-
- # Raises "No Bot instance"
- with self.assertRaises(NoBotInstanceError):
- await cog.cache.get("john")
-
- # Raises "RedisCache has no namespace"
- with self.assertRaises(NoNamespaceError):
- await cog.other_cache.get("was")
-
- # Raises "You must access the RedisCache instance through the cog instance"
- with self.assertRaises(NoParentInstanceError):
- await MyCog.cache.get("afraid")