@@ -75,13 +75,13 @@ type testTimer struct {
75
75
// ScheduleEventID is the ID of the schedule event for this timer
76
76
ScheduleEventID int64
77
77
78
- // At is the time this timer is scheduled for. This will advance the mock clock
79
- // to this timestamp
78
+ // At is the time this timer is scheduled for
80
79
At time.Time
81
80
82
- // Callback is called when the timer should fire. It can return a history event which
83
- // will be added to the event history being executed.
81
+ // Callback is called when the timer should fire.
84
82
Callback func ()
83
+
84
+ wallClockTimer * clock.Timer
85
85
}
86
86
87
87
type testWorkflow struct {
@@ -122,9 +122,12 @@ type workflowTester[TResult any] struct {
122
122
123
123
workflowHistory []* history.Event
124
124
clock * clock.Mock
125
+ wallClock clock.Clock
125
126
startTime time.Time
126
127
128
+ sync.Map
127
129
timers []* testTimer
130
+ nextTimer * testTimer
128
131
callbacks chan func () * history.WorkflowEvent
129
132
130
133
subWorkflowListener func (* core.WorkflowInstance , string )
@@ -159,9 +162,13 @@ func WithTestTimeout(timeout time.Duration) WorkflowTesterOption {
159
162
}
160
163
161
164
func NewWorkflowTester [TResult any ](wf interface {}, opts ... WorkflowTesterOption ) WorkflowTester [TResult ] {
162
- // Start with the current wall-clock time
163
- clock := clock .NewMock ()
164
- clock .Set (time .Now ())
165
+ if err := margs.ReturnTypeMatch [TResult ](wf ); err != nil {
166
+ panic (fmt .Sprintf ("workflow return type does not match: %s" , err ))
167
+ }
168
+
169
+ // Start with the current wall-c time
170
+ c := clock .NewMock ()
171
+ c .Set (time .Now ())
165
172
166
173
wfi := core .NewWorkflowInstance (uuid .NewString (), uuid .NewString ())
167
174
registry := workflow .NewRegistry ()
@@ -195,12 +202,13 @@ func NewWorkflowTester[TResult any](wf interface{}, opts ...WorkflowTesterOption
195
202
mockedWorkflows : make (map [string ]bool ),
196
203
197
204
workflowHistory : make ([]* history.Event , 0 ),
198
- clock : clock ,
205
+ clock : c ,
206
+ wallClock : clock .New (),
199
207
200
208
timers : make ([]* testTimer , 0 ),
201
209
callbacks : make (chan func () * history.WorkflowEvent , 1024 ),
202
210
203
- logger : options .Logger ,
211
+ logger : options .Logger . With ( "source" , "tester" ) ,
204
212
tracer : tracer ,
205
213
converter : options .Converter ,
206
214
}
@@ -263,7 +271,7 @@ func (wt *workflowTester[TResult]) Execute(args ...interface{}) {
263
271
wt .addWorkflow (wt .wfi , initialEvent )
264
272
265
273
for ! wt .workflowFinished {
266
- // Execute all workflows until no more events?
274
+ // Execute all workflows until no more events
267
275
gotNewEvents := false
268
276
269
277
for _ , tw := range wt .testWorkflows {
@@ -334,14 +342,14 @@ func (wt *workflowTester[TResult]) Execute(args ...interface{}) {
334
342
// Schedule timers
335
343
for _ , timerEvent := range result .TimerEvents {
336
344
gotNewEvents = true
337
- wt .logger .Debug ("Timer event" , "event_type" , timerEvent .Type )
345
+ wt .logger .Debug ("Timer future event" , "event_type" , timerEvent .Type , "at" , * timerEvent . VisibleAt )
338
346
339
347
wt .scheduleTimer (tw .instance , timerEvent )
340
348
}
341
349
}
342
350
343
351
for ! wt .workflowFinished && ! gotNewEvents {
344
- // No new events left and the workflow isn 't finished yet. Check for timers or callbacks
352
+ // No new events left and workflows aren 't finished yet. Check for callbacks
345
353
select {
346
354
case callback := <- wt .callbacks :
347
355
event := callback ()
@@ -353,31 +361,52 @@ func (wt *workflowTester[TResult]) Execute(args ...interface{}) {
353
361
default :
354
362
}
355
363
356
- // If there are no running activities and timers, skip time and jump to the next scheduled timer
357
-
358
- if atomic .LoadInt32 (& wt .runningActivities ) == 0 && len (wt .timers ) > 0 {
364
+ // No callbacks, try to fire any pending timers
365
+ if len (wt .timers ) > 0 && wt .nextTimer == nil {
359
366
// Take first timer and execute it
360
367
t := wt .timers [0 ]
361
368
wt .timers = wt .timers [1 :]
362
369
363
- // Advance workflow clock to fire the timer
364
- wt .logger .Debug ("Advancing workflow clock to fire timer" )
365
- wt .clock .Set (t .At )
366
- t .Callback ()
367
- } else {
368
- t := time .NewTimer (wt .options .TestTimeout )
369
-
370
- select {
371
- case callback := <- wt .callbacks :
372
- event := callback ()
373
- if event != nil {
374
- wt .sendEvent (event .WorkflowInstance , event .HistoryEvent )
375
- gotNewEvents = true
376
- }
377
- case <- t .C :
378
- t .Stop ()
379
- panic ("No new events generated during workflow execution and no pending timers, workflow blocked?" )
370
+ // If there are no running activities, we can time-travel to the next timer and execute it. Otherwise, if
371
+ // there are running activities, only fire the timer if it is due.
372
+ runningActivities := atomic .LoadInt32 (& wt .runningActivities )
373
+ if runningActivities > 0 {
374
+ // Wall-clock mode
375
+ wt .logger .Debug ("Scheduling wall-clock timer" , "at" , t .At )
376
+
377
+ wt .nextTimer = t
378
+
379
+ remainingTime := wt .clock .Until (t .At )
380
+ t .wallClockTimer = wt .wallClock .AfterFunc (remainingTime , func () {
381
+ t .Callback ()
382
+ wt .nextTimer = nil
383
+ })
384
+ } else {
385
+ // Time-travel mode
386
+ wt .logger .Debug ("Advancing workflow clock to fire timer" , "to" , t .At )
387
+
388
+ // Advance workflow clock and fire the timer
389
+ wt .clock .Set (t .At )
390
+ t .Callback ()
380
391
}
392
+
393
+ continue
394
+ }
395
+
396
+ // Wait until a callback is ready or we hit the test/idle timeout
397
+ t := time .NewTimer (wt .options .TestTimeout )
398
+
399
+ select {
400
+ case callback := <- wt .callbacks :
401
+ event := callback ()
402
+ if event != nil {
403
+ wt .sendEvent (event .WorkflowInstance , event .HistoryEvent )
404
+ gotNewEvents = true
405
+ }
406
+
407
+ case <- t .C :
408
+ t .Stop ()
409
+ panic ("No new events generated during workflow execution and no pending timers, workflow blocked?" )
381
410
}
382
411
}
383
412
}
@@ -569,6 +598,11 @@ func (wt *workflowTester[TResult]) scheduleTimer(instance *core.WorkflowInstance
569
598
func (wt * workflowTester [TResult ]) cancelTimer (instance * core.WorkflowInstance , event * history.Event ) {
570
599
for i , t := range wt .timers {
571
600
if t .Instance != nil && t .Instance .InstanceID == instance .InstanceID && t .ScheduleEventID == event .ScheduleEventID {
601
+ // If this was the next timer to fire, stop the timer
602
+ if t .wallClockTimer != nil {
603
+ t .wallClockTimer .Stop ()
604
+ }
605
+
572
606
wt .timers = append (wt .timers [:i ], wt .timers [i + 1 :]... )
573
607
break
574
608
}
0 commit comments