Skip to content

Commit 1741d2b

Browse files
author
Andrei Neagu
committed
refactor tests
1 parent b53ef64 commit 1741d2b

File tree

3 files changed

+168
-69
lines changed

3 files changed

+168
-69
lines changed

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/services/generic_scheduler/_core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ async def _continue_handling_as_creation(
408408
await enqueue_schedule_event(self.app, schedule_id)
409409
else:
410410
# TODO: the end has bean reached, do nothing from now on
411+
# here an event can be sent when the opration finishes succesfully
411412
_logger.debug(
412413
"Operation '%s' for schedule_id='%s' COMPLETED successfully",
413414
operation_name,

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/services/generic_scheduler/_operation.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ def __init__(self, *, repeat_steps: bool, wait_before_repeat: timedelta) -> None
9595
def __len__(self) -> int:
9696
"""number of steps in this group"""
9797

98+
@abstractmethod
99+
def __repr__(self) -> str:
100+
"""text representation of this step group"""
101+
98102
@abstractmethod
99103
def get_step_group_name(self, *, index: NonNegativeInt) -> StepGroupName:
100104
"""returns the name of this step group"""
@@ -128,6 +132,9 @@ def __init__(
128132
def __len__(self) -> int:
129133
return 1
130134

135+
def __repr__(self) -> str:
136+
return f"{self.__class__.__name__}({self._step.get_step_name()})"
137+
131138
def get_step_group_name(self, *, index: NonNegativeInt) -> StepGroupName:
132139
return f"{index}S{'R' if self.repeat_steps else ''}"
133140

@@ -148,16 +155,17 @@ def __init__(
148155
repeat_steps: bool = _DEFAULT_REPEAT_STEPS,
149156
wait_before_repeat: timedelta = _DEFAULT_WAIT_BEFORE_REPEAT,
150157
) -> None:
151-
152158
self._steps: list[type[BaseStep]] = list(steps)
153-
154159
super().__init__(
155160
repeat_steps=repeat_steps, wait_before_repeat=wait_before_repeat
156161
)
157162

158163
def __len__(self) -> int:
159164
return len(self._steps)
160165

166+
def __repr__(self) -> str:
167+
return f"{self.__class__.__name__}({', '.join(step.get_step_name() for step in self._steps)})"
168+
161169
@property
162170
def steps(self) -> list[type[BaseStep]]:
163171
return self._steps

services/dynamic-scheduler/tests/unit/service_generic_scheduler/test__core.py

Lines changed: 157 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44

55
import asyncio
6-
from collections.abc import AsyncIterable, Awaitable, Callable
6+
from collections.abc import AsyncIterable, Awaitable, Callable, Iterable
77
from contextlib import AsyncExitStack
88
from copy import deepcopy
99
from secrets import choice
1010
from typing import Final
1111

1212
import pytest
1313
from asgi_lifespan import LifespanManager
14+
from faker import Faker
1415
from fastapi import FastAPI
1516
from pydantic import NonNegativeInt
1617
from pytest_mock import MockerFixture
@@ -20,14 +21,16 @@
2021
from settings_library.redis import RedisSettings
2122
from simcore_service_dynamic_scheduler.core.application import create_app
2223
from simcore_service_dynamic_scheduler.services.generic_scheduler._core import (
23-
Operation,
2424
get_core,
2525
)
2626
from simcore_service_dynamic_scheduler.services.generic_scheduler._models import (
27+
OperationContext,
28+
OperationName,
2729
ScheduleId,
2830
)
2931
from 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

132164
class _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+
)
234315
async 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

Comments
 (0)