1919from durabletask .internal import helpers
2020from durabletask .internal import orchestrator_service_pb2 as pb
2121from tests .durabletask .test_coroutine_orchestrators import (
22+ INSTANCE_ID ,
2223 NOW ,
2324 _completion , # pyright: ignore[reportPrivateUsage]
2425 _execute , # pyright: ignore[reportPrivateUsage]
@@ -32,6 +33,56 @@ def test_additive_public_api_has_no_new_abstract_requirements() -> None:
3233 assert task .AsyncOrchestrationContext is AsyncOrchestrationContext
3334
3435
36+ # Intentional differences between the contexts; anything else is drift.
37+ _ASYNC_ONLY_MEMBERS = {"as_task_context" }
38+ _EAGER_ONLY_MEMBERS : set [str ] = set ()
39+ _ASYNC_OPERATIONS = {
40+ "call_activity" , "call_sub_orchestrator" , "call_entity" , "lock_entities" ,
41+ "create_timer" , "wait_for_external_event" ,
42+ }
43+ _EXTRA_ASYNC_PARAMETERS = {"wait_for_external_event" : {"timeout" }}
44+
45+
46+ def _public_members (cls : type ) -> set [str ]:
47+ return {name for name in dir (cls ) if not name .startswith ("_" )}
48+
49+
50+ def test_async_context_public_api_matches_task_context () -> None :
51+ async_members = _public_members (AsyncOrchestrationContext )
52+ eager_members = _public_members (task .OrchestrationContext )
53+ assert async_members - eager_members == _ASYNC_ONLY_MEMBERS
54+ assert eager_members - async_members == _EAGER_ONLY_MEMBERS
55+
56+ for name in sorted (async_members & eager_members ):
57+ async_member = inspect .getattr_static (AsyncOrchestrationContext , name )
58+ eager_member = inspect .getattr_static (task .OrchestrationContext , name )
59+ assert isinstance (async_member , property ) == isinstance (eager_member , property ), name
60+ if isinstance (async_member , property ):
61+ continue
62+ assert inspect .iscoroutinefunction (async_member ) == (name in _ASYNC_OPERATIONS ), name
63+ async_parameters = set (inspect .signature (async_member ).parameters )
64+ eager_parameters = set (inspect .signature (eager_member ).parameters )
65+ assert async_parameters - eager_parameters == _EXTRA_ASYNC_PARAMETERS .get (name , set ()), name
66+ assert eager_parameters <= async_parameters , name
67+
68+
69+ def test_async_context_metadata_matches_task_context () -> None :
70+ observed : list [tuple [Any , Any ]] = []
71+
72+ async def orchestrator (ctx : task .AsyncOrchestrationContext , _ : Any ) -> None :
73+ eager = ctx .as_task_context ()
74+ for name in ("instance_id" , "parent_instance_id" , "version" , "current_utc_datetime" , "is_replaying" ):
75+ observed .append ((getattr (ctx , name ), getattr (eager , name )))
76+ await ctx .call_activity ("work" )
77+ observed .append ((ctx .is_replaying , eager .is_replaying ))
78+
79+ old = _start () + [helpers .new_task_scheduled_event (1 , "work" )]
80+ _execute (orchestrator , old , [helpers .new_task_completed_event (1 , "null" )])
81+ assert observed and all (async_value == eager_value for async_value , eager_value in observed )
82+ assert observed [0 ][0 ] == INSTANCE_ID
83+ assert observed [4 ][0 ] is True and observed [- 1 ][0 ] is False
84+
85+
3586def test_public_async_context_annotations_resolve_at_runtime () -> None :
3687 assert get_type_hints (AsyncOrchestrationContext .call_activity )["return" ] is Any
3788 assert get_type_hints (AsyncOrchestrationContext .call_sub_orchestrator )["return" ] is Any
@@ -348,6 +399,63 @@ async def orchestrator(ctx: task.AsyncOrchestrationContext, _: Any) -> int:
348399 assert json .loads (_completion (result ).result .value ) == 42
349400
350401
402+ @pytest .mark .parametrize ("seconds" , [float ("inf" ), float ("-inf" ), float ("nan" )])
403+ def test_nonfinite_timer_delay_rejected_before_scheduling (seconds : float ) -> None :
404+ class InfiniteDelay (timedelta ):
405+ def total_seconds (self ) -> float :
406+ return seconds
407+
408+ async def orchestrator (ctx : task .AsyncOrchestrationContext , _ : Any ) -> str :
409+ with pytest .raises (ValueError , match = "finite" ):
410+ await ctx .create_timer (InfiniteDelay ())
411+ return await ctx .call_activity ("work" , return_type = str )
412+
413+ actions = _execute (orchestrator , [], _start ()).actions
414+ assert [(action .id , action .scheduleTask .name ) for action in actions ] == [(1 , "work" )]
415+
416+
417+ @pytest .mark .parametrize ("approved_first" , [True , False ])
418+ def test_winner_completion_and_result_idioms (approved_first : bool ) -> None :
419+ observed : list [Any ] = []
420+
421+ async def orchestrator (ctx : task .AsyncOrchestrationContext , _ : Any ) -> str :
422+ eager = ctx .as_task_context ()
423+ approval = eager .wait_for_external_event ("approval" , data_type = bool )
424+ deadline = eager .create_timer (timedelta (hours = 1 ))
425+ winner = await task .when_any ([approval , deadline ])
426+ observed .append ((winner is approval , approval .is_complete , deadline .is_complete ))
427+ if winner is approval :
428+ deadline .cancel ()
429+ return f"approved:{ approval .get_result ()} "
430+
431+ report = asyncio .create_task (ctx .call_activity ("report" , return_type = str ))
432+ await asyncio .sleep (0 )
433+ observed .append (report .done ())
434+ return f"timed-out:{ await report } "
435+
436+ deadline = NOW + timedelta (hours = 1 )
437+ old = _start () + [helpers .new_timer_created_event (1 , deadline )]
438+ if approved_first :
439+ new = [helpers .new_event_raised_event ("approval" , "true" )]
440+ for _ in range (2 ):
441+ observed .clear ()
442+ complete = _completion (_execute (orchestrator , old , new ))
443+ assert json .loads (complete .result .value ) == "approved:True"
444+ assert observed == [(True , True , False )]
445+ return
446+
447+ fired = [helpers .new_orchestrator_started_event (deadline ), helpers .new_timer_fired_event (1 , deadline )]
448+ actions = _execute (orchestrator , old , fired ).actions
449+ assert [action .scheduleTask .name for action in actions ] == ["report" ]
450+ assert observed == [(False , False , True ), False ]
451+ old += fired + [helpers .new_task_scheduled_event (2 , "report" )]
452+ for _ in range (2 ):
453+ observed .clear ()
454+ complete = _completion (_execute (orchestrator , old , [helpers .new_task_completed_event (2 , '"sent"' )]))
455+ assert json .loads (complete .result .value ) == "timed-out:sent"
456+ assert observed == [(False , False , True ), False ]
457+
458+
351459def test_owned_timer_cancellation_does_not_cancel_borrowed_timer () -> None :
352460 async def orchestrator (ctx : task .AsyncOrchestrationContext , _ : Any ) -> None :
353461 borrowed = ctx .as_task_context ().create_timer (timedelta (seconds = 10 ))
0 commit comments