@@ -153,6 +153,8 @@ type (
153
153
onTimerScheduledListener func (timerID string , duration time.Duration )
154
154
onTimerFiredListener func (timerID string )
155
155
onTimerCancelledListener func (timerID string )
156
+
157
+ cronMaxIterations int
156
158
}
157
159
158
160
// testWorkflowEnvironmentImpl is the environment that runs the workflow/activity unit tests.
@@ -183,6 +185,11 @@ type (
183
185
184
186
workerStopChannel chan struct {}
185
187
sessionEnvironment * testSessionEnvironmentImpl
188
+
189
+ cronSchedule string
190
+ cronIterations int
191
+ workflowInput []byte
192
+
186
193
}
187
194
188
195
testSessionEnvironmentImpl struct {
@@ -225,6 +232,8 @@ func newTestWorkflowEnvironmentImpl(s *WorkflowTestSuite, parentRegistry *regist
225
232
testTimeout : time .Second * 3 ,
226
233
227
234
expectedMockCalls : make (map [string ]struct {}),
235
+
236
+ cronMaxIterations : - 1 ,
228
237
},
229
238
230
239
workflowInfo : & WorkflowInfo {
@@ -246,13 +255,19 @@ func newTestWorkflowEnvironmentImpl(s *WorkflowTestSuite, parentRegistry *regist
246
255
247
256
doneChannel : make (chan struct {}),
248
257
workerStopChannel : make (chan struct {}),
258
+
259
+ cronIterations : 0 ,
249
260
}
250
261
251
262
// move forward the mock clock to start time.
252
263
env .setStartTime (time .Now ())
253
264
254
265
// put current workflow as a running workflow so child can send signal to parent
255
- env .runningWorkflows [env .workflowInfo .WorkflowExecution .ID ] = & testWorkflowHandle {env : env , callback : func (result []byte , err error ) {}}
266
+ testWorkflowHandle := & testWorkflowHandle {env : env , callback : func (result []byte , err error ) {}}
267
+ if env .workflowInfo .CronSchedule != nil && len (* env .workflowInfo .CronSchedule ) > 0 {
268
+ testWorkflowHandle .params .cronSchedule = * env .workflowInfo .CronSchedule
269
+ }
270
+ env .runningWorkflows [env .workflowInfo .WorkflowExecution .ID ] = testWorkflowHandle
256
271
257
272
if env .logger == nil {
258
273
logger , _ := zap .NewDevelopment ()
@@ -329,7 +344,14 @@ func (env *testWorkflowEnvironmentImpl) setStartTime(startTime time.Time) {
329
344
startTime = env .wallClock .Now ()
330
345
}
331
346
env .mockClock .Add (startTime .Sub (env .mockClock .Now ()))
347
+ }
348
+
349
+ func (env * testWorkflowEnvironmentImpl ) setCronSchedule (cronSchedule string ) {
350
+ env .workflowInfo .CronSchedule = & cronSchedule
351
+ }
332
352
353
+ func (env * testWorkflowEnvironmentImpl ) setCronMaxIterationas (cronMaxIterations int ) {
354
+ env .cronMaxIterations = cronMaxIterations
333
355
}
334
356
335
357
func (env * testWorkflowEnvironmentImpl ) newTestWorkflowEnvironmentForChild (params * executeWorkflowParams , callback resultHandler , startedHandler func (r WorkflowExecution , e error )) (* testWorkflowEnvironmentImpl , error ) {
@@ -457,6 +479,8 @@ func (env *testWorkflowEnvironmentImpl) executeWorkflowInternal(delayStart time.
457
479
panic (err )
458
480
}
459
481
env .workflowDef = workflowDefinition
482
+ // Store the Workflow input for potential Cron
483
+ env .workflowInput = input
460
484
461
485
// env.workflowDef.Execute() method will execute dispatcher. We want the dispatcher to only run in main loop.
462
486
// In case of child workflow, this executeWorkflowInternal() is run in separate goroutinue, so use postCallback
@@ -784,7 +808,10 @@ func (env *testWorkflowEnvironmentImpl) Complete(result []byte, err error) {
784
808
}
785
809
786
810
dc := env .GetDataConverter ()
787
- env .isTestCompleted = true
811
+ // Test is potentially not over, for parent Cron workflows
812
+ if (! env .isChildWorkflow () && ! env .IsCron ()) || env .isChildWorkflow () {
813
+ env .isTestCompleted = true
814
+ }
788
815
789
816
if err != nil {
790
817
switch err := err .(type ) {
@@ -800,7 +827,12 @@ func (env *testWorkflowEnvironmentImpl) Complete(result []byte, err error) {
800
827
env .testResult = newEncodedValue (result , dc )
801
828
}
802
829
803
- close (env .doneChannel )
830
+ // Only close on:
831
+ // 1. Child-Workflows
832
+ // 2. non-cron Workflows
833
+ if env .isChildWorkflow () && ! env .IsCron () {
834
+ close (env .doneChannel )
835
+ }
804
836
805
837
if env .isChildWorkflow () {
806
838
// this is completion of child workflow
@@ -810,7 +842,7 @@ func (env *testWorkflowEnvironmentImpl) Complete(result []byte, err error) {
810
842
// would have already been removed from the runningWorkflows map by RequestCancelWorkflow().
811
843
childWorkflowHandle .handled = true
812
844
// check if a retry is needed
813
- if childWorkflowHandle .rerunAsChild ( ) {
845
+ if childWorkflowHandle .rerun ( true ) {
814
846
// rerun requested, so we don't want to post the error to parent workflow, return here.
815
847
return
816
848
}
@@ -825,12 +857,27 @@ func (env *testWorkflowEnvironmentImpl) Complete(result []byte, err error) {
825
857
}
826
858
}, true /* true to trigger parent workflow to resume to handle child workflow's result */ )
827
859
}
860
+ } else {
861
+ if env .IsCron () {
862
+ workflowID := env .workflowInfo .WorkflowExecution .ID
863
+ if workflowHandle , ok := env .runningWorkflows [workflowID ]; ok {
864
+ // On rerun, consider Workflow as not-handled
865
+ if workflowHandle .rerun (false ) {
866
+ return
867
+ }
868
+ }
869
+ }
828
870
}
871
+ // No Reruns....Test is Complete
872
+ env .isTestCompleted = true
829
873
}
830
874
831
- func (h * testWorkflowHandle ) rerunAsChild ( ) bool {
875
+ func (h * testWorkflowHandle ) rerun ( asChild bool ) bool {
832
876
env := h .env
833
- if ! env .isChildWorkflow () {
877
+ if asChild && ! env .isChildWorkflow () {
878
+ return false
879
+ }
880
+ if ! asChild && env .isChildWorkflow () {
834
881
return false
835
882
}
836
883
params := h .params
@@ -844,40 +891,74 @@ func (h *testWorkflowHandle) rerunAsChild() bool {
844
891
// not successful run this time, carry over from whatever previous run pass to this run.
845
892
result = env .workflowInfo .lastCompletionResult
846
893
}
847
- params .lastCompletionResult = result
894
+ if asChild {
895
+ params .lastCompletionResult = result
848
896
849
- if params .retryPolicy != nil && env .testError != nil {
850
- errReason , _ := getErrorDetails (env .testError , env .GetDataConverter ())
851
- var expireTime time.Time
852
- if params .retryPolicy .GetExpirationIntervalInSeconds () > 0 {
853
- expireTime = params .scheduledTime .Add (time .Second * time .Duration (params .retryPolicy .GetExpirationIntervalInSeconds ()))
854
- }
855
- backoff := getRetryBackoffFromThriftRetryPolicy (params .retryPolicy , env .workflowInfo .Attempt , errReason , env .Now (), expireTime )
856
- if backoff > 0 {
857
- // remove the current child workflow from the pending child workflow map because
858
- // the childWorkflowID will be the same for retry run.
859
- delete (env .runningWorkflows , env .workflowInfo .WorkflowExecution .ID )
860
- params .attempt ++
861
- env .parentEnv .executeChildWorkflowWithDelay (backoff , * params , h .callback , nil /* child workflow already started */ )
862
-
863
- return true
897
+ if params .retryPolicy != nil && env .testError != nil {
898
+ errReason , _ := getErrorDetails (env .testError , env .GetDataConverter ())
899
+ var expireTime time.Time
900
+ if params .retryPolicy .GetExpirationIntervalInSeconds () > 0 {
901
+ expireTime = params .scheduledTime .Add (time .Second * time .Duration (params .retryPolicy .GetExpirationIntervalInSeconds ()))
902
+ }
903
+ backoff := getRetryBackoffFromThriftRetryPolicy (params .retryPolicy , env .workflowInfo .Attempt , errReason , env .Now (), expireTime )
904
+ if backoff > 0 {
905
+ // remove the current child workflow from the pending child workflow map because
906
+ // the childWorkflowID will be the same for retry run.
907
+ delete (env .runningWorkflows , env .workflowInfo .WorkflowExecution .ID )
908
+ params .attempt ++
909
+ env .parentEnv .executeChildWorkflowWithDelay (backoff , * params , h .callback , nil /* child workflow already started */ )
910
+ return true
911
+ }
864
912
}
865
- }
866
-
867
- if len (params .cronSchedule ) > 0 {
868
- schedule , err := cron .ParseStandard (params .cronSchedule )
869
- if err != nil {
870
- panic (fmt .Errorf ("invalid cron schedule %v, err: %v" , params .cronSchedule , err ))
913
+ if len (params .cronSchedule ) > 0 {
914
+ if env .cronMaxIterations < 0 || (env .cronMaxIterations > 0 && env .cronIterations < env .cronMaxIterations ) {
915
+ schedule , err := cron .ParseStandard (params .cronSchedule )
916
+ if err != nil {
917
+ panic (fmt .Errorf ("invalid cron schedule %v, err: %v" , params .cronSchedule , err ))
918
+ }
919
+ workflowNow := env .Now ().In (time .UTC )
920
+ backoff := schedule .Next (workflowNow ).Sub (workflowNow )
921
+ if backoff > 0 {
922
+ env .cronIterations += 1
923
+ delete (env .runningWorkflows , env .workflowInfo .WorkflowExecution .ID )
924
+ params .attempt = 0
925
+ params .scheduledTime = env .Now ()
926
+ env .parentEnv .executeChildWorkflowWithDelay (backoff , * params , h .callback , nil /* child workflow already started */ )
927
+ return true
928
+ }
929
+ }
871
930
}
872
-
873
- workflowNow := env .Now ().In (time .UTC )
874
- backoff := schedule .Next (workflowNow ).Sub (workflowNow )
875
- if backoff > 0 {
876
- delete (env .runningWorkflows , env .workflowInfo .WorkflowExecution .ID )
877
- params .attempt = 0
878
- params .scheduledTime = env .Now ()
879
- env .parentEnv .executeChildWorkflowWithDelay (backoff , * params , h .callback , nil /* child workflow already started */ )
880
- return true
931
+ } else {
932
+ // Re-run a non-Child workflow if it has a Cron Schedule
933
+ if h .env .workflowInfo .CronSchedule != nil {
934
+ if env .cronMaxIterations < 0 || (env .cronMaxIterations > 0 && env .cronIterations < env .cronMaxIterations ) {
935
+ cronSchedule := * h .env .workflowInfo .CronSchedule
936
+ if len (cronSchedule ) == 0 {
937
+ return false
938
+ }
939
+ schedule , err := cron .ParseStandard (cronSchedule )
940
+ if err != nil {
941
+ panic (fmt .Errorf ("invalid cron schedule %v, err: %v" , cronSchedule , err ))
942
+ }
943
+ workflowNow := env .Now ().In (time .UTC )
944
+ backoff := schedule .Next (workflowNow ).Sub (workflowNow )
945
+ if backoff > 0 {
946
+ env .cronIterations += 1
947
+ // Prepare the env for the next iteration
948
+ env .runningCount --
949
+ env .setLastCompletionResult (result )
950
+ // Since MainLoop is already running, we just want to execute the dispatcher
951
+ // which will run the Workflow,
952
+ env .registerDelayedCallback (func () {
953
+ env .runningCount ++
954
+ env .workflowDef , _ = env .getWorkflowDefinition (env .workflowInfo .WorkflowType )
955
+ // Use the existing headers and input
956
+ env .workflowDef .Execute (env , env .header , env .workflowInput )
957
+ env .startDecisionTask ()
958
+ }, backoff - backoff )
959
+ return true
960
+ }
961
+ }
881
962
}
882
963
}
883
964
@@ -1722,6 +1803,11 @@ func (env *testWorkflowEnvironmentImpl) IsReplaying() bool {
1722
1803
return false
1723
1804
}
1724
1805
1806
+ func (env * testWorkflowEnvironmentImpl ) IsCron () bool {
1807
+ // this test environment never replay
1808
+ return env .workflowInfo .CronSchedule != nil && len (* env .workflowInfo .CronSchedule ) > 0
1809
+ }
1810
+
1725
1811
func (env * testWorkflowEnvironmentImpl ) SignalExternalWorkflow (domainName , workflowID , runID , signalName string , input []byte , arg interface {}, childWorkflowOnly bool , callback resultHandler ) {
1726
1812
// check if target workflow is a known workflow
1727
1813
if childHandle , ok := env .runningWorkflows [workflowID ]; ok {
0 commit comments