Skip to content

Commit 04c1262

Browse files
committed
fix: Level code coverage
1 parent 4c4cf2a commit 04c1262

9 files changed

Lines changed: 870 additions & 11 deletions

File tree

tests/mongo_tests/test_async_mongo_core.py

Lines changed: 288 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,131 @@
11
"""Tests for async Mongo client with async functions."""
22

33
import asyncio
4+
from datetime import datetime, timedelta
45
from random import random
56

67
import pytest
78

89
from cachier import cachier
10+
from cachier.cores.mongo import _MongoCore
911
from tests.mongo_tests.test_mongo_core import _test_mongetter
1012

1113

14+
class _AsyncInMemoryMongoCollection:
15+
"""Minimal in-memory Mongo-like collection for async and sync path tests."""
16+
17+
def __init__(self):
18+
self._docs: dict[tuple[str, str], dict[str, object]] = {}
19+
self._indexes: dict[str, dict[str, object]] = {}
20+
self.await_index_information = False
21+
self.await_create_indexes = False
22+
self.await_find_one = False
23+
self.await_update_one = False
24+
self.await_update_many = False
25+
self.await_delete_many = False
26+
27+
@staticmethod
28+
async def _awaitable(value):
29+
return value
30+
31+
def index_information(self):
32+
result = dict(self._indexes)
33+
if self.await_index_information:
34+
return _AsyncInMemoryMongoCollection._awaitable(result)
35+
return result
36+
37+
def create_indexes(self, indexes):
38+
for index in indexes:
39+
document = getattr(index, "document", {})
40+
name = document.get("name", "index") if isinstance(document, dict) else "index"
41+
self._indexes[name] = {"name": name}
42+
result = list(self._indexes)
43+
if self.await_create_indexes:
44+
return _AsyncInMemoryMongoCollection._awaitable(result)
45+
return result
46+
47+
def find_one(self, query=None, **kwargs):
48+
if query is None:
49+
query = kwargs.get("filter", {})
50+
doc = self._docs.get((query.get("func"), query.get("key")))
51+
result = None if doc is None else dict(doc)
52+
if self.await_find_one:
53+
return _AsyncInMemoryMongoCollection._awaitable(result)
54+
return result
55+
56+
def update_one(self, query=None, update=None, upsert=False, **kwargs):
57+
if query is None:
58+
query = kwargs.get("filter", {})
59+
if update is None:
60+
update = kwargs.get("update", {})
61+
key = (query.get("func"), query.get("key"))
62+
doc = self._docs.get(key)
63+
if doc is None:
64+
if not upsert:
65+
result = {"matched_count": 0}
66+
if self.await_update_one:
67+
return _AsyncInMemoryMongoCollection._awaitable(result)
68+
return result
69+
doc = {"func": query.get("func"), "key": query.get("key")}
70+
doc.update(update.get("$set", {}))
71+
self._docs[key] = doc
72+
result = {"matched_count": 1}
73+
if self.await_update_one:
74+
return _AsyncInMemoryMongoCollection._awaitable(result)
75+
return result
76+
77+
def update_many(self, query=None, update=None, **kwargs):
78+
if query is None:
79+
query = kwargs.get("filter", {})
80+
if update is None:
81+
update = kwargs.get("update", {})
82+
changed = 0
83+
for doc in self._docs.values():
84+
if "func" in query and doc.get("func") != query["func"]:
85+
continue
86+
if "processing" in query and doc.get("processing") != query["processing"]:
87+
continue
88+
doc.update(update.get("$set", {}))
89+
changed += 1
90+
result = {"matched_count": changed}
91+
if self.await_update_many:
92+
return _AsyncInMemoryMongoCollection._awaitable(result)
93+
return result
94+
95+
def delete_many(self, query=None, **kwargs):
96+
if query is None:
97+
query = kwargs.get("filter", {})
98+
deleted = 0
99+
time_filter = query.get("time")
100+
for key, doc in list(self._docs.items()):
101+
if "func" in query and doc.get("func") != query["func"]:
102+
continue
103+
if isinstance(time_filter, dict) and "$lt" in time_filter:
104+
doc_time = doc.get("time")
105+
if doc_time is None or doc_time >= time_filter["$lt"]:
106+
continue
107+
del self._docs[key]
108+
deleted += 1
109+
result = {"deleted_count": deleted}
110+
if self.await_delete_many:
111+
return _AsyncInMemoryMongoCollection._awaitable(result)
112+
return result
113+
114+
115+
def _build_mongo_core(mongetter):
116+
"""Build a Mongo core configured for direct core tests."""
117+
core = _MongoCore(hash_func=None, mongetter=mongetter, wait_for_calc_timeout=10)
118+
119+
def _func(x: int) -> int:
120+
return x
121+
122+
core.set_func(_func)
123+
return core
124+
125+
12126
@pytest.mark.mongo
13127
@pytest.mark.asyncio
14128
async def test_async_mongo_mongetter():
15-
from cachier.cores.mongo import _MongoCore
16-
17129
base_collection = _test_mongetter()
18130
collection = base_collection.database["cachier_async_mongetter"]
19131
collection.delete_many({})
@@ -36,8 +148,6 @@ async def async_cached_mongo(x: int) -> float:
36148
@pytest.mark.mongo
37149
@pytest.mark.asyncio
38150
async def test_async_mongo_mongetter_method_args_and_kwargs():
39-
from cachier.cores.mongo import _MongoCore
40-
41151
base_collection = _test_mongetter()
42152
collection = base_collection.database["cachier_async_mongetter_methods"]
43153
collection.delete_many({})
@@ -62,3 +172,177 @@ async def async_cached_mongo_method_args_kwargs(self, x: int, y: int) -> int:
62172
assert call_count == 1
63173

64174
assert _MongoCore._INDEX_NAME in collection.index_information()
175+
176+
177+
@pytest.mark.mongo
178+
def test_sync_mongo_core_ensure_collection_state_branches():
179+
collection = _AsyncInMemoryMongoCollection()
180+
mongetter_calls = 0
181+
182+
def mongetter():
183+
nonlocal mongetter_calls
184+
mongetter_calls += 1
185+
return collection
186+
187+
core = _build_mongo_core(mongetter)
188+
assert core._ensure_collection() is collection
189+
assert mongetter_calls == 1
190+
assert _MongoCore._INDEX_NAME in collection.index_information()
191+
192+
core._index_verified = False
193+
core.mongo_collection = collection
194+
assert core._ensure_collection() is collection
195+
assert mongetter_calls == 1
196+
197+
core._index_verified = True
198+
core.mongo_collection = None
199+
assert core._ensure_collection() is collection
200+
assert mongetter_calls == 2
201+
202+
203+
@pytest.mark.mongo
204+
def test_sync_mongo_core_rejects_async_mongetter():
205+
async def async_mongetter():
206+
return _AsyncInMemoryMongoCollection()
207+
208+
core = _build_mongo_core(async_mongetter)
209+
with pytest.raises(TypeError, match="async mongetter is only supported for async cached functions"):
210+
core._ensure_collection()
211+
212+
213+
@pytest.mark.mongo
214+
def test_sync_mongo_core_rejects_awaitable_without_close():
215+
class _AwaitableNoClose:
216+
def __await__(self):
217+
async def _resolve():
218+
return _AsyncInMemoryMongoCollection()
219+
220+
return _resolve().__await__()
221+
222+
def mongetter():
223+
return _AwaitableNoClose()
224+
225+
core = _build_mongo_core(mongetter)
226+
with pytest.raises(TypeError, match="async mongetter is only supported for async cached functions"):
227+
core._ensure_collection()
228+
229+
230+
@pytest.mark.mongo
231+
def test_mongo_core_set_entry_should_not_store():
232+
core = _build_mongo_core(lambda: _AsyncInMemoryMongoCollection())
233+
core._should_store = lambda _value: False
234+
assert core.set_entry("ignored", None) is False
235+
236+
237+
@pytest.mark.mongo
238+
@pytest.mark.asyncio
239+
async def test_async_mongo_core_collection_resolution_and_index_branches():
240+
sync_collection = _AsyncInMemoryMongoCollection()
241+
sync_collection._indexes[_MongoCore._INDEX_NAME] = {"name": _MongoCore._INDEX_NAME}
242+
sync_core = _build_mongo_core(lambda: sync_collection)
243+
244+
assert await sync_core._ensure_collection_async() is sync_collection
245+
sync_core.mongo_collection = None
246+
sync_core._index_verified = True
247+
assert await sync_core._ensure_collection_async() is sync_collection
248+
249+
async_collection = _AsyncInMemoryMongoCollection()
250+
async_collection.await_index_information = True
251+
async_collection.await_create_indexes = True
252+
253+
async def async_mongetter():
254+
return async_collection
255+
256+
async_core = _build_mongo_core(async_mongetter)
257+
assert await async_core._ensure_collection_async() is async_collection
258+
assert _MongoCore._INDEX_NAME in async_collection._indexes
259+
260+
261+
@pytest.mark.mongo
262+
@pytest.mark.asyncio
263+
async def test_async_mongo_core_entry_read_write_paths():
264+
collection = _AsyncInMemoryMongoCollection()
265+
collection.await_find_one = True
266+
collection.await_update_one = True
267+
core = _build_mongo_core(lambda: collection)
268+
269+
assert await core.aset_entry("k1", {"value": 1}) is True
270+
_, entry = await core.aget_entry_by_key("k1")
271+
assert entry is not None
272+
assert entry.value == {"value": 1}
273+
274+
collection._docs[(core._func_str, "no-value")] = {
275+
"func": core._func_str,
276+
"key": "no-value",
277+
"time": datetime.now(),
278+
"processing": False,
279+
"completed": True,
280+
}
281+
_, no_value_entry = await core.aget_entry_by_key("no-value")
282+
assert no_value_entry is not None
283+
assert no_value_entry.value is None
284+
285+
core._should_store = lambda _value: False
286+
assert await core.aset_entry("ignored", None) is False
287+
288+
289+
@pytest.mark.mongo
290+
@pytest.mark.asyncio
291+
async def test_async_mongo_core_mark_clear_and_stale_paths():
292+
collection = _AsyncInMemoryMongoCollection()
293+
collection.await_update_one = True
294+
collection.await_update_many = True
295+
collection.await_delete_many = True
296+
core = _build_mongo_core(lambda: collection)
297+
298+
await core.aset_entry("stale", 1)
299+
await core.aset_entry("fresh", 2)
300+
collection._docs[(core._func_str, "stale")]["time"] = datetime.now() - timedelta(hours=2)
301+
collection._docs[(core._func_str, "fresh")]["time"] = datetime.now()
302+
303+
await core.amark_entry_being_calculated("fresh")
304+
await core.amark_entry_not_calculated("fresh")
305+
await core.aclear_being_calculated()
306+
await core.adelete_stale_entries(timedelta(hours=1))
307+
308+
assert (core._func_str, "stale") not in collection._docs
309+
assert (core._func_str, "fresh") in collection._docs
310+
311+
await core.aclear_cache()
312+
assert (core._func_str, "fresh") not in collection._docs
313+
314+
315+
@pytest.mark.mongo
316+
@pytest.mark.asyncio
317+
async def test_async_mongo_core_mark_clear_and_stale_paths_non_awaitable_results():
318+
collection = _AsyncInMemoryMongoCollection()
319+
core = _build_mongo_core(lambda: collection)
320+
321+
await core.aset_entry("old", 1)
322+
await core.aset_entry("new", 2)
323+
collection._docs[(core._func_str, "old")]["time"] = datetime.now() - timedelta(hours=2)
324+
collection._docs[(core._func_str, "new")]["time"] = datetime.now()
325+
326+
await core.amark_entry_being_calculated("new")
327+
await core.amark_entry_not_calculated("new")
328+
await core.aclear_being_calculated()
329+
await core.adelete_stale_entries(timedelta(hours=1))
330+
await core.aclear_cache()
331+
332+
assert collection._docs == {}
333+
334+
335+
@pytest.mark.mongo
336+
def test_mongo_core_delete_stale_entries_sync_path():
337+
collection = _AsyncInMemoryMongoCollection()
338+
core = _build_mongo_core(lambda: collection)
339+
340+
assert core.set_entry("stale", 1) is True
341+
assert core.set_entry("fresh", 2) is True
342+
collection._docs[(core._func_str, "stale")]["time"] = datetime.now() - timedelta(hours=2)
343+
collection._docs[(core._func_str, "fresh")]["time"] = datetime.now()
344+
345+
core.delete_stale_entries(timedelta(hours=1))
346+
347+
assert (core._func_str, "stale") not in collection._docs
348+
assert (core._func_str, "fresh") in collection._docs

0 commit comments

Comments
 (0)