/ clock / test_clock.go
test_clock.go
 1  package clock
 2  
 3  import (
 4  	"sync"
 5  	"time"
 6  )
 7  
 8  // TestClock can be used in tests to mock time.
 9  type TestClock struct {
10  	currentTime time.Time
11  	timeChanMap map[time.Time][]chan time.Time
12  	timeLock    sync.Mutex
13  	tickSignal  chan time.Duration
14  }
15  
16  // NewTestClock returns a new test clock.
17  func NewTestClock(startTime time.Time) *TestClock {
18  	return &TestClock{
19  		currentTime: startTime,
20  		timeChanMap: make(map[time.Time][]chan time.Time),
21  	}
22  }
23  
24  // NewTestClockWithTickSignal will create a new test clock with an added
25  // channel which will be used to signal when a new ticker is registered.
26  // This is useful when creating a ticker on a separate goroutine and we'd
27  // like to wait for that to happen before advancing the test case.
28  func NewTestClockWithTickSignal(startTime time.Time,
29  	tickSignal chan time.Duration) *TestClock {
30  
31  	testClock := NewTestClock(startTime)
32  	testClock.tickSignal = tickSignal
33  
34  	return testClock
35  }
36  
37  // Now returns the current (test) time.
38  func (c *TestClock) Now() time.Time {
39  	c.timeLock.Lock()
40  	defer c.timeLock.Unlock()
41  
42  	return c.currentTime
43  }
44  
45  // TickAfter returns a channel that will receive a tick after the specified
46  // duration has passed passed by the user set test time.
47  func (c *TestClock) TickAfter(duration time.Duration) <-chan time.Time {
48  	c.timeLock.Lock()
49  	defer func() {
50  		c.timeLock.Unlock()
51  
52  		// Signal that the ticker has been added.
53  		if c.tickSignal != nil {
54  			c.tickSignal <- duration
55  		}
56  	}()
57  
58  	triggerTime := c.currentTime.Add(duration)
59  	ch := make(chan time.Time, 1)
60  
61  	// If already expired, tick immediately.
62  	if !triggerTime.After(c.currentTime) {
63  		ch <- c.currentTime
64  		return ch
65  	}
66  
67  	// Otherwise store the channel until the trigger time is there.
68  	chans := c.timeChanMap[triggerTime]
69  	chans = append(chans, ch)
70  	c.timeChanMap[triggerTime] = chans
71  
72  	return ch
73  }
74  
75  // SetTime sets the (test) time and triggers tick channels when they expire.
76  func (c *TestClock) SetTime(now time.Time) {
77  	c.timeLock.Lock()
78  	defer c.timeLock.Unlock()
79  
80  	c.currentTime = now
81  	remainingChans := make(map[time.Time][]chan time.Time)
82  	for triggerTime, chans := range c.timeChanMap {
83  		// If the trigger time is still in the future, keep this channel
84  		// in the channel map for later.
85  		if triggerTime.After(now) {
86  			remainingChans[triggerTime] = chans
87  			continue
88  		}
89  
90  		for _, c := range chans {
91  			c <- now
92  		}
93  	}
94  
95  	c.timeChanMap = remainingChans
96  }