pythonasyncioeventletgeventlong-pollinglow-latencysocket-iosocketiosocketio-serverweb-serverwebsocket
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
6.6 KiB
214 lines
6.6 KiB
import asyncio
|
|
import pytest
|
|
import redis
|
|
import valkey
|
|
import types
|
|
|
|
from socketio import async_redis_manager
|
|
from socketio.async_redis_manager import AsyncRedisManager
|
|
|
|
|
|
class TestAsyncRedisManager:
|
|
def test_redis_not_installed(self):
|
|
saved_redis = async_redis_manager.aioredis
|
|
async_redis_manager.aioredis = None
|
|
|
|
with pytest.raises(RuntimeError):
|
|
AsyncRedisManager('redis://')
|
|
assert AsyncRedisManager('unix:///var/sock/redis.sock') is not None
|
|
|
|
async_redis_manager.aioredis = saved_redis
|
|
|
|
def test_valkey_not_installed(self):
|
|
saved_valkey = async_redis_manager.aiovalkey
|
|
async_redis_manager.aiovalkey = None
|
|
|
|
with pytest.raises(RuntimeError):
|
|
AsyncRedisManager('valkey://')
|
|
assert AsyncRedisManager('unix:///var/sock/redis.sock') is not None
|
|
|
|
async_redis_manager.aiovalkey = saved_valkey
|
|
|
|
def test_redis_valkey_not_installed(self):
|
|
saved_redis = async_redis_manager.aioredis
|
|
async_redis_manager.aioredis = None
|
|
saved_valkey = async_redis_manager.aiovalkey
|
|
async_redis_manager.aiovalkey = None
|
|
|
|
with pytest.raises(RuntimeError):
|
|
AsyncRedisManager('redis://')
|
|
with pytest.raises(RuntimeError):
|
|
AsyncRedisManager('valkey://')
|
|
with pytest.raises(RuntimeError):
|
|
AsyncRedisManager('unix:///var/sock/redis.sock')
|
|
|
|
async_redis_manager.aioredis = saved_redis
|
|
async_redis_manager.aiovalkey = saved_valkey
|
|
|
|
def test_bad_url(self):
|
|
with pytest.raises(ValueError):
|
|
AsyncRedisManager('http://localhost:6379')
|
|
|
|
def test_redis_connect(self):
|
|
urls = [
|
|
'redis://localhost:6379',
|
|
'redis://localhost:6379/0',
|
|
'redis://:password@localhost:6379',
|
|
'redis://:password@localhost:6379/0',
|
|
'redis://user:password@localhost:6379',
|
|
'redis://user:password@localhost:6379/0',
|
|
|
|
'rediss://localhost:6379',
|
|
'rediss://localhost:6379/0',
|
|
'rediss://:password@localhost:6379',
|
|
'rediss://:password@localhost:6379/0',
|
|
'rediss://user:password@localhost:6379',
|
|
'rediss://user:password@localhost:6379/0',
|
|
|
|
'unix:///var/sock/redis.sock',
|
|
'unix:///var/sock/redis.sock?db=0',
|
|
'unix://user@/var/sock/redis.sock',
|
|
'unix://user@/var/sock/redis.sock?db=0',
|
|
|
|
'redis+sentinel://192.168.0.1:6379,192.168.0.2:6379/'
|
|
]
|
|
for url in urls:
|
|
c = AsyncRedisManager(url)
|
|
assert isinstance(c.redis, redis.asyncio.Redis)
|
|
|
|
def test_valkey_connect(self):
|
|
saved_redis = async_redis_manager.aioredis
|
|
async_redis_manager.aioredis = None
|
|
|
|
urls = [
|
|
'valkey://localhost:6379',
|
|
'valkey://localhost:6379/0',
|
|
'valkey://:password@localhost:6379',
|
|
'valkey://:password@localhost:6379/0',
|
|
'valkey://user:password@localhost:6379',
|
|
'valkey://user:password@localhost:6379/0',
|
|
|
|
'valkeys://localhost:6379',
|
|
'valkeys://localhost:6379/0',
|
|
'valkeys://:password@localhost:6379',
|
|
'valkeys://:password@localhost:6379/0',
|
|
'valkeys://user:password@localhost:6379',
|
|
'valkeys://user:password@localhost:6379/0',
|
|
|
|
'unix:///var/sock/redis.sock',
|
|
'unix:///var/sock/redis.sock?db=0',
|
|
'unix://user@/var/sock/redis.sock',
|
|
'unix://user@/var/sock/redis.sock?db=0',
|
|
|
|
'valkey+sentinel://192.168.0.1:6379,192.168.0.2:6379/'
|
|
]
|
|
for url in urls:
|
|
c = AsyncRedisManager(url)
|
|
assert isinstance(c.redis, valkey.asyncio.Valkey)
|
|
|
|
async_redis_manager.aioredis = saved_redis
|
|
|
|
|
|
class _FakePubSub:
|
|
def __init__(self, chan_bytes, script):
|
|
self._chan = chan_bytes
|
|
self._script = list(script)
|
|
self._unsubscribed = False
|
|
|
|
async def subscribe(self, channel):
|
|
return True
|
|
|
|
async def unsubscribe(self, channel):
|
|
self._unsubscribed = True
|
|
return True
|
|
|
|
async def listen(self):
|
|
while self._script:
|
|
step = self._script.pop(0)
|
|
if step == "timeout":
|
|
raise TimeoutError("simulated timeout")
|
|
if step == "msg":
|
|
yield {
|
|
"type": "message",
|
|
"channel": self._chan,
|
|
"data": b"ok",
|
|
}
|
|
while True:
|
|
await asyncio.sleep(3600)
|
|
|
|
|
|
class _FakeRedis:
|
|
def __init__(self, **opts):
|
|
self._opts = opts
|
|
self._scripts = []
|
|
|
|
@classmethod
|
|
def from_url(cls, url, **kwargs):
|
|
obj = cls(**kwargs)
|
|
return obj
|
|
|
|
def pubsub(self, ignore_subscribe_messages=True):
|
|
script = self._scripts.pop(0) if self._scripts else ["msg"]
|
|
return _FakePubSub(b"socketio", script)
|
|
|
|
|
|
class _FakeSentinel:
|
|
class Sentinel:
|
|
def __init__(self, *a, **kw):
|
|
pass
|
|
|
|
def master_for(self, *a, **kw):
|
|
return _FakeRedis()
|
|
|
|
|
|
def _fake_valkey_module():
|
|
mod = types.SimpleNamespace()
|
|
mod.__name__ = "valkey.asyncio"
|
|
mod.Redis = _FakeRedis
|
|
mod.sentinel = _FakeSentinel
|
|
return mod
|
|
|
|
|
|
class _TestManager(AsyncRedisManager):
|
|
"""AsyncRedisManager that uses our fake 'valkey' module and scripts."""
|
|
|
|
def __init__(self, scripts, **kw):
|
|
# scripts is a list of lists, e.g. [["timeout"], ["msg"]]
|
|
self._scripts = scripts
|
|
super().__init__(url="valkey://localhost/0", **kw)
|
|
|
|
def _get_redis_module_and_error(self):
|
|
fake = _fake_valkey_module()
|
|
return fake, RuntimeError
|
|
|
|
def _redis_connect(self):
|
|
module, _ = self._get_redis_module_and_error()
|
|
self.redis = module.Redis.from_url(
|
|
self.redis_url, **(self.redis_options or {})
|
|
)
|
|
self.redis._scripts = self._scripts
|
|
self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_listen_reconnects_after_timeout_and_yields():
|
|
"""First TimeoutError -> reconnect + resubscribe -> yield next message."""
|
|
mgr = _TestManager(scripts=[["timeout"], ["msg"]])
|
|
agen = mgr._listen()
|
|
|
|
got = await asyncio.wait_for(agen.__anext__(), timeout=2.5)
|
|
assert got == b"ok"
|
|
|
|
await agen.aclose()
|
|
assert mgr.pubsub._unsubscribed is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_listen_aclose_unsubscribes():
|
|
"""Closing the async generator must unsubscribe the pub/sub."""
|
|
mgr = _TestManager(scripts=[["msg"]])
|
|
agen = mgr._listen()
|
|
|
|
_ = await asyncio.wait_for(agen.__anext__(), timeout=1.0)
|
|
await agen.aclose()
|
|
assert mgr.pubsub._unsubscribed is True
|
|
|