Skip to content

Commit 88c6bba

Browse files
committed
Added more tests on caching and update project test dependencies
1 parent cd34861 commit 88c6bba

11 files changed

+543
-60
lines changed

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ test = [
7575
"anyio[trio] >= 3.2.1",
7676
"autoflake",
7777
"email_validator >=1.1.1",
78+
"pylibmc",
79+
"pymemcache",
80+
"aiomcache",
81+
"redis",
7882

7983
# types
8084
"types-ujson ==5.7.0.0",

tests/test_application/test_application_functions.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ def test_app_staticfiles_route(self, tmpdir):
212212

213213
config = Config(STATIC_DIRECTORIES=[tmpdir])
214214
injector = EllarInjector()
215-
CoreServiceRegistration(injector).register_all()
215+
CoreServiceRegistration(injector, config=Config()).register_all()
216216
injector.container.register_instance(config)
217217
app = App(injector=injector, config=config)
218218
client = TestClient(app)
@@ -234,7 +234,7 @@ def test_app_staticfiles_with_different_static_path(self, tmpdir):
234234
STATIC_MOUNT_PATH="/static-modified", STATIC_DIRECTORIES=[tmpdir]
235235
)
236236
injector = EllarInjector()
237-
CoreServiceRegistration(injector).register_all()
237+
CoreServiceRegistration(injector, config=Config()).register_all()
238238
injector.container.register_instance(config)
239239
app = App(injector=injector, config=config)
240240
client = TestClient(app)
@@ -284,7 +284,7 @@ def test_app_initialization_completion(self):
284284
injector = EllarInjector(
285285
auto_bind=False
286286
) # will raise an exception is service is not registered
287-
CoreServiceRegistration(injector).register_all()
287+
CoreServiceRegistration(injector, config=Config()).register_all()
288288
injector.container.register_instance(config)
289289

290290
app = App(config=config, injector=injector)
@@ -308,7 +308,7 @@ async def catch(
308308
injector = EllarInjector(
309309
auto_bind=False
310310
) # will raise an exception is service is not registered
311-
CoreServiceRegistration(injector).register_all()
311+
CoreServiceRegistration(injector, config=Config()).register_all()
312312
injector.container.register_instance(config)
313313
app = App(config=config, injector=injector)
314314
app.add_exception_handler(CustomExceptionHandler())

tests/test_cache/pylib_mock.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import time
2+
from unittest import mock
3+
4+
from pylibmc import Client
5+
6+
7+
class MockClient(Client):
8+
def __init__(self, *args, **kwargs):
9+
super().__init__(*args, **kwargs)
10+
self.args = args
11+
self.kwargs = kwargs
12+
self._cache = {}
13+
14+
def set(self, *args, **kwargs):
15+
key, value, _time = args
16+
with mock.patch.object(Client, "set") as set_mock:
17+
set_mock.return_value = True
18+
res = super().set(*args, **kwargs)
19+
assert set_mock.call_args.args == args
20+
self._cache[key] = (value, _time)
21+
return res
22+
23+
def get(self, *args, **kwargs):
24+
(key,) = args
25+
_res = self._cache.get(key)
26+
27+
if _res and time.time() >= _res[1]:
28+
del self._cache[key]
29+
_res = None
30+
31+
with mock.patch.object(Client, "get") as get_mock:
32+
get_mock.return_value = _res[0] if _res else None
33+
res = super().get(*args, **kwargs)
34+
assert get_mock.call_args.args == args
35+
return res
36+
37+
def touch(self, *args, **kwargs):
38+
key, _time = args
39+
_res = self._cache.get(key)
40+
value = False
41+
if _res:
42+
self._cache.update({key: (_res[0], _time)})
43+
value = True
44+
45+
with mock.patch.object(Client, "touch") as touch_mock:
46+
touch_mock.return_value = value
47+
res = super().touch(*args, **kwargs)
48+
assert touch_mock.call_args.args == args
49+
return res
50+
51+
def delete(self, *args, **kwargs):
52+
(key,) = args
53+
value = True
54+
if self._cache.get(key):
55+
del self._cache[key]
56+
else:
57+
value = False
58+
with mock.patch.object(Client, "delete") as delete_mock:
59+
delete_mock.return_value = value
60+
res = super().delete(*args, **kwargs)
61+
assert delete_mock.call_args.args == args
62+
return res
63+
64+
def disconnect_all(self, *args, **kwargs):
65+
with mock.patch.object(Client, "disconnect_all") as disconnect_all_mock:
66+
disconnect_all_mock.return_value = None
67+
res = super().disconnect_all(*args, **kwargs)
68+
assert disconnect_all_mock.call_args.args == args
69+
return res

tests/test_cache/redis_mock.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import time
2+
3+
4+
class MockAsyncMemCacheClient:
5+
def __init__(self, *args, time_lookup="ex", **kwargs):
6+
self.args = args
7+
self.kwargs = kwargs
8+
self._time_lookup = time_lookup
9+
self._cache_default = {}
10+
11+
@property
12+
def _cache(self):
13+
return self._cache_default
14+
15+
def get_backend_timeout(self, timeout):
16+
return timeout
17+
18+
async def set(self, *args, **kwargs):
19+
key, value = args
20+
self._cache[key] = (
21+
value,
22+
self.get_backend_timeout(kwargs.get(self._time_lookup)),
23+
)
24+
return True
25+
26+
async def get(self, *args, **kwargs):
27+
(key,) = args
28+
_res = self._cache.get(key)
29+
30+
if _res and time.time() >= _res[1]:
31+
del self._cache[key]
32+
_res = None
33+
return _res[0] if _res else None
34+
35+
async def touch(self, *args, **kwargs):
36+
(key,) = args
37+
_res = self._cache.get(key)
38+
value = False
39+
if _res:
40+
self._cache.update(
41+
{
42+
key: (
43+
_res[0],
44+
self.get_backend_timeout(kwargs.get(self._time_lookup)),
45+
)
46+
}
47+
)
48+
value = True
49+
return value
50+
51+
async def delete(self, *args, **kwargs):
52+
(key,) = args
53+
value = True
54+
if self._cache.get(key):
55+
del self._cache[key]
56+
else:
57+
value = False
58+
return value
59+
60+
61+
class MockRedisClient(MockAsyncMemCacheClient):
62+
_cache_static = {}
63+
64+
@property
65+
def _cache(self):
66+
return self._cache_static
67+
68+
def get_backend_timeout(self, timeout):
69+
return time.time() + int(timeout)
70+
71+
async def persist(self, key):
72+
return self._cache.get(key) is not None
73+
74+
async def expire(self, key, ex):
75+
return await self.touch(key, ex=ex)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from ellar.cache import ICacheService
2+
from ellar.cache.backends.local_cache import LocalMemCacheBackend
3+
from ellar.common import Controller, cache, get
4+
from ellar.core import TestClientFactory
5+
from ellar.core.response import PlainTextResponse
6+
from ellar.helper.importer import get_class_import
7+
8+
9+
class ManyCacheBackendConfig:
10+
CACHES = {
11+
"default": LocalMemCacheBackend(key_prefix="default"),
12+
"another": LocalMemCacheBackend(key_prefix="another", version=2),
13+
}
14+
15+
16+
@Controller("")
17+
class ExampleController:
18+
@get("/index-1")
19+
@cache(timeout=1, backend="another")
20+
def index_1(self):
21+
return PlainTextResponse("ExampleController cache 1")
22+
23+
@get("/index-2")
24+
@cache(timeout=1, backend="default")
25+
def index_2(self):
26+
return dict(message="ExampleController cache 2")
27+
28+
29+
tm = TestClientFactory.create_test_module(
30+
controllers=[ExampleController],
31+
config_module=get_class_import(ManyCacheBackendConfig),
32+
)
33+
34+
35+
def test_cache_backend_has_many_cache_backend():
36+
cache_service = tm.app.injector.get(ICacheService)
37+
assert isinstance(cache_service._get_backend("another"), LocalMemCacheBackend)
38+
assert isinstance(cache_service._get_backend("default"), LocalMemCacheBackend)
39+
40+
41+
def test_cache_operation_with_backend_works():
42+
cache_service = tm.app.injector.get(ICacheService)
43+
client = tm.get_client(base_url="http://testserver")
44+
45+
response = client.get("/index-1")
46+
assert response.text == "ExampleController cache 1"
47+
result = cache_service.get("http://testserver/index-1:view", backend="another")
48+
assert result.body == b"ExampleController cache 1"
49+
50+
response = client.get("/index-2")
51+
assert response.json() == dict(message="ExampleController cache 2")
52+
result = cache_service.get("http://testserver/index-2:view", backend="default")
53+
assert result == dict(message="ExampleController cache 2")

tests/test_cache/test_local_cache.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from time import sleep
2+
3+
from ellar.cache.backends.local_cache import LocalMemCacheBackend
4+
5+
6+
class TestLocalMemCacheBackend:
7+
def setup(self):
8+
self.backend = LocalMemCacheBackend()
9+
10+
def test_set(self):
11+
assert self.backend.set("test", "1", 0.1)
12+
value = self.backend.get("test")
13+
assert value == "1"
14+
15+
def test_has_key(self):
16+
assert not self.backend.has_key("test-has-key")
17+
assert self.backend.set("test-has-key", "1", 0.1)
18+
assert self.backend.has_key("test-has-key")
19+
20+
def test_delete(self):
21+
assert not self.backend.delete("test-delete")
22+
assert self.backend.set("test-delete", "1", 0.1)
23+
assert self.backend.delete("test-delete")
24+
25+
def test_touch(self):
26+
assert not self.backend.touch("test-touch")
27+
assert self.backend.set("test-touch", "1", 0.1)
28+
assert self.backend.touch("test-touch", 30)
29+
sleep(0.22)
30+
assert self.backend.get("test-touch") == "1"
31+
32+
33+
class TestLocalMemCacheBackendAsync:
34+
def setup(self):
35+
self.backend = LocalMemCacheBackend()
36+
37+
async def test_set_async(self, anyio_backend):
38+
assert await self.backend.set_async("test", "1", 0.1)
39+
value = await self.backend.get_async("test")
40+
assert value == "1"
41+
42+
async def test_has_key_async(self, anyio_backend):
43+
assert not await self.backend.has_key_async("test-has-key")
44+
assert await self.backend.set_async("test-has-key", "1", 0.1)
45+
assert await self.backend.has_key_async("test-has-key")
46+
47+
async def test_delete_async(self, anyio_backend):
48+
assert not await self.backend.delete_async("test-delete")
49+
assert await self.backend.set_async("test-delete", "1", 0.1)
50+
assert await self.backend.delete_async("test-delete")
51+
52+
async def test_touch_async(self, anyio_backend):
53+
assert not await self.backend.touch_async("test-touch")
54+
assert await self.backend.set_async("test-touch", "1", 0.1)
55+
assert await self.backend.touch_async("test-touch", 30)
56+
sleep(0.22)
57+
assert await self.backend.get_async("test-touch") == "1"
58+
59+
async def test_simple_cache_backend_with_init_params(
60+
self, anyio_backend: str
61+
) -> None:
62+
backend = LocalMemCacheBackend(key_prefix="ellar", timeout=300, version=2)
63+
await backend.set_async("test", "2", 20) # type: ignore
64+
key = backend.make_key("test")
65+
assert backend._cache[key]
66+
assert isinstance(backend._cache[key], bytes)

0 commit comments

Comments
 (0)