11# pylint: disable=redefined-outer-name
2+ # pylint: disable=unused-argument
23
34import asyncio
45import copy
56import random
67from collections import deque
8+ from dataclasses import dataclass
79from time import time
8- from typing import Any , Dict , List
10+ from typing import Any , AsyncIterable , Dict , List , Optional
911
1012import pytest
11- from servicelib .async_utils import run_sequentially_in_context , sequential_jobs_contexts
13+ from servicelib .async_utils import (
14+ _sequential_jobs_contexts ,
15+ run_sequentially_in_context ,
16+ stop_sequential_workers ,
17+ )
1218
19+ RETRIES = 10
20+ DIFFERENT_CONTEXTS_COUNT = 10
1321
14- @pytest .fixture (autouse = True )
15- def ensure_run_in_sequence_context_is_empty ():
16- # NOTE: since the contexts variable is initialized at import time, when several test run
17- # the import happens only once and is rendered invalid, therefore explicit clearance is necessary
18- sequential_jobs_contexts .clear ()
22+
23+ @pytest .fixture
24+ async def ensure_run_in_sequence_context_is_empty (loop ) -> AsyncIterable [None ]:
25+ yield
26+ # NOTE
27+ # required when shutting down the application or ending tests
28+ # otherwise errors will occur when closing the loop
29+ await stop_sequential_workers ()
30+
31+
32+ @pytest .fixture
33+ def payload () -> str :
34+ return "some string payload"
35+
36+
37+ @pytest .fixture
38+ def expected_param_name () -> str :
39+ return "expected_param_name"
40+
41+
42+ @pytest .fixture
43+ def sleep_duration () -> float :
44+ return 0.01
1945
2046
2147class LockedStore :
@@ -34,12 +60,14 @@ async def get_all(self) -> List[Any]:
3460 return list (self ._queue )
3561
3662
37- async def test_context_aware_dispatch () -> None :
63+ async def test_context_aware_dispatch (
64+ sleep_duration : float ,
65+ ensure_run_in_sequence_context_is_empty : None ,
66+ ) -> None :
3867 @run_sequentially_in_context (target_args = ["c1" , "c2" , "c3" ])
3968 async def orderly (c1 : Any , c2 : Any , c3 : Any , control : Any ) -> None :
4069 _ = (c1 , c2 , c3 )
41- sleep_interval = random .uniform (0 , 0.01 )
42- await asyncio .sleep (sleep_interval )
70+ await asyncio .sleep (sleep_duration )
4371
4472 context = dict (c1 = c1 , c2 = c2 , c3 = c3 )
4573 await locked_stores [make_key_from_context (context )].push (control )
@@ -81,12 +109,14 @@ def make_context():
81109 assert list (expected_outcomes [key ]) == await locked_stores [key ].get_all ()
82110
83111
84- async def test_context_aware_function_sometimes_fails () -> None :
112+ async def test_context_aware_function_sometimes_fails (
113+ ensure_run_in_sequence_context_is_empty : None ,
114+ ) -> None :
85115 class DidFailException (Exception ):
86116 pass
87117
88118 @run_sequentially_in_context (target_args = ["will_fail" ])
89- async def sometimes_failing (will_fail : bool ) -> None :
119+ async def sometimes_failing (will_fail : bool ) -> bool :
90120 if will_fail :
91121 raise DidFailException ("I was instructed to fail" )
92122 return True
@@ -101,8 +131,10 @@ async def sometimes_failing(will_fail: bool) -> None:
101131 assert await sometimes_failing (raise_error ) is True
102132
103133
104- async def test_context_aware_wrong_target_args_name () -> None :
105- expected_param_name = "wrong_parameter"
134+ async def test_context_aware_wrong_target_args_name (
135+ expected_param_name : str ,
136+ ensure_run_in_sequence_context_is_empty : None , # pylint: disable=unused-argument
137+ ) -> None :
106138
107139 # pylint: disable=unused-argument
108140 @run_sequentially_in_context (target_args = [expected_param_name ])
@@ -119,15 +151,17 @@ async def target_function(the_param: Any) -> None:
119151 assert str (excinfo .value ).startswith (message ) is True
120152
121153
122- async def test_context_aware_measure_parallelism () -> None :
154+ async def test_context_aware_measure_parallelism (
155+ sleep_duration : float ,
156+ ensure_run_in_sequence_context_is_empty : None ,
157+ ) -> None :
123158 # expected duration 1 second
124159 @run_sequentially_in_context (target_args = ["control" ])
125160 async def sleep_for (sleep_interval : float , control : Any ) -> Any :
126161 await asyncio .sleep (sleep_interval )
127162 return control
128163
129- control_sequence = list (range (1000 ))
130- sleep_duration = 0.5
164+ control_sequence = list (range (RETRIES ))
131165 functions = [sleep_for (sleep_duration , x ) for x in control_sequence ]
132166
133167 start = time ()
@@ -138,15 +172,17 @@ async def sleep_for(sleep_interval: float, control: Any) -> Any:
138172 assert control_sequence == result
139173
140174
141- async def test_context_aware_measure_serialization () -> None :
175+ async def test_context_aware_measure_serialization (
176+ sleep_duration : float ,
177+ ensure_run_in_sequence_context_is_empty : None ,
178+ ) -> None :
142179 # expected duration 1 second
143180 @run_sequentially_in_context (target_args = ["control" ])
144181 async def sleep_for (sleep_interval : float , control : Any ) -> Any :
145182 await asyncio .sleep (sleep_interval )
146183 return control
147184
148- control_sequence = [1 for _ in range (10 )]
149- sleep_duration = 0.1
185+ control_sequence = [1 for _ in range (RETRIES )]
150186 functions = [sleep_for (sleep_duration , x ) for x in control_sequence ]
151187
152188 start = time ()
@@ -156,3 +192,36 @@ async def sleep_for(sleep_interval: float, control: Any) -> Any:
156192 minimum_timelapse = (sleep_duration ) * len (control_sequence )
157193 assert elapsed > minimum_timelapse
158194 assert control_sequence == result
195+
196+
197+ async def test_nested_object_attribute (
198+ payload : str ,
199+ ensure_run_in_sequence_context_is_empty : None ,
200+ ) -> None :
201+ @dataclass
202+ class ObjectWithPropos :
203+ attr1 : str = payload
204+
205+ @run_sequentially_in_context (target_args = ["object_with_props.attr1" ])
206+ async def test_attribute (
207+ object_with_props : ObjectWithPropos , other_attr : Optional [int ] = None
208+ ) -> str :
209+ return object_with_props .attr1
210+
211+ for _ in range (RETRIES ):
212+ assert payload == await test_attribute (ObjectWithPropos ())
213+
214+
215+ async def test_different_contexts (
216+ payload : str ,
217+ ensure_run_in_sequence_context_is_empty : None ,
218+ ) -> None :
219+ @run_sequentially_in_context (target_args = ["context_param" ])
220+ async def test_multiple_context_calls (context_param : int ) -> int :
221+ return context_param
222+
223+ for _ in range (RETRIES ):
224+ for i in range (DIFFERENT_CONTEXTS_COUNT ):
225+ assert i == await test_multiple_context_calls (i )
226+
227+ assert len (_sequential_jobs_contexts ) == RETRIES
0 commit comments