33
44
55import asyncio
6- from collections .abc import AsyncIterable , Awaitable , Callable
6+ from collections .abc import AsyncIterable , Awaitable , Callable , Iterable
77from contextlib import AsyncExitStack
88from copy import deepcopy
99from secrets import choice
1010from typing import Final
1111
1212import pytest
1313from asgi_lifespan import LifespanManager
14+ from faker import Faker
1415from fastapi import FastAPI
1516from pydantic import NonNegativeInt
1617from pytest_mock import MockerFixture
2021from settings_library .redis import RedisSettings
2122from simcore_service_dynamic_scheduler .core .application import create_app
2223from simcore_service_dynamic_scheduler .services .generic_scheduler ._core import (
23- Operation ,
2424 get_core ,
2525)
2626from simcore_service_dynamic_scheduler .services .generic_scheduler ._models import (
27+ OperationContext ,
28+ OperationName ,
2729 ScheduleId ,
2830)
2931from simcore_service_dynamic_scheduler .services .generic_scheduler ._operation import (
3032 BaseStep ,
33+ Operation ,
3134 OperationRegistry ,
3235 ParallelStepGroup ,
3336 SingleStepGroup ,
@@ -106,8 +109,31 @@ async def selected_app(
106109 return choice (apps )
107110
108111
112+ @pytest .fixture
113+ def register_operation () -> Iterable [Callable [[OperationName , Operation ], None ]]:
114+ to_unregister : list [OperationName ] = []
115+
116+ def _ (opration_name : OperationName , operation : Operation ) -> None :
117+ OperationRegistry .register (opration_name , operation )
118+ to_unregister .append (opration_name )
119+
120+ yield _
121+
122+ for opration_name in to_unregister :
123+ OperationRegistry .unregister (opration_name )
124+
125+
109126_STEPS_CALL_ORDER : list [tuple [str , str ]] = []
110127
128+
129+ @pytest .fixture
130+ def steps_call_order () -> Iterable [list [tuple [str , str ]]]:
131+ yield _STEPS_CALL_ORDER
132+ _STEPS_CALL_ORDER .clear ()
133+
134+
135+ # UTILS ---------------------------------------------------------------
136+
111137_CREATED : Final [str ] = "create"
112138_REVERTED : Final [str ] = "revert"
113139
@@ -128,6 +154,12 @@ class _BaseExpectedStepOrder:
128154 def __init__ (self , * steps : type [BaseStep ]) -> None :
129155 self .steps = steps
130156
157+ def __len__ (self ) -> int :
158+ return len (self .steps )
159+
160+ def __repr__ (self ) -> str :
161+ return f"{ self .__class__ .__name__ } ({ ', ' .join (step .get_step_name () for step in self .steps )} )"
162+
131163
132164class _CreateSequence (_BaseExpectedStepOrder ):
133165 """steps appear in a sequence as CREATE"""
@@ -145,110 +177,168 @@ class _RevertRandom(_BaseExpectedStepOrder):
145177 """steps appear in any given order as REVERT"""
146178
147179
148- def _asseert_order (* expected : _BaseExpectedStepOrder ) -> None :
149- call_order = deepcopy (_STEPS_CALL_ORDER )
150-
151- def _check_sequence (
152- tracked : list [tuple [str , str ]],
153- steps : tuple [type [BaseStep ], ...],
154- * ,
155- expected_status : str ,
156- ) -> None :
157- for step in steps :
158- step_name , actual = tracked .pop (0 )
159- assert step_name == step .__name__
160- assert actual == expected_status
161-
162- def _check_random (
163- tracked : list [tuple [str , str ]],
164- steps : tuple [type [BaseStep ], ...],
165- * ,
166- expected_status : str ,
167- ) -> None :
168- names = [step .__name__ for step in steps ]
169- for _ in steps :
170- step_name , actual = tracked .pop (0 )
171- assert step_name in names
172- assert actual == expected_status
173- names .remove (step_name )
174-
175- for group in expected :
180+ def _assert_sequence (
181+ remaning_call_order : list [tuple [str , str ]],
182+ steps : tuple [type [BaseStep ], ...],
183+ * ,
184+ expected : str ,
185+ ) -> None :
186+ for step in steps :
187+ step_name , actual = remaning_call_order .pop (0 )
188+ assert step_name == step .get_step_name ()
189+ assert actual == expected
190+
191+
192+ def _assert_random (
193+ remaning_call_order : list [tuple [str , str ]],
194+ steps : tuple [type [BaseStep ], ...],
195+ * ,
196+ expected : str ,
197+ ) -> None :
198+ steps_names = {step .get_step_name () for step in steps }
199+ for _ in steps :
200+ step_name , actual = remaning_call_order .pop (0 )
201+ assert step_name in steps_names
202+ assert actual == expected
203+ steps_names .remove (step_name )
204+
205+
206+ def _asseert_order_as_expected (
207+ steps_call_order : list [tuple [str , str ]],
208+ expected_order : list [_BaseExpectedStepOrder ],
209+ ) -> None :
210+ # below operations are destructive make a copy
211+ call_order = deepcopy (steps_call_order )
212+
213+ assert len (call_order ) == sum (len (x ) for x in expected_order )
214+
215+ for group in expected_order :
176216 if isinstance (group , _CreateSequence ):
177- _check_sequence (call_order , group .steps , expected_status = _CREATED )
217+ _assert_sequence (call_order , group .steps , expected = _CREATED )
178218 elif isinstance (group , _CreateRandom ):
179- _check_random (call_order , group .steps , expected_status = _CREATED )
219+ _assert_random (call_order , group .steps , expected = _CREATED )
180220 elif isinstance (group , _RevertSequence ):
181- _check_sequence (call_order , group .steps , expected_status = _REVERTED )
221+ _assert_sequence (call_order , group .steps , expected = _REVERTED )
182222 elif isinstance (group , _RevertRandom ):
183- _check_random (call_order , group .steps , expected_status = _REVERTED )
223+ _assert_random (call_order , group .steps , expected = _REVERTED )
184224 else :
185225 msg = f"Unknown { group = } "
186226 raise NotImplementedError (msg )
187227 assert not call_order , f"Left overs { call_order = } "
188228
189229
190- class _PeelPotates (_BS ): ...
191-
230+ # TESTS ---------------------------------------------------------------
192231
193- class _BoilPotates (_BS ): ...
194232
233+ class _S1 (_BS ): ...
195234
196- class _MashPotates (_BS ): ...
197235
236+ class _S2 (_BS ): ...
198237
199- class _AddButter (_BS ): ...
200238
239+ class _S3 (_BS ): ...
201240
202- class _AddSalt (_BS ): ...
203241
242+ class _S4 (_BS ): ...
204243
205- class _AddPepper (_BS ): ...
206244
245+ class _S5 (_BS ): ...
207246
208- class _AddPaprika (_BS ): ...
209247
248+ class _S6 (_BS ): ...
210249
211- class _AddMint (_BS ): ...
212250
251+ class _S7 (_BS ): ...
213252
214- class _AddMilk (_BS ): ...
215253
254+ class _S8 (_BS ): ...
216255
217- class _StirTillDone (_BS ): ...
218256
257+ class _S9 (_BS ): ...
219258
220- _MASHED_POTATOES : Final [Operation ] = [
221- SingleStepGroup (_PeelPotates ),
222- SingleStepGroup (_BoilPotates ),
223- SingleStepGroup (_MashPotates ),
224- ParallelStepGroup (
225- _AddButter , _AddSalt , _AddPepper , _AddPaprika , _AddMint , _AddMilk
226- ),
227- SingleStepGroup (_StirTillDone ),
228- ]
229259
230- OperationRegistry . register ( "mash_potatoes" , _MASHED_POTATOES ) # type: ignore[call-arg
260+ class _S10 ( _BS ): ...
231261
232262
233263@pytest .mark .parametrize ("app_count" , [10 ])
264+ @pytest .mark .parametrize (
265+ "operation, operation_context, expected_order" ,
266+ [
267+ pytest .param (
268+ [
269+ SingleStepGroup (_S1 ),
270+ ],
271+ {},
272+ [
273+ _CreateSequence (_S1 ),
274+ ],
275+ id = "s1" ,
276+ ),
277+ pytest .param (
278+ [
279+ ParallelStepGroup (_S1 , _S2 ),
280+ ],
281+ {},
282+ [
283+ _CreateRandom (_S1 , _S2 ),
284+ ],
285+ id = "p2" ,
286+ ),
287+ pytest .param (
288+ [
289+ ParallelStepGroup (_S1 , _S2 , _S3 , _S4 , _S5 , _S6 , _S7 , _S8 , _S9 , _S10 ),
290+ ],
291+ {},
292+ [
293+ _CreateRandom (_S1 , _S2 , _S3 , _S4 , _S5 , _S6 , _S7 , _S8 , _S9 , _S10 ),
294+ ],
295+ id = "p10" ,
296+ ),
297+ pytest .param (
298+ [
299+ SingleStepGroup (_S1 ),
300+ SingleStepGroup (_S2 ),
301+ SingleStepGroup (_S3 ),
302+ ParallelStepGroup (_S4 , _S5 , _S6 , _S7 , _S8 , _S9 ),
303+ SingleStepGroup (_S10 ),
304+ ],
305+ {},
306+ [
307+ _CreateSequence (_S1 , _S2 , _S3 ),
308+ _CreateRandom (_S4 , _S5 , _S6 , _S7 , _S8 , _S9 ),
309+ _CreateSequence (_S10 ),
310+ ],
311+ id = "s1-s1-s1-p6-s1" ,
312+ ),
313+ ],
314+ )
234315async def test_core_workflow (
235- preserve_caplog_for_async_logging : None , selected_app : FastAPI
316+ preserve_caplog_for_async_logging : None ,
317+ steps_call_order : list [tuple [str , str ]],
318+ selected_app : FastAPI ,
319+ register_operation : Callable [[OperationName , Operation ], None ],
320+ operation : Operation ,
321+ operation_context : OperationContext ,
322+ expected_order : list [_BaseExpectedStepOrder ],
323+ faker : Faker ,
236324):
237- schedule_id : ScheduleId = await get_core (selected_app ).create ("mash_potatoes" , {})
238- print (f"started { schedule_id = } " )
325+ operation_name : OperationName = faker .uuid4 ()
326+
327+ register_operation (operation_name , operation )
328+
329+ schedule_id = await get_core (selected_app ).create (operation_name , operation_context )
330+ assert isinstance (schedule_id , ScheduleId )
239331
240332 async for attempt in AsyncRetrying (
241333 wait = wait_fixed (0.1 ),
242- stop = stop_after_delay (5 ),
334+ stop = stop_after_delay (10 ),
243335 retry = retry_if_exception_type (AssertionError ),
244336 ):
245337 with attempt :
246338 await asyncio .sleep (0 ) # wait for envet to trigger
247- assert len (_STEPS_CALL_ORDER ) == 10
248- _asseert_order (
249- _CreateSequence (_PeelPotates , _BoilPotates , _MashPotates ),
250- _CreateRandom (
251- _AddButter , _AddSalt , _AddPepper , _AddPaprika , _AddMint , _AddMilk
252- ),
253- _CreateSequence (_StirTillDone ),
254- )
339+ _asseert_order_as_expected (steps_call_order , expected_order )
340+
341+
342+ # TODO: test reversal
343+ # TODO: test manual intervention
344+ # TODO: test repeating
0 commit comments