azure-functions-durable 1.2.9__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. azure/durable_functions/__init__.py +81 -81
  2. azure/durable_functions/constants.py +9 -9
  3. azure/durable_functions/decorators/__init__.py +3 -3
  4. azure/durable_functions/decorators/durable_app.py +260 -249
  5. azure/durable_functions/decorators/metadata.py +109 -109
  6. azure/durable_functions/entity.py +129 -125
  7. azure/durable_functions/models/DurableEntityContext.py +201 -201
  8. azure/durable_functions/models/DurableHttpRequest.py +58 -58
  9. azure/durable_functions/models/DurableOrchestrationBindings.py +66 -66
  10. azure/durable_functions/models/DurableOrchestrationClient.py +812 -781
  11. azure/durable_functions/models/DurableOrchestrationContext.py +761 -707
  12. azure/durable_functions/models/DurableOrchestrationStatus.py +156 -156
  13. azure/durable_functions/models/EntityStateResponse.py +23 -23
  14. azure/durable_functions/models/FunctionContext.py +7 -7
  15. azure/durable_functions/models/OrchestrationRuntimeStatus.py +32 -32
  16. azure/durable_functions/models/OrchestratorState.py +117 -116
  17. azure/durable_functions/models/PurgeHistoryResult.py +33 -33
  18. azure/durable_functions/models/ReplaySchema.py +9 -8
  19. azure/durable_functions/models/RetryOptions.py +69 -69
  20. azure/durable_functions/models/RpcManagementOptions.py +86 -86
  21. azure/durable_functions/models/Task.py +540 -426
  22. azure/durable_functions/models/TaskOrchestrationExecutor.py +352 -336
  23. azure/durable_functions/models/TokenSource.py +56 -56
  24. azure/durable_functions/models/__init__.py +26 -24
  25. azure/durable_functions/models/actions/Action.py +23 -23
  26. azure/durable_functions/models/actions/ActionType.py +18 -18
  27. azure/durable_functions/models/actions/CallActivityAction.py +41 -41
  28. azure/durable_functions/models/actions/CallActivityWithRetryAction.py +45 -45
  29. azure/durable_functions/models/actions/CallEntityAction.py +46 -46
  30. azure/durable_functions/models/actions/CallHttpAction.py +35 -35
  31. azure/durable_functions/models/actions/CallSubOrchestratorAction.py +40 -40
  32. azure/durable_functions/models/actions/CallSubOrchestratorWithRetryAction.py +44 -44
  33. azure/durable_functions/models/actions/CompoundAction.py +35 -35
  34. azure/durable_functions/models/actions/ContinueAsNewAction.py +36 -36
  35. azure/durable_functions/models/actions/CreateTimerAction.py +48 -48
  36. azure/durable_functions/models/actions/NoOpAction.py +35 -35
  37. azure/durable_functions/models/actions/SignalEntityAction.py +47 -47
  38. azure/durable_functions/models/actions/WaitForExternalEventAction.py +63 -63
  39. azure/durable_functions/models/actions/WhenAllAction.py +14 -14
  40. azure/durable_functions/models/actions/WhenAnyAction.py +14 -14
  41. azure/durable_functions/models/actions/__init__.py +24 -24
  42. azure/durable_functions/models/entities/EntityState.py +74 -74
  43. azure/durable_functions/models/entities/OperationResult.py +94 -76
  44. azure/durable_functions/models/entities/RequestMessage.py +53 -53
  45. azure/durable_functions/models/entities/ResponseMessage.py +48 -48
  46. azure/durable_functions/models/entities/Signal.py +62 -62
  47. azure/durable_functions/models/entities/__init__.py +17 -17
  48. azure/durable_functions/models/history/HistoryEvent.py +92 -92
  49. azure/durable_functions/models/history/HistoryEventType.py +27 -27
  50. azure/durable_functions/models/history/__init__.py +8 -8
  51. azure/durable_functions/models/utils/__init__.py +7 -7
  52. azure/durable_functions/models/utils/entity_utils.py +103 -91
  53. azure/durable_functions/models/utils/http_utils.py +80 -69
  54. azure/durable_functions/models/utils/json_utils.py +96 -56
  55. azure/durable_functions/orchestrator.py +73 -71
  56. azure/durable_functions/testing/OrchestratorGeneratorWrapper.py +42 -0
  57. azure/durable_functions/testing/__init__.py +6 -0
  58. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/LICENSE +21 -21
  59. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/METADATA +59 -58
  60. azure_functions_durable-1.3.0.dist-info/RECORD +103 -0
  61. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/WHEEL +1 -1
  62. tests/models/test_DecoratorMetadata.py +135 -135
  63. tests/models/test_Decorators.py +107 -107
  64. tests/models/test_DurableOrchestrationBindings.py +68 -68
  65. tests/models/test_DurableOrchestrationClient.py +730 -730
  66. tests/models/test_DurableOrchestrationContext.py +102 -102
  67. tests/models/test_DurableOrchestrationStatus.py +59 -59
  68. tests/models/test_OrchestrationState.py +28 -28
  69. tests/models/test_RpcManagementOptions.py +79 -79
  70. tests/models/test_TokenSource.py +10 -10
  71. tests/orchestrator/models/OrchestrationInstance.py +18 -18
  72. tests/orchestrator/orchestrator_test_utils.py +130 -130
  73. tests/orchestrator/schemas/OrchetrationStateSchema.py +66 -66
  74. tests/orchestrator/test_call_http.py +235 -176
  75. tests/orchestrator/test_continue_as_new.py +67 -67
  76. tests/orchestrator/test_create_timer.py +126 -126
  77. tests/orchestrator/test_entity.py +397 -395
  78. tests/orchestrator/test_external_event.py +53 -53
  79. tests/orchestrator/test_fan_out_fan_in.py +175 -175
  80. tests/orchestrator/test_is_replaying_flag.py +101 -101
  81. tests/orchestrator/test_retries.py +308 -308
  82. tests/orchestrator/test_sequential_orchestrator.py +841 -841
  83. tests/orchestrator/test_sequential_orchestrator_custom_status.py +119 -119
  84. tests/orchestrator/test_sequential_orchestrator_with_retry.py +465 -465
  85. tests/orchestrator/test_serialization.py +30 -30
  86. tests/orchestrator/test_sub_orchestrator.py +95 -95
  87. tests/orchestrator/test_sub_orchestrator_with_retry.py +129 -129
  88. tests/orchestrator/test_task_any.py +60 -60
  89. tests/tasks/tasks_test_utils.py +17 -17
  90. tests/tasks/test_long_timers.py +70 -0
  91. tests/tasks/test_new_uuid.py +34 -34
  92. tests/test_utils/ContextBuilder.py +174 -174
  93. tests/test_utils/EntityContextBuilder.py +56 -56
  94. tests/test_utils/constants.py +1 -1
  95. tests/test_utils/json_utils.py +30 -30
  96. tests/test_utils/testClasses.py +56 -56
  97. tests/utils/__init__.py +1 -0
  98. tests/utils/test_entity_utils.py +24 -0
  99. azure_functions_durable-1.2.9.data/data/_manifest/bsi.json +0 -1
  100. azure_functions_durable-1.2.9.data/data/_manifest/manifest.cat +0 -0
  101. azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json +0 -11985
  102. azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json.sha256 +0 -1
  103. azure_functions_durable-1.2.9.dist-info/RECORD +0 -102
  104. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/top_level.txt +0 -0
@@ -1,841 +1,841 @@
1
- from azure.durable_functions.models.actions.WhenAnyAction import WhenAnyAction
2
- from azure.durable_functions.models.actions.WhenAllAction import WhenAllAction
3
- from azure.durable_functions.models.ReplaySchema import ReplaySchema
4
- from datetime import datetime, timedelta
5
- from .orchestrator_test_utils \
6
- import assert_orchestration_state_equals, get_orchestration_state_result, assert_valid_schema
7
- from tests.test_utils.ContextBuilder import ContextBuilder
8
- from azure.durable_functions.models.OrchestratorState import OrchestratorState
9
- from azure.durable_functions.models.actions.CallActivityAction \
10
- import CallActivityAction
11
- from tests.test_utils.testClasses import SerializableClass
12
- import azure.durable_functions as df
13
- import azure.functions as func
14
-
15
- app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS)
16
-
17
- def generator_function(context):
18
- outputs = []
19
-
20
- task1 = yield context.call_activity("Hello", "Tokyo")
21
- task2 = yield context.call_activity("Hello", "Seattle")
22
- task3 = yield context.call_activity("Hello", "London")
23
-
24
- outputs.append(task1)
25
- outputs.append(task2)
26
- outputs.append(task3)
27
-
28
- return outputs
29
-
30
- @app.function_name("generator_function_with_pystein")
31
- @app.orchestration_trigger(context_name="context")
32
- def generator_function_with_pystein(context):
33
- outputs = []
34
-
35
- task1 = yield context.call_activity("Hello", "Tokyo")
36
- task2 = yield context.call_activity("Hello", "Seattle")
37
- task3 = yield context.call_activity("Hello", "London")
38
-
39
- outputs.append(task1)
40
- outputs.append(task2)
41
- outputs.append(task3)
42
-
43
- return outputs
44
-
45
- def generator_function_multi_yield_when_all(context):
46
- outputs = []
47
-
48
- task1 = context.call_activity("Hello", "Tokyo")
49
- yield context.task_all([task1])
50
- result = yield context.task_all([task1])
51
-
52
- return result
53
-
54
- def generator_function_is_replaying(context):
55
- outputs = []
56
-
57
- outputs.append(context.is_replaying)
58
- yield context.call_activity("Hello", "Tokyo")
59
- outputs.append(context.is_replaying)
60
- yield context.call_activity("Hello", "Seattle")
61
- outputs.append(context.is_replaying)
62
- yield context.call_activity("Hello", "London")
63
- return outputs
64
-
65
- def generator_function_no_yield(context):
66
- outputs = []
67
-
68
- task1 = context.call_activity("Hello", "Tokyo")
69
- task2 = context.call_activity("Hello", "Seattle")
70
- task3 = yield context.call_activity("Hello", "London")
71
-
72
- return task3
73
-
74
- def generator_function_duplicate_yield(context):
75
- task1 = context.call_activity("Hello", "Tokyo")
76
- yield task1
77
- yield task1
78
-
79
- return ""
80
-
81
- def generator_function_reducing_when_all(context):
82
- task1 = context.call_activity("Hello", "Tokyo")
83
- task2 = context.call_activity("Hello", "Seattle")
84
- pending_tasks = [task1, task2]
85
-
86
- # Yield until first task is completed
87
- finished_task1 = yield context.task_any(pending_tasks)
88
-
89
- # Remove completed task from pending tasks
90
- pending_tasks.remove(finished_task1)
91
-
92
- # Yield remaining task
93
- yield context.task_any(pending_tasks)
94
-
95
- # Ensure we can still schedule new tasks
96
- yield context.call_activity("Hello", "London")
97
- return ""
98
-
99
-
100
- def generator_function_reuse_task_in_whenany(context):
101
- task1 = context.call_activity("Hello", "Tokyo")
102
- task2 = context.call_activity("Hello", "Seattle")
103
- pending_tasks = [task1, task2]
104
-
105
- # Yield until first task is completed
106
- finished_task1 = yield context.task_any(pending_tasks)
107
-
108
- # Remove completed task from pending tasks
109
- pending_tasks.remove(finished_task1)
110
-
111
- task3 = context.call_activity("Hello", "London")
112
- tasks = pending_tasks + [task3]
113
-
114
- # Yield remaining tasks
115
- yield context.task_any(tasks)
116
- return ""
117
-
118
- def generator_function_compound_tasks(context):
119
- yield context.call_activity("Hello", "Tokyo")
120
-
121
- task1 = context.call_activity("Hello", "Tokyo")
122
- task2 = context.call_activity("Hello", "Tokyo")
123
- task3 = context.call_activity("Hello", "Tokyo")
124
- task4 = context.task_any([task3])
125
- task5 = context.task_all([task1, task2, task4])
126
- task6 = context.task_any([task5])
127
- yield task6
128
-
129
- return ""
130
-
131
- def generator_function_time_is_not_none(context):
132
- outputs = []
133
-
134
- now = context.current_utc_datetime
135
- if not now:
136
- raise Exception("No time! 1st attempt")
137
- task1 = yield context.call_activity("Hello", "Tokyo")
138
-
139
- now = context.current_utc_datetime
140
- if not now:
141
- raise Exception("No time! 2nd attempt")
142
- task2 = yield context.call_activity("Hello", "Seattle")
143
-
144
- now = context.current_utc_datetime
145
- if not now:
146
- raise Exception("No time! 3rd attempt")
147
- task3 = yield context.call_activity("Hello", "London")
148
-
149
- now = context.current_utc_datetime
150
- if not now:
151
- raise Exception("No time! 4th attempt")
152
-
153
- outputs.append(task1)
154
- outputs.append(task2)
155
- outputs.append(task3)
156
-
157
- return outputs
158
-
159
- def generator_function_time_gather(context):
160
- outputs = []
161
-
162
- outputs.append(context.current_utc_datetime.strftime("%m/%d/%Y, %H:%M:%S"))
163
- yield context.call_activity("Hello", "Tokyo")
164
-
165
- outputs.append(context.current_utc_datetime.strftime("%m/%d/%Y, %H:%M:%S"))
166
- yield context.call_activity("Hello", "Seattle")
167
-
168
- outputs.append(context.current_utc_datetime.strftime("%m/%d/%Y, %H:%M:%S"))
169
- yield context.call_activity("Hello", "London")
170
-
171
- outputs.append(context.current_utc_datetime.strftime("%m/%d/%Y, %H:%M:%S"))
172
- return outputs
173
-
174
- def generator_function_rasing_ex(context):
175
- outputs = []
176
-
177
- task1 = yield context.call_activity("Hello", "Tokyo")
178
- task2 = yield context.call_activity("Hello", "Seattle")
179
- task3 = yield context.call_activity("Hello", "London")
180
-
181
- outputs.append(task1)
182
- outputs.append(task2)
183
- outputs.append(task3)
184
-
185
- raise ValueError("Oops!")
186
-
187
- @app.orchestration_trigger(context_name="context")
188
- def generator_function_rasing_ex_with_pystein(context):
189
- outputs = []
190
-
191
- task1 = yield context.call_activity("Hello", "Tokyo")
192
- task2 = yield context.call_activity("Hello", "Seattle")
193
- task3 = yield context.call_activity("Hello", "London")
194
-
195
- outputs.append(task1)
196
- outputs.append(task2)
197
- outputs.append(task3)
198
-
199
- raise ValueError("Oops!")
200
-
201
- def generator_function_with_serialization(context):
202
- """Ochestrator to test sequential activity calls with a serializable input arguments."""
203
- outputs = []
204
-
205
- task1 = yield context.call_activity("Hello", SerializableClass("Tokyo"))
206
- task2 = yield context.call_activity("Hello", SerializableClass("Seattle"))
207
- task3 = yield context.call_activity("Hello", SerializableClass("London"))
208
-
209
- outputs.append(task1)
210
- outputs.append(task2)
211
- outputs.append(task3)
212
-
213
- return outputs
214
-
215
- def generator_function_new_guid(context):
216
- """Simple orchestrator that generates 3 GUIDs"""
217
- outputs = []
218
-
219
- output1 = context.new_guid()
220
- output2 = context.new_guid()
221
- output3 = context.new_guid()
222
-
223
- outputs.append(str(output1))
224
- outputs.append(str(output2))
225
- outputs.append(str(output3))
226
- return outputs
227
-
228
- def generator_function_call_activity_with_name(context):
229
- """Simple orchestrator that call activity function with function name"""
230
- outputs = []
231
-
232
- task1 = yield context.call_activity(Hello, "Tokyo")
233
- task2 = yield context.call_activity(Hello, "Seattle")
234
- task3 = yield context.call_activity(Hello, "London")
235
-
236
- outputs.append(task1)
237
- outputs.append(task2)
238
- outputs.append(task3)
239
-
240
- return outputs
241
-
242
- def generator_function_call_activity_with_callable(context):
243
- outputs = []
244
-
245
- task1 = yield context.call_activity(generator_function, "Tokyo")
246
-
247
- outputs.append(task1)
248
-
249
- return outputs
250
-
251
- def generator_function_call_activity_with_orchestrator(context):
252
- outputs = []
253
-
254
- task1 = yield context.call_activity(generator_function_rasing_ex_with_pystein, "Tokyo")
255
-
256
- outputs.append(task1)
257
-
258
- return outputs
259
-
260
- def generator_function_call_activity_with_none_return(context):
261
- """Simple orchestrator that call activity function which can return None"""
262
- outputs = []
263
-
264
- task1 = yield context.call_activity(hello_return_none, "Tokyo")
265
- task2 = yield context.call_activity(hello_return_none, "Seattle")
266
- task3 = yield context.call_activity(hello_return_none, "London")
267
-
268
- outputs.append(task1)
269
- outputs.append(task2)
270
- outputs.append(task3)
271
-
272
- return outputs
273
-
274
- @app.activity_trigger(input_name = "myArg")
275
- def Hello(myArg: str):
276
- return "Hello" + myArg
277
-
278
- @app.activity_trigger(input_name = "myArg")
279
- def hello_return_none(myArg: str):
280
- if myArg == "London":
281
- return None
282
- else:
283
- return "Hello" + myArg
284
-
285
- def base_expected_state(output=None, replay_schema: ReplaySchema = ReplaySchema.V1) -> OrchestratorState:
286
- return OrchestratorState(is_done=False, actions=[], output=output, replay_schema=replay_schema)
287
-
288
- def add_timer_fired_events(context_builder: ContextBuilder, id_: int, timestamp: str):
289
- fire_at: str = context_builder.add_timer_created_event(id_, timestamp)
290
- context_builder.add_orchestrator_completed_event()
291
- context_builder.add_orchestrator_started_event()
292
- context_builder.add_timer_fired_event(id_=id_, fire_at=fire_at)
293
-
294
- def add_hello_action(state: OrchestratorState, input_: str, activity_name="Hello"):
295
- action = CallActivityAction(function_name=activity_name, input_=input_)
296
- state.actions.append([action])
297
-
298
- def add_hello_completed_events(
299
- context_builder: ContextBuilder, id_: int, result: str, is_played=False, activity_name="Hello"):
300
- context_builder.add_task_scheduled_event(name=activity_name, id_=id_)
301
- context_builder.add_orchestrator_completed_event()
302
- context_builder.add_orchestrator_started_event()
303
- context_builder.add_task_completed_event(id_=id_, result=result, is_played=is_played)
304
-
305
-
306
- def add_hello_failed_events(
307
- context_builder: ContextBuilder, id_: int, reason: str, details: str):
308
- context_builder.add_task_scheduled_event(name='Hello', id_=id_)
309
- context_builder.add_orchestrator_completed_event()
310
- context_builder.add_orchestrator_started_event()
311
- context_builder.add_task_failed_event(
312
- id_=id_, reason=reason, details=details)
313
-
314
-
315
- def test_initial_orchestration_state():
316
- context_builder = ContextBuilder('test_simple_function')
317
-
318
- result = get_orchestration_state_result(
319
- context_builder, generator_function)
320
-
321
- expected_state = base_expected_state()
322
- add_hello_action(expected_state, 'Tokyo')
323
- expected = expected_state.to_json()
324
-
325
- assert_valid_schema(result)
326
- assert_orchestration_state_equals(expected, result)
327
-
328
-
329
- def test_tokyo_state():
330
- context_builder = ContextBuilder('test_simple_function')
331
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
332
-
333
- result = get_orchestration_state_result(
334
- context_builder, generator_function)
335
-
336
- expected_state = base_expected_state()
337
- add_hello_action(expected_state, 'Tokyo')
338
- add_hello_action(expected_state, 'Seattle')
339
- expected = expected_state.to_json()
340
-
341
- assert_valid_schema(result)
342
- assert_orchestration_state_equals(expected, result)
343
-
344
-
345
- def test_failed_tokyo_state():
346
- failed_reason = 'Reasons'
347
- failed_details = 'Stuff and Things'
348
- context_builder = ContextBuilder('test_simple_function')
349
- add_hello_failed_events(
350
- context_builder, 0, failed_reason, failed_details)
351
-
352
- try:
353
- result = get_orchestration_state_result(
354
- context_builder, generator_function)
355
- # expected an exception
356
- assert False
357
- except Exception as e:
358
- error_label = "\n\n$OutOfProcData$:"
359
- error_str = str(e)
360
-
361
- expected_state = base_expected_state()
362
- add_hello_action(expected_state, 'Tokyo')
363
- error_msg = f'{failed_reason} \n {failed_details}'
364
- expected_state._error = error_msg
365
- state_str = expected_state.to_json_string()
366
-
367
- expected_error_str = f"{error_msg}{error_label}{state_str}"
368
- assert expected_error_str == error_str
369
-
370
- def test_call_activity_with_name():
371
- context_builder = ContextBuilder('test_call_activity_with_name')
372
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
373
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
374
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
375
- result = get_orchestration_state_result(
376
- context_builder, generator_function_call_activity_with_name)
377
-
378
- expected_state = base_expected_state(
379
- ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
380
- add_hello_action(expected_state, 'Tokyo')
381
- add_hello_action(expected_state, 'Seattle')
382
- add_hello_action(expected_state, 'London')
383
- expected_state._is_done = True
384
- expected = expected_state.to_json()
385
-
386
- assert_valid_schema(result)
387
- assert_orchestration_state_equals(expected, result)
388
-
389
- def test_call_activity_with_none_return():
390
- context_builder = ContextBuilder('test_call_activity_with_none_return')
391
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"", "hello_return_none")
392
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"", "hello_return_none")
393
- add_hello_completed_events(context_builder, 2, None, "hello_return_none")
394
- result = get_orchestration_state_result(
395
- context_builder, generator_function_call_activity_with_none_return)
396
-
397
- expected_state = base_expected_state(
398
- ['Hello Tokyo!', 'Hello Seattle!', None])
399
- add_hello_action(expected_state, 'Tokyo', "hello_return_none")
400
- add_hello_action(expected_state, 'Seattle', "hello_return_none")
401
- add_hello_action(expected_state, 'London', "hello_return_none")
402
- expected_state._is_done = True
403
- expected = expected_state.to_json()
404
-
405
- assert_valid_schema(result)
406
- assert_orchestration_state_equals(expected, result)
407
-
408
- def test_call_activity_function_callable_exception():
409
- context_builder = ContextBuilder('test_call_activity_by_name_exception')
410
-
411
- try:
412
- result = get_orchestration_state_result(
413
- context_builder, generator_function_call_activity_with_callable)
414
- # expected an exception
415
- assert False
416
- except Exception as e:
417
- error_label = "\n\n$OutOfProcData$:"
418
- error_str = str(e)
419
-
420
- expected_state = base_expected_state()
421
- error_msg = "The `call_activity` API received a `Callable` without an "\
422
- "associated Azure Functions trigger-type. "\
423
- "Please ensure you're using the Python programming model V2 "\
424
- "and that your activity function is annotated with the `activity_trigger`"\
425
- "decorator. Otherwise, provide in the name of the activity as a string."
426
- expected_state._error = error_msg
427
- state_str = expected_state.to_json_string()
428
-
429
- expected_error_str = f"{error_msg}{error_label}{state_str}"
430
- assert expected_error_str == error_str
431
-
432
- def test_call_activity_function_with_orchestrator_exception():
433
- context_builder = ContextBuilder('test_call_activity_by_name_exception')
434
-
435
- try:
436
- result = get_orchestration_state_result(
437
- context_builder, generator_function_call_activity_with_orchestrator)
438
- # expected an exception
439
- assert False
440
- except Exception as e:
441
- error_label = "\n\n$OutOfProcData$:"
442
- error_str = str(e)
443
-
444
- expected_state = base_expected_state()
445
- error_msg = "Received function with Trigger-type `"\
446
- + generator_function_rasing_ex_with_pystein._function._trigger.type\
447
- + "` but expected `ActivityTrigger`. Ensure your "\
448
- "function is annotated with the `ActivityTrigger`" \
449
- " decorator or directly pass in the name of the "\
450
- "function as a string."
451
- expected_state._error = error_msg
452
- state_str = expected_state.to_json_string()
453
-
454
- expected_error_str = f"{error_msg}{error_label}{state_str}"
455
- assert expected_error_str == error_str
456
-
457
- def test_user_code_raises_exception():
458
- context_builder = ContextBuilder('test_simple_function')
459
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
460
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
461
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
462
-
463
- try:
464
- result = get_orchestration_state_result(
465
- context_builder, generator_function_rasing_ex)
466
- # expected an exception
467
- assert False
468
- except Exception as e:
469
- error_label = "\n\n$OutOfProcData$:"
470
- error_str = str(e)
471
-
472
- expected_state = base_expected_state()
473
- add_hello_action(expected_state, 'Tokyo')
474
- add_hello_action(expected_state, 'Seattle')
475
- add_hello_action(expected_state, 'London')
476
- error_msg = 'Oops!'
477
- expected_state._error = error_msg
478
- state_str = expected_state.to_json_string()
479
-
480
- expected_error_str = f"{error_msg}{error_label}{state_str}"
481
- assert expected_error_str == error_str
482
-
483
- def test_user_code_raises_exception_with_pystein():
484
- context_builder = ContextBuilder('test_simple_function')
485
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
486
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
487
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
488
-
489
- try:
490
- result = get_orchestration_state_result(
491
- context_builder, generator_function_rasing_ex_with_pystein,
492
- uses_pystein=True)
493
- # expected an exception
494
- assert False
495
- except Exception as e:
496
- error_label = "\n\n$OutOfProcData$:"
497
- error_str = str(e)
498
-
499
- expected_state = base_expected_state()
500
- add_hello_action(expected_state, 'Tokyo')
501
- add_hello_action(expected_state, 'Seattle')
502
- add_hello_action(expected_state, 'London')
503
- error_msg = 'Oops!'
504
- expected_state._error = error_msg
505
- state_str = expected_state.to_json_string()
506
-
507
- expected_error_str = f"{error_msg}{error_label}{state_str}"
508
- assert expected_error_str == error_str
509
-
510
- def test_tokyo_and_seattle_state():
511
- context_builder = ContextBuilder('test_simple_function')
512
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
513
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
514
-
515
- result = get_orchestration_state_result(
516
- context_builder, generator_function)
517
-
518
- expected_state = base_expected_state()
519
- add_hello_action(expected_state, 'Tokyo')
520
- add_hello_action(expected_state, 'Seattle')
521
- add_hello_action(expected_state, 'London')
522
- expected = expected_state.to_json()
523
-
524
- assert_valid_schema(result)
525
- assert_orchestration_state_equals(expected, result)
526
-
527
-
528
- def test_tokyo_and_seattle_and_london_state():
529
- context_builder = ContextBuilder('test_simple_function')
530
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
531
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
532
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
533
-
534
- result = get_orchestration_state_result(
535
- context_builder, generator_function)
536
-
537
- expected_state = base_expected_state(
538
- ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
539
- add_hello_action(expected_state, 'Tokyo')
540
- add_hello_action(expected_state, 'Seattle')
541
- add_hello_action(expected_state, 'London')
542
- expected_state._is_done = True
543
- expected = expected_state.to_json()
544
-
545
- assert_valid_schema(result)
546
- assert_orchestration_state_equals(expected, result)
547
-
548
- def test_tokyo_and_seattle_and_london_state_pystein():
549
- context_builder = ContextBuilder('test_simple_function')
550
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
551
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
552
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
553
-
554
- result = get_orchestration_state_result(
555
- context_builder, generator_function_with_pystein,
556
- uses_pystein=True)
557
-
558
- expected_state = base_expected_state(
559
- ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
560
- add_hello_action(expected_state, 'Tokyo')
561
- add_hello_action(expected_state, 'Seattle')
562
- add_hello_action(expected_state, 'London')
563
- expected_state._is_done = True
564
- expected = expected_state.to_json()
565
-
566
- assert_valid_schema(result)
567
- assert_orchestration_state_equals(expected, result)
568
-
569
- def test_multi_when_all_yield():
570
- context_builder = ContextBuilder('test_simple_function')
571
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
572
-
573
- result = get_orchestration_state_result(
574
- context_builder, generator_function_multi_yield_when_all)
575
-
576
- expected_state = base_expected_state(
577
- ['Hello Tokyo!'])
578
- add_hello_action(expected_state, 'Tokyo')
579
- expected_state._is_done = True
580
- expected = expected_state.to_json()
581
-
582
- assert_valid_schema(result)
583
- assert_orchestration_state_equals(expected, result)
584
-
585
- def test_sequential_is_replaying():
586
- context_builder = ContextBuilder('test_simple_function', is_replaying=True)
587
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"", True)
588
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"", True)
589
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"", True)
590
-
591
- result = get_orchestration_state_result(
592
- context_builder, generator_function_is_replaying)
593
-
594
- expected_state = base_expected_state(
595
- [True, True, True])
596
- add_hello_action(expected_state, 'Tokyo')
597
- add_hello_action(expected_state, 'Seattle')
598
- add_hello_action(expected_state, 'London')
599
- expected_state._is_done = True
600
- expected = expected_state.to_json()
601
-
602
- assert_valid_schema(result)
603
- assert_orchestration_state_equals(expected, result)
604
-
605
- def test_sequential_orchestration_no_yield():
606
- context_builder = ContextBuilder('test_simple_function')
607
- add_hello_completed_events(context_builder, 0, "\"Hello London!\"")
608
-
609
- result = get_orchestration_state_result(
610
- context_builder, generator_function_no_yield)
611
-
612
- expected_state = base_expected_state('Hello London!')
613
- add_hello_action(expected_state, 'London')
614
- expected_state._is_done = True
615
- expected = expected_state.to_json()
616
-
617
- assert_valid_schema(result)
618
- assert_orchestration_state_equals(expected, result)
619
-
620
-
621
- def test_tokyo_and_seattle_and_london_with_serialization_state():
622
- """Tests the sequential function pattern with custom object serialization.
623
-
624
- This simple test validates that a sequential function pattern returns
625
- the expected state when the input to activities is a user-provided
626
- serializable class.
627
- """
628
- context_builder = ContextBuilder('test_simple_function')
629
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
630
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
631
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
632
-
633
- result = get_orchestration_state_result(
634
- context_builder, generator_function_with_serialization)
635
-
636
- expected_state = base_expected_state(
637
- ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
638
- add_hello_action(expected_state, SerializableClass("Tokyo"))
639
- add_hello_action(expected_state, SerializableClass("Seattle"))
640
- add_hello_action(expected_state, SerializableClass("London"))
641
- expected_state._is_done = True
642
- expected = expected_state.to_json()
643
-
644
- assert_valid_schema(result)
645
- assert_orchestration_state_equals(expected, result)
646
-
647
- def test_utc_time_is_never_none():
648
- """Tests an orchestrator that errors out if its current_utc_datetime is ever None.
649
-
650
- If we receive all activity results, it means we never error'ed out. Our test has
651
- a history events array with identical timestamps, simulating events arriving
652
- very close to one another."""
653
-
654
- # we set `increase_time` to False to make sure the changes are resilient
655
- # to undistinguishable timestamps (events arrive very close to each other)
656
- context_builder = ContextBuilder('test_simple_function', increase_time=False)
657
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
658
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
659
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
660
-
661
- result = get_orchestration_state_result(
662
- context_builder, generator_function_deterministic_utc_time)
663
-
664
- expected_state = base_expected_state(
665
- ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
666
- add_hello_action(expected_state, 'Tokyo')
667
- add_hello_action(expected_state, 'Seattle')
668
- add_hello_action(expected_state, 'London')
669
- expected_state._is_done = True
670
- expected = expected_state.to_json()
671
-
672
- assert_valid_schema(result)
673
- assert_orchestration_state_equals(expected, result)
674
-
675
- def test_utc_time_is_never_none():
676
- """Tests an orchestrator that errors out if its current_utc_datetime is ever None.
677
-
678
- If we receive all activity results, it means we never error'ed out. Our test has
679
- a history events array with identical timestamps, simulating events arriving
680
- very close to one another."""
681
-
682
- # we set `increase_time` to False to make sure the changes are resilient
683
- # to undistinguishable timestamps (events arrive very close to each other)
684
- context_builder = ContextBuilder('test_simple_function', increase_time=False)
685
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
686
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
687
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
688
-
689
- result = get_orchestration_state_result(
690
- context_builder, generator_function_time_is_not_none)
691
-
692
- expected_state = base_expected_state(
693
- ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
694
- add_hello_action(expected_state, 'Tokyo')
695
- add_hello_action(expected_state, 'Seattle')
696
- add_hello_action(expected_state, 'London')
697
- expected_state._is_done = True
698
- expected = expected_state.to_json()
699
-
700
- assert_valid_schema(result)
701
- assert_orchestration_state_equals(expected, result)
702
-
703
- def test_utc_time_updates_correctly():
704
- """Tests that current_utc_datetime updates correctly"""
705
-
706
- now = datetime.utcnow()
707
- # the first orchestrator-started event starts 1 second after `now`
708
- context_builder = ContextBuilder('test_simple_function', starting_time=now)
709
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
710
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
711
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
712
-
713
- result = get_orchestration_state_result(
714
- context_builder, generator_function_time_gather)
715
-
716
- # In the expected history, the orchestrator starts again every 4 seconds
717
- # The current_utc_datetime should update to the orchestrator start event timestamp
718
- num_restarts = 3
719
- expected_utc_time = now + timedelta(seconds=1)
720
- outputs = [expected_utc_time.strftime("%m/%d/%Y, %H:%M:%S")]
721
- for _ in range(num_restarts):
722
- expected_utc_time += timedelta(seconds=4)
723
- outputs.append(expected_utc_time.strftime("%m/%d/%Y, %H:%M:%S"))
724
-
725
- expected_state = base_expected_state(outputs)
726
- add_hello_action(expected_state, 'Tokyo')
727
- add_hello_action(expected_state, 'Seattle')
728
- add_hello_action(expected_state, 'London')
729
- expected_state._is_done = True
730
- expected = expected_state.to_json()
731
-
732
- assert_valid_schema(result)
733
- assert_orchestration_state_equals(expected, result)
734
-
735
- def test_new_guid_orchestrator():
736
- """Tests that the new_guid API is replay-safe and produces new GUIDs every time"""
737
- context_builder = ContextBuilder('test_guid_orchestrator')
738
-
739
- # To test that the API is replay-safe, we generate two orchestrators
740
- # with the same starting context
741
- result1 = get_orchestration_state_result(
742
- context_builder, generator_function_new_guid)
743
- outputs1 = result1["output"]
744
-
745
- result2 = get_orchestration_state_result(
746
- context_builder, generator_function_new_guid)
747
- outputs2 = result2["output"]
748
-
749
- # All GUIDs should be unique
750
- assert len(outputs1) == len(set(outputs1))
751
- # The two GUID lists should be the same
752
- assert outputs1 == outputs2
753
-
754
- def test_duplicate_yields_do_not_add_duplicate_actions():
755
- """Tests that yield'ing a Task twice does not double the task's actions"""
756
- context_builder = ContextBuilder('test_guid_orchestrator')
757
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
758
-
759
- result = get_orchestration_state_result(
760
- context_builder, generator_function_duplicate_yield)
761
-
762
- expected_state = base_expected_state("")
763
- add_hello_action(expected_state, 'Tokyo')
764
- expected_state._is_done = True
765
- expected = expected_state.to_json()
766
-
767
- assert_valid_schema(result)
768
- assert_orchestration_state_equals(expected, result)
769
-
770
- def test_reducing_when_any_pattern():
771
- """Tests that a user can call when_any on a progressively smaller list of already scheduled tasks"""
772
- context_builder = ContextBuilder('test_reducing_when_any', replay_schema=ReplaySchema.V2)
773
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
774
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
775
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
776
-
777
- result = get_orchestration_state_result(
778
- context_builder, generator_function_reducing_when_all)
779
-
780
- # this scenario is only supported for V2 replay
781
- expected_state = base_expected_state("",replay_schema=ReplaySchema.V2)
782
- expected_state._actions = [
783
- [WhenAnyAction(
784
- [CallActivityAction("Hello", "Seattle"), CallActivityAction("Hello", "Tokyo")]),
785
- CallActivityAction("Hello", "London")
786
- ]
787
- ]
788
-
789
- expected_state._is_done = True
790
- expected = expected_state.to_json()
791
-
792
- assert_orchestration_state_equals(expected, result)
793
-
794
- def test_reducing_when_any_pattern():
795
- """Tests that a user can call when_any on a progressively smaller list of already scheduled tasks"""
796
- context_builder = ContextBuilder('generator_function_reuse_task_in_whenany', replay_schema=ReplaySchema.V2)
797
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
798
- add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
799
- add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
800
-
801
- result = get_orchestration_state_result(
802
- context_builder, generator_function_reuse_task_in_whenany)
803
-
804
- # this scenario is only supported for V2 replay
805
- expected_state = base_expected_state("",replay_schema=ReplaySchema.V2)
806
- expected_state._actions = [
807
- [WhenAnyAction(
808
- [CallActivityAction("Hello", "Seattle"), CallActivityAction("Hello", "Tokyo")]),
809
- WhenAnyAction(
810
- [CallActivityAction("Hello", "London")])
811
- ]
812
- ]
813
-
814
- expected_state._is_done = True
815
- expected = expected_state.to_json()
816
-
817
- assert_orchestration_state_equals(expected, result)
818
-
819
- def test_compound_tasks_return_single_action_in_V2():
820
- """Tests that compound tasks, in the v2 replay schema, are represented as a single "deep" action"""
821
- context_builder = ContextBuilder('test_v2_replay_schema', replay_schema=ReplaySchema.V2)
822
- add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
823
-
824
- result = get_orchestration_state_result(
825
- context_builder, generator_function_compound_tasks)
826
-
827
- expected_state = base_expected_state(replay_schema=ReplaySchema.V2)
828
- expected_state._actions = [
829
- [CallActivityAction("Hello", "Tokyo"), WhenAnyAction(
830
- [WhenAllAction(
831
- [CallActivityAction("Hello", "Tokyo"), CallActivityAction("Hello", "Tokyo"), WhenAnyAction(
832
- [CallActivityAction("Hello", "Tokyo")])
833
- ])
834
- ])
835
- ]
836
- ]
837
- expected_state._is_done = False
838
- expected = expected_state.to_json()
839
-
840
- #assert_valid_schema(result)
841
- assert_orchestration_state_equals(expected, result)
1
+ from azure.durable_functions.models.actions.WhenAnyAction import WhenAnyAction
2
+ from azure.durable_functions.models.actions.WhenAllAction import WhenAllAction
3
+ from azure.durable_functions.models.ReplaySchema import ReplaySchema
4
+ from datetime import datetime, timedelta
5
+ from .orchestrator_test_utils \
6
+ import assert_orchestration_state_equals, get_orchestration_state_result, assert_valid_schema
7
+ from tests.test_utils.ContextBuilder import ContextBuilder
8
+ from azure.durable_functions.models.OrchestratorState import OrchestratorState
9
+ from azure.durable_functions.models.actions.CallActivityAction \
10
+ import CallActivityAction
11
+ from tests.test_utils.testClasses import SerializableClass
12
+ import azure.durable_functions as df
13
+ import azure.functions as func
14
+
15
+ app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS)
16
+
17
+ def generator_function(context):
18
+ outputs = []
19
+
20
+ task1 = yield context.call_activity("Hello", "Tokyo")
21
+ task2 = yield context.call_activity("Hello", "Seattle")
22
+ task3 = yield context.call_activity("Hello", "London")
23
+
24
+ outputs.append(task1)
25
+ outputs.append(task2)
26
+ outputs.append(task3)
27
+
28
+ return outputs
29
+
30
+ @app.function_name("generator_function_with_pystein")
31
+ @app.orchestration_trigger(context_name="context")
32
+ def generator_function_with_pystein(context):
33
+ outputs = []
34
+
35
+ task1 = yield context.call_activity("Hello", "Tokyo")
36
+ task2 = yield context.call_activity("Hello", "Seattle")
37
+ task3 = yield context.call_activity("Hello", "London")
38
+
39
+ outputs.append(task1)
40
+ outputs.append(task2)
41
+ outputs.append(task3)
42
+
43
+ return outputs
44
+
45
+ def generator_function_multi_yield_when_all(context):
46
+ outputs = []
47
+
48
+ task1 = context.call_activity("Hello", "Tokyo")
49
+ yield context.task_all([task1])
50
+ result = yield context.task_all([task1])
51
+
52
+ return result
53
+
54
+ def generator_function_is_replaying(context):
55
+ outputs = []
56
+
57
+ outputs.append(context.is_replaying)
58
+ yield context.call_activity("Hello", "Tokyo")
59
+ outputs.append(context.is_replaying)
60
+ yield context.call_activity("Hello", "Seattle")
61
+ outputs.append(context.is_replaying)
62
+ yield context.call_activity("Hello", "London")
63
+ return outputs
64
+
65
+ def generator_function_no_yield(context):
66
+ outputs = []
67
+
68
+ task1 = context.call_activity("Hello", "Tokyo")
69
+ task2 = context.call_activity("Hello", "Seattle")
70
+ task3 = yield context.call_activity("Hello", "London")
71
+
72
+ return task3
73
+
74
+ def generator_function_duplicate_yield(context):
75
+ task1 = context.call_activity("Hello", "Tokyo")
76
+ yield task1
77
+ yield task1
78
+
79
+ return ""
80
+
81
+ def generator_function_reducing_when_all(context):
82
+ task1 = context.call_activity("Hello", "Tokyo")
83
+ task2 = context.call_activity("Hello", "Seattle")
84
+ pending_tasks = [task1, task2]
85
+
86
+ # Yield until first task is completed
87
+ finished_task1 = yield context.task_any(pending_tasks)
88
+
89
+ # Remove completed task from pending tasks
90
+ pending_tasks.remove(finished_task1)
91
+
92
+ # Yield remaining task
93
+ yield context.task_any(pending_tasks)
94
+
95
+ # Ensure we can still schedule new tasks
96
+ yield context.call_activity("Hello", "London")
97
+ return ""
98
+
99
+
100
+ def generator_function_reuse_task_in_whenany(context):
101
+ task1 = context.call_activity("Hello", "Tokyo")
102
+ task2 = context.call_activity("Hello", "Seattle")
103
+ pending_tasks = [task1, task2]
104
+
105
+ # Yield until first task is completed
106
+ finished_task1 = yield context.task_any(pending_tasks)
107
+
108
+ # Remove completed task from pending tasks
109
+ pending_tasks.remove(finished_task1)
110
+
111
+ task3 = context.call_activity("Hello", "London")
112
+ tasks = pending_tasks + [task3]
113
+
114
+ # Yield remaining tasks
115
+ yield context.task_any(tasks)
116
+ return ""
117
+
118
+ def generator_function_compound_tasks(context):
119
+ yield context.call_activity("Hello", "Tokyo")
120
+
121
+ task1 = context.call_activity("Hello", "Tokyo")
122
+ task2 = context.call_activity("Hello", "Tokyo")
123
+ task3 = context.call_activity("Hello", "Tokyo")
124
+ task4 = context.task_any([task3])
125
+ task5 = context.task_all([task1, task2, task4])
126
+ task6 = context.task_any([task5])
127
+ yield task6
128
+
129
+ return ""
130
+
131
+ def generator_function_time_is_not_none(context):
132
+ outputs = []
133
+
134
+ now = context.current_utc_datetime
135
+ if not now:
136
+ raise Exception("No time! 1st attempt")
137
+ task1 = yield context.call_activity("Hello", "Tokyo")
138
+
139
+ now = context.current_utc_datetime
140
+ if not now:
141
+ raise Exception("No time! 2nd attempt")
142
+ task2 = yield context.call_activity("Hello", "Seattle")
143
+
144
+ now = context.current_utc_datetime
145
+ if not now:
146
+ raise Exception("No time! 3rd attempt")
147
+ task3 = yield context.call_activity("Hello", "London")
148
+
149
+ now = context.current_utc_datetime
150
+ if not now:
151
+ raise Exception("No time! 4th attempt")
152
+
153
+ outputs.append(task1)
154
+ outputs.append(task2)
155
+ outputs.append(task3)
156
+
157
+ return outputs
158
+
159
+ def generator_function_time_gather(context):
160
+ outputs = []
161
+
162
+ outputs.append(context.current_utc_datetime.strftime("%m/%d/%Y, %H:%M:%S"))
163
+ yield context.call_activity("Hello", "Tokyo")
164
+
165
+ outputs.append(context.current_utc_datetime.strftime("%m/%d/%Y, %H:%M:%S"))
166
+ yield context.call_activity("Hello", "Seattle")
167
+
168
+ outputs.append(context.current_utc_datetime.strftime("%m/%d/%Y, %H:%M:%S"))
169
+ yield context.call_activity("Hello", "London")
170
+
171
+ outputs.append(context.current_utc_datetime.strftime("%m/%d/%Y, %H:%M:%S"))
172
+ return outputs
173
+
174
+ def generator_function_rasing_ex(context):
175
+ outputs = []
176
+
177
+ task1 = yield context.call_activity("Hello", "Tokyo")
178
+ task2 = yield context.call_activity("Hello", "Seattle")
179
+ task3 = yield context.call_activity("Hello", "London")
180
+
181
+ outputs.append(task1)
182
+ outputs.append(task2)
183
+ outputs.append(task3)
184
+
185
+ raise ValueError("Oops!")
186
+
187
+ @app.orchestration_trigger(context_name="context")
188
+ def generator_function_rasing_ex_with_pystein(context):
189
+ outputs = []
190
+
191
+ task1 = yield context.call_activity("Hello", "Tokyo")
192
+ task2 = yield context.call_activity("Hello", "Seattle")
193
+ task3 = yield context.call_activity("Hello", "London")
194
+
195
+ outputs.append(task1)
196
+ outputs.append(task2)
197
+ outputs.append(task3)
198
+
199
+ raise ValueError("Oops!")
200
+
201
+ def generator_function_with_serialization(context):
202
+ """Ochestrator to test sequential activity calls with a serializable input arguments."""
203
+ outputs = []
204
+
205
+ task1 = yield context.call_activity("Hello", SerializableClass("Tokyo"))
206
+ task2 = yield context.call_activity("Hello", SerializableClass("Seattle"))
207
+ task3 = yield context.call_activity("Hello", SerializableClass("London"))
208
+
209
+ outputs.append(task1)
210
+ outputs.append(task2)
211
+ outputs.append(task3)
212
+
213
+ return outputs
214
+
215
+ def generator_function_new_guid(context):
216
+ """Simple orchestrator that generates 3 GUIDs"""
217
+ outputs = []
218
+
219
+ output1 = context.new_guid()
220
+ output2 = context.new_guid()
221
+ output3 = context.new_guid()
222
+
223
+ outputs.append(str(output1))
224
+ outputs.append(str(output2))
225
+ outputs.append(str(output3))
226
+ return outputs
227
+
228
+ def generator_function_call_activity_with_name(context):
229
+ """Simple orchestrator that call activity function with function name"""
230
+ outputs = []
231
+
232
+ task1 = yield context.call_activity(Hello, "Tokyo")
233
+ task2 = yield context.call_activity(Hello, "Seattle")
234
+ task3 = yield context.call_activity(Hello, "London")
235
+
236
+ outputs.append(task1)
237
+ outputs.append(task2)
238
+ outputs.append(task3)
239
+
240
+ return outputs
241
+
242
+ def generator_function_call_activity_with_callable(context):
243
+ outputs = []
244
+
245
+ task1 = yield context.call_activity(generator_function, "Tokyo")
246
+
247
+ outputs.append(task1)
248
+
249
+ return outputs
250
+
251
+ def generator_function_call_activity_with_orchestrator(context):
252
+ outputs = []
253
+
254
+ task1 = yield context.call_activity(generator_function_rasing_ex_with_pystein, "Tokyo")
255
+
256
+ outputs.append(task1)
257
+
258
+ return outputs
259
+
260
+ def generator_function_call_activity_with_none_return(context):
261
+ """Simple orchestrator that call activity function which can return None"""
262
+ outputs = []
263
+
264
+ task1 = yield context.call_activity(hello_return_none, "Tokyo")
265
+ task2 = yield context.call_activity(hello_return_none, "Seattle")
266
+ task3 = yield context.call_activity(hello_return_none, "London")
267
+
268
+ outputs.append(task1)
269
+ outputs.append(task2)
270
+ outputs.append(task3)
271
+
272
+ return outputs
273
+
274
+ @app.activity_trigger(input_name = "myArg")
275
+ def Hello(myArg: str):
276
+ return "Hello" + myArg
277
+
278
+ @app.activity_trigger(input_name = "myArg")
279
+ def hello_return_none(myArg: str):
280
+ if myArg == "London":
281
+ return None
282
+ else:
283
+ return "Hello" + myArg
284
+
285
+ def base_expected_state(output=None, replay_schema: ReplaySchema = ReplaySchema.V1) -> OrchestratorState:
286
+ return OrchestratorState(is_done=False, actions=[], output=output, replay_schema=replay_schema)
287
+
288
+ def add_timer_fired_events(context_builder: ContextBuilder, id_: int, timestamp: str):
289
+ fire_at: str = context_builder.add_timer_created_event(id_, timestamp)
290
+ context_builder.add_orchestrator_completed_event()
291
+ context_builder.add_orchestrator_started_event()
292
+ context_builder.add_timer_fired_event(id_=id_, fire_at=fire_at)
293
+
294
+ def add_hello_action(state: OrchestratorState, input_: str, activity_name="Hello"):
295
+ action = CallActivityAction(function_name=activity_name, input_=input_)
296
+ state.actions.append([action])
297
+
298
+ def add_hello_completed_events(
299
+ context_builder: ContextBuilder, id_: int, result: str, is_played=False, activity_name="Hello"):
300
+ context_builder.add_task_scheduled_event(name=activity_name, id_=id_)
301
+ context_builder.add_orchestrator_completed_event()
302
+ context_builder.add_orchestrator_started_event()
303
+ context_builder.add_task_completed_event(id_=id_, result=result, is_played=is_played)
304
+
305
+
306
+ def add_hello_failed_events(
307
+ context_builder: ContextBuilder, id_: int, reason: str, details: str):
308
+ context_builder.add_task_scheduled_event(name='Hello', id_=id_)
309
+ context_builder.add_orchestrator_completed_event()
310
+ context_builder.add_orchestrator_started_event()
311
+ context_builder.add_task_failed_event(
312
+ id_=id_, reason=reason, details=details)
313
+
314
+
315
+ def test_initial_orchestration_state():
316
+ context_builder = ContextBuilder('test_simple_function')
317
+
318
+ result = get_orchestration_state_result(
319
+ context_builder, generator_function)
320
+
321
+ expected_state = base_expected_state()
322
+ add_hello_action(expected_state, 'Tokyo')
323
+ expected = expected_state.to_json()
324
+
325
+ assert_valid_schema(result)
326
+ assert_orchestration_state_equals(expected, result)
327
+
328
+
329
+ def test_tokyo_state():
330
+ context_builder = ContextBuilder('test_simple_function')
331
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
332
+
333
+ result = get_orchestration_state_result(
334
+ context_builder, generator_function)
335
+
336
+ expected_state = base_expected_state()
337
+ add_hello_action(expected_state, 'Tokyo')
338
+ add_hello_action(expected_state, 'Seattle')
339
+ expected = expected_state.to_json()
340
+
341
+ assert_valid_schema(result)
342
+ assert_orchestration_state_equals(expected, result)
343
+
344
+
345
+ def test_failed_tokyo_state():
346
+ failed_reason = 'Reasons'
347
+ failed_details = 'Stuff and Things'
348
+ context_builder = ContextBuilder('test_simple_function')
349
+ add_hello_failed_events(
350
+ context_builder, 0, failed_reason, failed_details)
351
+
352
+ try:
353
+ result = get_orchestration_state_result(
354
+ context_builder, generator_function)
355
+ # expected an exception
356
+ assert False
357
+ except Exception as e:
358
+ error_label = "\n\n$OutOfProcData$:"
359
+ error_str = str(e)
360
+
361
+ expected_state = base_expected_state()
362
+ add_hello_action(expected_state, 'Tokyo')
363
+ error_msg = f'{failed_reason} \n {failed_details}'
364
+ expected_state._error = error_msg
365
+ state_str = expected_state.to_json_string()
366
+
367
+ expected_error_str = f"{error_msg}{error_label}{state_str}"
368
+ assert expected_error_str == error_str
369
+
370
+ def test_call_activity_with_name():
371
+ context_builder = ContextBuilder('test_call_activity_with_name')
372
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
373
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
374
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
375
+ result = get_orchestration_state_result(
376
+ context_builder, generator_function_call_activity_with_name)
377
+
378
+ expected_state = base_expected_state(
379
+ ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
380
+ add_hello_action(expected_state, 'Tokyo')
381
+ add_hello_action(expected_state, 'Seattle')
382
+ add_hello_action(expected_state, 'London')
383
+ expected_state._is_done = True
384
+ expected = expected_state.to_json()
385
+
386
+ assert_valid_schema(result)
387
+ assert_orchestration_state_equals(expected, result)
388
+
389
+ def test_call_activity_with_none_return():
390
+ context_builder = ContextBuilder('test_call_activity_with_none_return')
391
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"", "hello_return_none")
392
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"", "hello_return_none")
393
+ add_hello_completed_events(context_builder, 2, None, "hello_return_none")
394
+ result = get_orchestration_state_result(
395
+ context_builder, generator_function_call_activity_with_none_return)
396
+
397
+ expected_state = base_expected_state(
398
+ ['Hello Tokyo!', 'Hello Seattle!', None])
399
+ add_hello_action(expected_state, 'Tokyo', "hello_return_none")
400
+ add_hello_action(expected_state, 'Seattle', "hello_return_none")
401
+ add_hello_action(expected_state, 'London', "hello_return_none")
402
+ expected_state._is_done = True
403
+ expected = expected_state.to_json()
404
+
405
+ assert_valid_schema(result)
406
+ assert_orchestration_state_equals(expected, result)
407
+
408
+ def test_call_activity_function_callable_exception():
409
+ context_builder = ContextBuilder('test_call_activity_by_name_exception')
410
+
411
+ try:
412
+ result = get_orchestration_state_result(
413
+ context_builder, generator_function_call_activity_with_callable)
414
+ # expected an exception
415
+ assert False
416
+ except Exception as e:
417
+ error_label = "\n\n$OutOfProcData$:"
418
+ error_str = str(e)
419
+
420
+ expected_state = base_expected_state()
421
+ error_msg = "The `call_activity` API received a `Callable` without an "\
422
+ "associated Azure Functions trigger-type. "\
423
+ "Please ensure you're using the Python programming model V2 "\
424
+ "and that your activity function is annotated with the `activity_trigger`"\
425
+ "decorator. Otherwise, provide in the name of the activity as a string."
426
+ expected_state._error = error_msg
427
+ state_str = expected_state.to_json_string()
428
+
429
+ expected_error_str = f"{error_msg}{error_label}{state_str}"
430
+ assert expected_error_str == error_str
431
+
432
+ def test_call_activity_function_with_orchestrator_exception():
433
+ context_builder = ContextBuilder('test_call_activity_by_name_exception')
434
+
435
+ try:
436
+ result = get_orchestration_state_result(
437
+ context_builder, generator_function_call_activity_with_orchestrator)
438
+ # expected an exception
439
+ assert False
440
+ except Exception as e:
441
+ error_label = "\n\n$OutOfProcData$:"
442
+ error_str = str(e)
443
+
444
+ expected_state = base_expected_state()
445
+ error_msg = "Received function with Trigger-type `"\
446
+ + generator_function_rasing_ex_with_pystein._function._trigger.type\
447
+ + "` but expected `ActivityTrigger`. Ensure your "\
448
+ "function is annotated with the `ActivityTrigger`" \
449
+ " decorator or directly pass in the name of the "\
450
+ "function as a string."
451
+ expected_state._error = error_msg
452
+ state_str = expected_state.to_json_string()
453
+
454
+ expected_error_str = f"{error_msg}{error_label}{state_str}"
455
+ assert expected_error_str == error_str
456
+
457
+ def test_user_code_raises_exception():
458
+ context_builder = ContextBuilder('test_simple_function')
459
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
460
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
461
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
462
+
463
+ try:
464
+ result = get_orchestration_state_result(
465
+ context_builder, generator_function_rasing_ex)
466
+ # expected an exception
467
+ assert False
468
+ except Exception as e:
469
+ error_label = "\n\n$OutOfProcData$:"
470
+ error_str = str(e)
471
+
472
+ expected_state = base_expected_state()
473
+ add_hello_action(expected_state, 'Tokyo')
474
+ add_hello_action(expected_state, 'Seattle')
475
+ add_hello_action(expected_state, 'London')
476
+ error_msg = 'Oops!'
477
+ expected_state._error = error_msg
478
+ state_str = expected_state.to_json_string()
479
+
480
+ expected_error_str = f"{error_msg}{error_label}{state_str}"
481
+ assert expected_error_str == error_str
482
+
483
+ def test_user_code_raises_exception_with_pystein():
484
+ context_builder = ContextBuilder('test_simple_function')
485
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
486
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
487
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
488
+
489
+ try:
490
+ result = get_orchestration_state_result(
491
+ context_builder, generator_function_rasing_ex_with_pystein,
492
+ uses_pystein=True)
493
+ # expected an exception
494
+ assert False
495
+ except Exception as e:
496
+ error_label = "\n\n$OutOfProcData$:"
497
+ error_str = str(e)
498
+
499
+ expected_state = base_expected_state()
500
+ add_hello_action(expected_state, 'Tokyo')
501
+ add_hello_action(expected_state, 'Seattle')
502
+ add_hello_action(expected_state, 'London')
503
+ error_msg = 'Oops!'
504
+ expected_state._error = error_msg
505
+ state_str = expected_state.to_json_string()
506
+
507
+ expected_error_str = f"{error_msg}{error_label}{state_str}"
508
+ assert expected_error_str == error_str
509
+
510
+ def test_tokyo_and_seattle_state():
511
+ context_builder = ContextBuilder('test_simple_function')
512
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
513
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
514
+
515
+ result = get_orchestration_state_result(
516
+ context_builder, generator_function)
517
+
518
+ expected_state = base_expected_state()
519
+ add_hello_action(expected_state, 'Tokyo')
520
+ add_hello_action(expected_state, 'Seattle')
521
+ add_hello_action(expected_state, 'London')
522
+ expected = expected_state.to_json()
523
+
524
+ assert_valid_schema(result)
525
+ assert_orchestration_state_equals(expected, result)
526
+
527
+
528
+ def test_tokyo_and_seattle_and_london_state():
529
+ context_builder = ContextBuilder('test_simple_function')
530
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
531
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
532
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
533
+
534
+ result = get_orchestration_state_result(
535
+ context_builder, generator_function)
536
+
537
+ expected_state = base_expected_state(
538
+ ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
539
+ add_hello_action(expected_state, 'Tokyo')
540
+ add_hello_action(expected_state, 'Seattle')
541
+ add_hello_action(expected_state, 'London')
542
+ expected_state._is_done = True
543
+ expected = expected_state.to_json()
544
+
545
+ assert_valid_schema(result)
546
+ assert_orchestration_state_equals(expected, result)
547
+
548
+ def test_tokyo_and_seattle_and_london_state_pystein():
549
+ context_builder = ContextBuilder('test_simple_function')
550
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
551
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
552
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
553
+
554
+ result = get_orchestration_state_result(
555
+ context_builder, generator_function_with_pystein,
556
+ uses_pystein=True)
557
+
558
+ expected_state = base_expected_state(
559
+ ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
560
+ add_hello_action(expected_state, 'Tokyo')
561
+ add_hello_action(expected_state, 'Seattle')
562
+ add_hello_action(expected_state, 'London')
563
+ expected_state._is_done = True
564
+ expected = expected_state.to_json()
565
+
566
+ assert_valid_schema(result)
567
+ assert_orchestration_state_equals(expected, result)
568
+
569
+ def test_multi_when_all_yield():
570
+ context_builder = ContextBuilder('test_simple_function')
571
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
572
+
573
+ result = get_orchestration_state_result(
574
+ context_builder, generator_function_multi_yield_when_all)
575
+
576
+ expected_state = base_expected_state(
577
+ ['Hello Tokyo!'])
578
+ add_hello_action(expected_state, 'Tokyo')
579
+ expected_state._is_done = True
580
+ expected = expected_state.to_json()
581
+
582
+ assert_valid_schema(result)
583
+ assert_orchestration_state_equals(expected, result)
584
+
585
+ def test_sequential_is_replaying():
586
+ context_builder = ContextBuilder('test_simple_function', is_replaying=True)
587
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"", True)
588
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"", True)
589
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"", True)
590
+
591
+ result = get_orchestration_state_result(
592
+ context_builder, generator_function_is_replaying)
593
+
594
+ expected_state = base_expected_state(
595
+ [True, True, True])
596
+ add_hello_action(expected_state, 'Tokyo')
597
+ add_hello_action(expected_state, 'Seattle')
598
+ add_hello_action(expected_state, 'London')
599
+ expected_state._is_done = True
600
+ expected = expected_state.to_json()
601
+
602
+ assert_valid_schema(result)
603
+ assert_orchestration_state_equals(expected, result)
604
+
605
+ def test_sequential_orchestration_no_yield():
606
+ context_builder = ContextBuilder('test_simple_function')
607
+ add_hello_completed_events(context_builder, 0, "\"Hello London!\"")
608
+
609
+ result = get_orchestration_state_result(
610
+ context_builder, generator_function_no_yield)
611
+
612
+ expected_state = base_expected_state('Hello London!')
613
+ add_hello_action(expected_state, 'London')
614
+ expected_state._is_done = True
615
+ expected = expected_state.to_json()
616
+
617
+ assert_valid_schema(result)
618
+ assert_orchestration_state_equals(expected, result)
619
+
620
+
621
+ def test_tokyo_and_seattle_and_london_with_serialization_state():
622
+ """Tests the sequential function pattern with custom object serialization.
623
+
624
+ This simple test validates that a sequential function pattern returns
625
+ the expected state when the input to activities is a user-provided
626
+ serializable class.
627
+ """
628
+ context_builder = ContextBuilder('test_simple_function')
629
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
630
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
631
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
632
+
633
+ result = get_orchestration_state_result(
634
+ context_builder, generator_function_with_serialization)
635
+
636
+ expected_state = base_expected_state(
637
+ ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
638
+ add_hello_action(expected_state, SerializableClass("Tokyo"))
639
+ add_hello_action(expected_state, SerializableClass("Seattle"))
640
+ add_hello_action(expected_state, SerializableClass("London"))
641
+ expected_state._is_done = True
642
+ expected = expected_state.to_json()
643
+
644
+ assert_valid_schema(result)
645
+ assert_orchestration_state_equals(expected, result)
646
+
647
+ def test_utc_time_is_never_none():
648
+ """Tests an orchestrator that errors out if its current_utc_datetime is ever None.
649
+
650
+ If we receive all activity results, it means we never error'ed out. Our test has
651
+ a history events array with identical timestamps, simulating events arriving
652
+ very close to one another."""
653
+
654
+ # we set `increase_time` to False to make sure the changes are resilient
655
+ # to undistinguishable timestamps (events arrive very close to each other)
656
+ context_builder = ContextBuilder('test_simple_function', increase_time=False)
657
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
658
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
659
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
660
+
661
+ result = get_orchestration_state_result(
662
+ context_builder, generator_function_deterministic_utc_time)
663
+
664
+ expected_state = base_expected_state(
665
+ ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
666
+ add_hello_action(expected_state, 'Tokyo')
667
+ add_hello_action(expected_state, 'Seattle')
668
+ add_hello_action(expected_state, 'London')
669
+ expected_state._is_done = True
670
+ expected = expected_state.to_json()
671
+
672
+ assert_valid_schema(result)
673
+ assert_orchestration_state_equals(expected, result)
674
+
675
+ def test_utc_time_is_never_none():
676
+ """Tests an orchestrator that errors out if its current_utc_datetime is ever None.
677
+
678
+ If we receive all activity results, it means we never error'ed out. Our test has
679
+ a history events array with identical timestamps, simulating events arriving
680
+ very close to one another."""
681
+
682
+ # we set `increase_time` to False to make sure the changes are resilient
683
+ # to undistinguishable timestamps (events arrive very close to each other)
684
+ context_builder = ContextBuilder('test_simple_function', increase_time=False)
685
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
686
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
687
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
688
+
689
+ result = get_orchestration_state_result(
690
+ context_builder, generator_function_time_is_not_none)
691
+
692
+ expected_state = base_expected_state(
693
+ ['Hello Tokyo!', 'Hello Seattle!', 'Hello London!'])
694
+ add_hello_action(expected_state, 'Tokyo')
695
+ add_hello_action(expected_state, 'Seattle')
696
+ add_hello_action(expected_state, 'London')
697
+ expected_state._is_done = True
698
+ expected = expected_state.to_json()
699
+
700
+ assert_valid_schema(result)
701
+ assert_orchestration_state_equals(expected, result)
702
+
703
+ def test_utc_time_updates_correctly():
704
+ """Tests that current_utc_datetime updates correctly"""
705
+
706
+ now = datetime.utcnow()
707
+ # the first orchestrator-started event starts 1 second after `now`
708
+ context_builder = ContextBuilder('test_simple_function', starting_time=now)
709
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
710
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
711
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
712
+
713
+ result = get_orchestration_state_result(
714
+ context_builder, generator_function_time_gather)
715
+
716
+ # In the expected history, the orchestrator starts again every 4 seconds
717
+ # The current_utc_datetime should update to the orchestrator start event timestamp
718
+ num_restarts = 3
719
+ expected_utc_time = now + timedelta(seconds=1)
720
+ outputs = [expected_utc_time.strftime("%m/%d/%Y, %H:%M:%S")]
721
+ for _ in range(num_restarts):
722
+ expected_utc_time += timedelta(seconds=4)
723
+ outputs.append(expected_utc_time.strftime("%m/%d/%Y, %H:%M:%S"))
724
+
725
+ expected_state = base_expected_state(outputs)
726
+ add_hello_action(expected_state, 'Tokyo')
727
+ add_hello_action(expected_state, 'Seattle')
728
+ add_hello_action(expected_state, 'London')
729
+ expected_state._is_done = True
730
+ expected = expected_state.to_json()
731
+
732
+ assert_valid_schema(result)
733
+ assert_orchestration_state_equals(expected, result)
734
+
735
+ def test_new_guid_orchestrator():
736
+ """Tests that the new_guid API is replay-safe and produces new GUIDs every time"""
737
+ context_builder = ContextBuilder('test_guid_orchestrator')
738
+
739
+ # To test that the API is replay-safe, we generate two orchestrators
740
+ # with the same starting context
741
+ result1 = get_orchestration_state_result(
742
+ context_builder, generator_function_new_guid)
743
+ outputs1 = result1["output"]
744
+
745
+ result2 = get_orchestration_state_result(
746
+ context_builder, generator_function_new_guid)
747
+ outputs2 = result2["output"]
748
+
749
+ # All GUIDs should be unique
750
+ assert len(outputs1) == len(set(outputs1))
751
+ # The two GUID lists should be the same
752
+ assert outputs1 == outputs2
753
+
754
+ def test_duplicate_yields_do_not_add_duplicate_actions():
755
+ """Tests that yield'ing a Task twice does not double the task's actions"""
756
+ context_builder = ContextBuilder('test_guid_orchestrator')
757
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
758
+
759
+ result = get_orchestration_state_result(
760
+ context_builder, generator_function_duplicate_yield)
761
+
762
+ expected_state = base_expected_state("")
763
+ add_hello_action(expected_state, 'Tokyo')
764
+ expected_state._is_done = True
765
+ expected = expected_state.to_json()
766
+
767
+ assert_valid_schema(result)
768
+ assert_orchestration_state_equals(expected, result)
769
+
770
+ def test_reducing_when_any_pattern():
771
+ """Tests that a user can call when_any on a progressively smaller list of already scheduled tasks"""
772
+ context_builder = ContextBuilder('test_reducing_when_any', replay_schema=ReplaySchema.V2)
773
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
774
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
775
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
776
+
777
+ result = get_orchestration_state_result(
778
+ context_builder, generator_function_reducing_when_all)
779
+
780
+ # this scenario is only supported for V2 replay
781
+ expected_state = base_expected_state("",replay_schema=ReplaySchema.V2)
782
+ expected_state._actions = [
783
+ [WhenAnyAction(
784
+ [CallActivityAction("Hello", "Seattle"), CallActivityAction("Hello", "Tokyo")]),
785
+ CallActivityAction("Hello", "London")
786
+ ]
787
+ ]
788
+
789
+ expected_state._is_done = True
790
+ expected = expected_state.to_json()
791
+
792
+ assert_orchestration_state_equals(expected, result)
793
+
794
+ def test_reducing_when_any_pattern():
795
+ """Tests that a user can call when_any on a progressively smaller list of already scheduled tasks"""
796
+ context_builder = ContextBuilder('generator_function_reuse_task_in_whenany', replay_schema=ReplaySchema.V2)
797
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
798
+ add_hello_completed_events(context_builder, 1, "\"Hello Seattle!\"")
799
+ add_hello_completed_events(context_builder, 2, "\"Hello London!\"")
800
+
801
+ result = get_orchestration_state_result(
802
+ context_builder, generator_function_reuse_task_in_whenany)
803
+
804
+ # this scenario is only supported for V2 replay
805
+ expected_state = base_expected_state("",replay_schema=ReplaySchema.V2)
806
+ expected_state._actions = [
807
+ [WhenAnyAction(
808
+ [CallActivityAction("Hello", "Seattle"), CallActivityAction("Hello", "Tokyo")]),
809
+ WhenAnyAction(
810
+ [CallActivityAction("Hello", "London")])
811
+ ]
812
+ ]
813
+
814
+ expected_state._is_done = True
815
+ expected = expected_state.to_json()
816
+
817
+ assert_orchestration_state_equals(expected, result)
818
+
819
+ def test_compound_tasks_return_single_action_in_V2():
820
+ """Tests that compound tasks, in the v2 replay schema, are represented as a single "deep" action"""
821
+ context_builder = ContextBuilder('test_v2_replay_schema', replay_schema=ReplaySchema.V2)
822
+ add_hello_completed_events(context_builder, 0, "\"Hello Tokyo!\"")
823
+
824
+ result = get_orchestration_state_result(
825
+ context_builder, generator_function_compound_tasks)
826
+
827
+ expected_state = base_expected_state(replay_schema=ReplaySchema.V2)
828
+ expected_state._actions = [
829
+ [CallActivityAction("Hello", "Tokyo"), WhenAnyAction(
830
+ [WhenAllAction(
831
+ [CallActivityAction("Hello", "Tokyo"), CallActivityAction("Hello", "Tokyo"), WhenAnyAction(
832
+ [CallActivityAction("Hello", "Tokyo")])
833
+ ])
834
+ ])
835
+ ]
836
+ ]
837
+ expected_state._is_done = False
838
+ expected = expected_state.to_json()
839
+
840
+ #assert_valid_schema(result)
841
+ assert_orchestration_state_equals(expected, result)