11"""Tests for async Mongo client with async functions."""
22
33import asyncio
4+ from datetime import datetime , timedelta
45from random import random
56
67import pytest
78
89from cachier import cachier
10+ from cachier .cores .mongo import _MongoCore
911from 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
14128async 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
38150async 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