├── go.mod ├── .travis.yml ├── realtime_test.go ├── interface.go ├── LICENSE ├── example_test.go ├── doc.go ├── realtime.go ├── README.md ├── manual_test.go └── manual.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thejerf/abtime 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.15 4 | - 1.16 5 | - tip 6 | -------------------------------------------------------------------------------- /realtime_test.go: -------------------------------------------------------------------------------- 1 | package abtime 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | // given the simplicity of the implementations here, there isn't much that 9 | // can subtly go wrong, so this is mostly a coverage test, although, it's 10 | // legit to at least ensure all functions are covered and don't crash. 11 | 12 | func TestConcrete(t *testing.T) { 13 | rt := NewRealTime() 14 | rt.Now() 15 | 16 | ch := rt.After(time.Nanosecond, 0) 17 | <-ch 18 | 19 | rt.Sleep(time.Nanosecond, 0) 20 | 21 | ch = rt.Tick(time.Nanosecond, 0) 22 | <-ch 23 | <-ch 24 | 25 | ticker := rt.NewTicker(time.Nanosecond, 0) 26 | ticker.Channel() 27 | ticker.Reset(time.Second) 28 | ticker.Stop() 29 | 30 | sendAfter := make(chan struct{}) 31 | rt.AfterFunc(time.Nanosecond, func() { 32 | sendAfter <- struct{}{} 33 | }, 0) 34 | <-sendAfter 35 | 36 | timer := rt.NewTimer(time.Nanosecond, 0) 37 | if timer.Channel() == nil { 38 | t.Fatal("Channel isn't working properly") 39 | } 40 | timer.Reset(time.Millisecond) 41 | timer.Stop() 42 | } 43 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package abtime 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Ticker defines an interface for the functions that return *time.Ticker 9 | // in the original Time module. 10 | type Ticker interface { 11 | Channel() <-chan time.Time 12 | Reset(time.Duration) 13 | Stop() 14 | } 15 | 16 | // Timer defines an interface for the functions that return *time.Timer 17 | // in the original Time module. 18 | type Timer interface { 19 | Stop() bool 20 | Reset(time.Duration) bool 21 | Channel() <-chan time.Time 22 | } 23 | 24 | // The AbstractTime interface abstracts the time module into an interface. 25 | type AbstractTime interface { 26 | Now() time.Time 27 | After(time.Duration, int) <-chan time.Time 28 | Sleep(time.Duration, int) 29 | Tick(time.Duration, int) <-chan time.Time 30 | NewTicker(time.Duration, int) Ticker 31 | AfterFunc(time.Duration, func(), int) Timer 32 | NewTimer(time.Duration, int) Timer 33 | 34 | WithDeadline(context.Context, time.Time, int) (context.Context, context.CancelFunc) 35 | WithTimeout(context.Context, time.Duration, int) (context.Context, context.CancelFunc) 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014 Barracuda Networks, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package abtime 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // It's best to allocate IDs like this for your time usages. 8 | const ( 9 | timeoutID = iota 10 | ) 11 | 12 | func ExampleAbstractTime() { 13 | // Suppose you have a goroutine feeding you something from a socket, 14 | // and you want to do something if that times out. You can test this 15 | // with: 16 | manualTime := NewManual() 17 | timedOut := make(chan struct{}) 18 | 19 | go ReadSocket(manualTime, timedOut) 20 | 21 | manualTime.Trigger(timeoutID) 22 | 23 | // This will read the struct{}{} from above. Getting here asserts 24 | // that we did what we wanted when we timed out. 25 | <-timedOut 26 | } 27 | 28 | // In production code, at would be a RealTime, and thus use the "real" 29 | // time.After function, ignoring the ID. 30 | func ReadSocket(at AbstractTime, timedOut chan struct{}) { 31 | timeout := at.After(time.Second, timeoutID) 32 | 33 | // in this example, this will never be filled 34 | fromSocket := make(chan []byte) 35 | 36 | select { 37 | case <-fromSocket: 38 | // handle socketData 39 | case <-timeout: 40 | timedOut <- struct{}{} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Package abtime provides abstracted time functionality that can be swapped 4 | between testing and real without changing application code. 5 | 6 | In any code that seriously uses time, such as billing or scheduling code, 7 | best software engineering practices are that you should not directly 8 | access the operating system time. 9 | 10 | Other people's discussions: http://blog.plover.com/prog/Moonpig.html#testing-sucks 11 | http://stackoverflow.com/questions/5622194/time-dependent-unit-tests/5622222#5622222 12 | http://jim-mcbeath.blogspot.com/2009/02/unit-testing-with-dates-and-times.html 13 | 14 | This module wraps the parts of the time module of Go that do access 15 | the OS time directly, as it stands at Go 1.2 and 1.3 (which are both the 16 | same.) Unfortunately, due to the fact I can not re-export types, you'll 17 | still need to import "time" for its types. 18 | 19 | This module declares an interface for time functions AbstractTime, 20 | provides an implementation that simply backs to the "real" time functions 21 | "RealTime", and provides an implementation that allows you to fully control 22 | the time "ManualTime", including setting "now", and requiring you to 23 | manually trigger all time-based events, such as alerts and alarms. 24 | 25 | Since there is no way to distinguish between different calls to the 26 | standard time functions, each of the methods in the AbstractTime interface 27 | adds an "id". The RealTime implementation simply ignores them. The 28 | ManualTime implementations uses these to trigger specific time events. 29 | Be sure to see the example for usage of the ManualTime implementation. 30 | 31 | Avoid re-using IDs on the Tick functions; it becomes confusing which 32 | .Trigger is affecting which Tick. 33 | 34 | Be sure to see the Example below. 35 | 36 | Quality: At the moment I would call this beta code. Go lint clean, go vet 37 | clean, 100% coverage in the tests. You and I both know that doesn't prove 38 | this is bug-free, but at least it shows I care. And bear in mind what 39 | this really provides is a structure, rather than a whackload of code; should 40 | the code prove not quite correct for your project, it will be easy for you 41 | to fix it. 42 | 43 | */ 44 | package abtime 45 | -------------------------------------------------------------------------------- /realtime.go: -------------------------------------------------------------------------------- 1 | package abtime 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // NewRealTime returns a AbTime-conforming object that backs to the 9 | // standard time module. 10 | func NewRealTime() RealTime { 11 | return RealTime{} 12 | } 13 | 14 | // TimerWrap wraps a Timer-conforming wrapper around a *time.Timer. 15 | type TimerWrap struct { 16 | T *time.Timer 17 | } 18 | 19 | // Channel returns the channel the *time.Timer will signal on. 20 | func (tw TimerWrap) Channel() <-chan time.Time { 21 | return tw.T.C 22 | } 23 | 24 | // Stop wraps the *time.Timer.Stop(). 25 | func (tw TimerWrap) Stop() bool { 26 | return tw.T.Stop() 27 | } 28 | 29 | // Reset wraps the *time.Timer.Reset(). 30 | func (tw TimerWrap) Reset(d time.Duration) bool { 31 | return tw.T.Reset(d) 32 | } 33 | 34 | // The RealTime object implements the direct calls to the time module. 35 | type RealTime struct{} 36 | 37 | // Now wraps time.Now. 38 | func (rt RealTime) Now() time.Time { 39 | return time.Now() 40 | } 41 | 42 | // After wraps time.After. 43 | func (rt RealTime) After(d time.Duration, token int) <-chan time.Time { 44 | return time.After(d) 45 | } 46 | 47 | // Sleep wraps time.Sleep. 48 | func (rt RealTime) Sleep(d time.Duration, token int) { 49 | time.Sleep(d) 50 | } 51 | 52 | // Tick wraps time.Tick. 53 | func (rt RealTime) Tick(d time.Duration, token int) <-chan time.Time { 54 | return time.Tick(d) // nolint: megacheck 55 | } 56 | 57 | // NewTicker wraps time.NewTicker. It returns something conforming to the 58 | // abtime.Ticker interface. 59 | func (rt RealTime) NewTicker(d time.Duration, token int) Ticker { 60 | return tickerWrapper{time.NewTicker(d)} 61 | } 62 | 63 | // AfterFunc wraps time.AfterFunc. It returns something conforming to the 64 | // abtime.Timer interface. 65 | func (rt RealTime) AfterFunc(d time.Duration, f func(), token int) Timer { 66 | return TimerWrap{time.AfterFunc(d, f)} 67 | } 68 | 69 | // NewTimer wraps time.NewTimer. It returns something conforming to the 70 | // abtime.Timer interface. 71 | func (rt RealTime) NewTimer(d time.Duration, token int) Timer { 72 | return TimerWrap{time.NewTimer(d)} 73 | } 74 | 75 | type tickerWrapper struct { 76 | *time.Ticker 77 | } 78 | 79 | func (tw tickerWrapper) Channel() <-chan time.Time { 80 | return tw.C 81 | } 82 | 83 | func (tw tickerWrapper) Reset(d time.Duration) { 84 | tw.Ticker.Reset(d) 85 | } 86 | 87 | // WithDeadline wraps context's normal WithDeadline invocation. 88 | func (rt RealTime) WithDeadline(parent context.Context, deadline time.Time, _ int) (context.Context, context.CancelFunc) { 89 | return context.WithDeadline(parent, deadline) 90 | } 91 | 92 | // WithTimeout wraps context's normal WithTimeout invocation. 93 | func (rt RealTime) WithTimeout(parent context.Context, timeout time.Duration, _ int) (context.Context, context.CancelFunc) { 94 | return context.WithTimeout(parent, timeout) 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # abtime 2 | 3 | [![Build Status](https://travis-ci.org/thejerf/abtime.png?branch=master)](https://travis-ci.org/thejerf/abtime) 4 | 5 | go get github.com/thejerf/abtime 6 | 7 | A library for abstracting away from the literal Go time library and the 8 | context's cancellation and timeout libraries, for testing and time control. 9 | 10 | In any code that seriously uses time, such as billing or scheduling code, 11 | best software engineering practices are that you should not directly 12 | access the operating system time. This module provides you with code to 13 | implement that principle in Go. 14 | 15 | See some discussions: 16 | 17 | * [blog.plover.com's Moonpig discussion](http://blog.plover.com/prog/Moonpig.html#testing-sucks) 18 | * [Jon Skeet's discussion on Stack Overflow](http://stackoverflow.com/questions/5622194/time-dependent-unit-tests/5622222#5622222) 19 | * [Jim McBeath's post](http://jim-mcbeath.blogspot.com/2009/02/unit-testing-with-dates-and-times.html) 20 | 21 | This module is fully covered with 22 | [godoc](http://godoc.org/github.com/thejerf/abtime), including examples, 23 | usage, and everything else you might expect from a README.md on GitHub. 24 | (DRY.) 25 | 26 | # Why abtime and not the more popular clock abstractions? 27 | 28 | Most if not all other time testing abstractions for Go attempt to simulate 29 | the passage of time itself. That is, you can set a Timer for a second from 30 | now, then, you tell the time replacement module that one second has passed, 31 | and it will trigger the timer at that point. 32 | 33 | That is indeed simpler for simple use cases than what I have here, and 34 | permits a drop-in interface replacement for the whole module. However, 35 | it does not permit you to test _all_ scenarios, because it is built on 36 | a fundamentally false premise, which is that time is a monotonic, 37 | agreed-upon value for all goroutines. That is not how goroutines "perceive" 38 | time. 39 | 40 | In reality, if you give one goroutine a timer for 1 second in the future, 41 | and another goroutine a timer for 1.1 seconds in the future, it is entirely 42 | possible for the second goroutine to entirely finish its execution before 43 | the first one even gets woken up. (The first goroutine may well have had 44 | its timer triggered, but then immediately descheduled for whatever reason, 45 | while the second runs to completion.) 46 | 47 | Proper testing of complex time-dependent multi-goroutine coordination 48 | requires deeper levels of control than a compatible API can offer. This 49 | package takes the hit of having to add unique IDs to timers and tickers 50 | in order to permit a deeper level of testing, as proper testing of 51 | time-sensitive code must be able to consider the case where events in 52 | different goroutines happen "out of order", because they will. 53 | 54 | If you only have one goroutine using time-based code then this package may 55 | be overkill. However, if you have multiple goroutines interacting with each 56 | other while also referring to the clock, you may find this package is 57 | worthwhile as it will permit you to set up important test scenarios that 58 | drop-in replacements for the time package simply can not express. 59 | 60 | # Changelog 61 | 62 | * 1.0.7: 63 | * Fixups in the internal registration of triggerable events. 64 | * Added wrappers around context.WithTimeout and context.WithCancel that 65 | allow controlled cancellation of contexts like the rest of time-based 66 | code. 67 | * 1.0.6: 68 | * Manual timer needs to reflect whether it was stopped, not _that_ it was 69 | stopped. 70 | * This also cleans up some of the concurrency. This was one of my earlier 71 | libraries. The place where a goroutine is spawned to perform the 72 | various triggered actions should now be more correct. 73 | * 1.0.5: 74 | * (INCORRECT) The manual timer is ALWAYS successfully stopped by a Stop 75 | call, so .Stop must always return true. 76 | * 1.0.4: 77 | * Add ticker.Reset for Go 1.15. This version requires Go 1.15. 78 | * Add proper go module support. 79 | * 1.0.3: 80 | * Fix locking for Unregister and UnregisterAll. 81 | * 1.0.2 82 | * Adds support for unregistering triggers, so the ids can be reused with 83 | the same abtime object. 84 | 85 | As the godoc says, this is a sign of some sort of flaw, but it is not 86 | yet clear how to handle it. I still haven't found a good option for an 87 | API for this stuff. My original goal with abtime was to be as close to 88 | the original `time` API as possible, I'm considering abandoning 89 | that. Though I still don't know what exactly that would look like. 90 | 91 | (Plus, this need some sort of context support now.) 92 | * 1.0.1 93 | * [Issue 3](https://github.com/thejerf/abtime/issues/3) reports a 94 | reversal in the sense of the timer.Reset return value, which is 95 | fixed. While fixing this, a race condition in setting the underlying 96 | value was also fixed. 97 | * 1.0.0 98 | * Initial Release. 99 | 100 | # Stability 101 | 102 | As I have been using this code for a while now and it has stopped changing, 103 | this is now at version 1.0.0. 104 | 105 | # Commit Signing 106 | 107 | Starting with the commit after 3003eee879c, I will be signing this repository 108 | with the ["jerf" keybase account](https://keybase.io/jerf). If you are viewing 109 | this repository through GitHub, you should see the commits as showing as 110 | "verified" in the commit view. 111 | 112 | (Bear in mind that due to the nature of how git commit signing works, there 113 | may be runs of unverified commits; what matters is that the top one is 114 | signed.) 115 | 116 | -------------------------------------------------------------------------------- /manual_test.go: -------------------------------------------------------------------------------- 1 | package abtime 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const ( 11 | afterID = iota 12 | sleepID 13 | tickID 14 | tickID2 15 | afterFuncID 16 | timerID 17 | contextID 18 | childContextID 19 | ) 20 | 21 | func TestAfter(t *testing.T) { 22 | at := NewManual() 23 | 24 | // assuredly trigger before the After is even called 25 | at.Trigger(afterID) 26 | 27 | sent := make(chan time.Time) 28 | go func() { 29 | ch := at.After(time.Second, afterID) 30 | t := <-ch 31 | sent <- t 32 | }() 33 | 34 | result := <-sent 35 | 36 | if result != at.now.Add(time.Second) { 37 | t.Fatal("Got wrong time sent for After") 38 | } 39 | 40 | go func() { 41 | ch := at.After(time.Second, afterID) 42 | t := <-ch 43 | sent <- t 44 | }() 45 | 46 | // Bootstrapping problem; we can't depend on abtime working to test 47 | // abtime... 48 | time.Sleep(time.Millisecond) 49 | at.Trigger(afterID) 50 | 51 | result = <-sent 52 | if result != at.now.Add(time.Second) { 53 | t.Fatal("Got the wrong time sent for the After after the call") 54 | } 55 | 56 | at.Advance(time.Second) 57 | } 58 | 59 | func TestSleep(t *testing.T) { 60 | at := NewManual() 61 | 62 | // trigger the sleep before it even exists 63 | at.Trigger(sleepID) 64 | 65 | finished := make(chan struct{}) 66 | go func() { 67 | at.Sleep(time.Second, sleepID) 68 | finished <- struct{}{} 69 | }() 70 | 71 | <-finished 72 | 73 | go func() { 74 | at.Sleep(time.Second, sleepID) 75 | finished <- struct{}{} 76 | }() 77 | 78 | time.Sleep(time.Millisecond) 79 | at.Trigger(sleepID) 80 | <-finished 81 | 82 | at.Trigger(2, 4) 83 | for i := 0; i < 5; i++ { 84 | go func(innerI int) { 85 | at.Sleep(time.Second, innerI) 86 | finished <- struct{}{} 87 | }(i) 88 | } 89 | time.Sleep(time.Millisecond) 90 | at.Trigger(0, 1, 3) 91 | 92 | for i := 0; i < 5; i++ { 93 | <-finished 94 | } 95 | 96 | // if we get here, we must not have deadlocked 97 | } 98 | 99 | func TestTick(t *testing.T) { 100 | // significance of this date left as an exercise for the reader 101 | testTime := time.Date(2012, 3, 28, 12, 0, 0, 0, time.UTC) 102 | 103 | at := NewManualAtTime(testTime) 104 | if at.Now() != testTime { 105 | t.Fatal("Now is not working correctly.") 106 | } 107 | at.Trigger(tickID) 108 | 109 | received := make(chan time.Time) 110 | go func() { 111 | ch := at.Tick(time.Second, tickID) 112 | tick1 := <-ch 113 | tick2 := <-ch 114 | 115 | received <- tick1 116 | received <- tick2 117 | }() 118 | 119 | time.Sleep(time.Millisecond) 120 | at.Trigger(tickID) 121 | time1 := <-received 122 | time2 := <-received 123 | 124 | if time1 != testTime.Add(time.Second) || time2 != testTime.Add(2*time.Second) { 125 | t.Fatal("tick did not deliver the correct time") 126 | } 127 | 128 | ticker := at.NewTicker(time.Second, tickID2) 129 | ch := ticker.Channel() 130 | at.Trigger(tickID2) 131 | <-ch 132 | ticker.Reset(time.Second) 133 | ticker.Stop() 134 | at.Trigger(tickID2) 135 | // if this test failed, it would hang the test waiting to write on ch. 136 | } 137 | 138 | func TestAfterFunc(t *testing.T) { 139 | at := NewManual() 140 | 141 | funcRun := make(chan struct{}) 142 | 143 | timer := at.AfterFunc(time.Second, func() { 144 | funcRun <- struct{}{} 145 | }, afterFuncID) 146 | 147 | if timer.Channel() != nil { 148 | t.Fatal("Channel on AfterFunc not working properly.") 149 | } 150 | 151 | // not that this really means much here 152 | if timer.Reset(time.Second * 2) { 153 | t.Fatal("Reset should not be returning true here") 154 | } 155 | at.Trigger(afterFuncID) 156 | 157 | <-funcRun 158 | 159 | timer2 := at.AfterFunc(time.Second, func() { 160 | panic("I should never be run!") 161 | }, afterFuncID+1) 162 | 163 | if !timer2.Stop() { 164 | t.Fatal("Stop should not return true like this") 165 | } 166 | at.Trigger(afterFuncID + 1) 167 | if timer2.Stop() || !timer2.Reset(time.Second*3) { 168 | t.Fatal("Stop/Reset should be returning true here") 169 | } 170 | } 171 | 172 | func TestTimer(t *testing.T) { 173 | at := NewManual() 174 | 175 | timer := at.NewTimer(time.Second, timerID) 176 | go func() { 177 | at.Trigger(timerID) 178 | }() 179 | 180 | curTime := <-timer.Channel() 181 | if at.now.Add(time.Second) != curTime { 182 | t.Fatal("Timer not sending proper time") 183 | } 184 | 185 | timer = at.NewTimer(time.Second, timerID) 186 | timer.Reset(2 * time.Second) 187 | if !timer.Stop() { 188 | t.Fatal("Stopping the timer should have returned true") 189 | } 190 | at.Trigger(timerID) 191 | 192 | // no good way to test the stop worked, the Stop description in 193 | // the time package explicitly says it does not close the channel. 194 | } 195 | 196 | // This tests for a bug I encountered in real code. If a timer is Stopped, 197 | // then Reset, it got itself marked as not running permanently, so future 198 | // Stops would return the wrong value, and things trying to drain it would 199 | // hang because there was nothing to drain. 200 | func TestTimerResetRunsCorrectly(t *testing.T) { 201 | at := NewManual() 202 | 203 | timer := at.NewTimer(time.Second, timerID) 204 | 205 | ret := timer.Stop() 206 | if !ret { 207 | t.Fatal("timer should return true to indicate Stop() stopped it") 208 | } 209 | ret = timer.Stop() 210 | if ret { 211 | t.Fatal("timer should return false to indicate Stop() didn't stop it") 212 | } 213 | timer.Reset(time.Second) 214 | ret = timer.Stop() 215 | if !ret { 216 | // This is where the bug would occur. 217 | t.Fatal("timer should return true to indicate Stop() stopped it") 218 | } 219 | } 220 | 221 | func TestInterfaceConformance(t *testing.T) { 222 | // this just verifies that both implementations actually implement 223 | // ManualTime. Nothing else in the package actually does.... 224 | var at AbstractTime // nolint: megacheck 225 | at = NewManual() 226 | at = NewRealTime() 227 | at.Now() 228 | } 229 | 230 | func TestNowQueueing(t *testing.T) { 231 | // this verifies that the "nows" queue in the expected manner 232 | at := NewManual() 233 | firstNow := at.Now() 234 | desired := []time.Time{firstNow.Add(10 * time.Second), firstNow.Add(20 * time.Second)} 235 | at.QueueNows(desired...) 236 | if at.Now() != desired[0] { 237 | t.Fatal("Failed to queue properly") 238 | } 239 | if at.Now() != desired[1] { 240 | t.Fatal("Failed to advance queue properly") 241 | } 242 | if at.Now() != desired[1] { 243 | t.Fatal("Failed to 'stick' the time properly.") 244 | } 245 | } 246 | 247 | func TestTimerReset(t *testing.T) { 248 | c := NewManual() 249 | d := time.Hour 250 | timer := c.NewTimer(d, timerID) 251 | 252 | wasActive := timer.Reset(d) 253 | if !wasActive { 254 | t.Fatal("Unexpected value from the Reset method") 255 | } 256 | 257 | go func() { 258 | c.Advance(d) 259 | c.Trigger(timerID) 260 | }() 261 | 262 | <-timer.Channel() 263 | wasActive = timer.Reset(d) 264 | if wasActive { 265 | t.Fatal("Unexpected reset result value") 266 | } 267 | } 268 | 269 | func TestMultipleTimerCreation(t *testing.T) { 270 | c := NewManual() 271 | 272 | _ = c.NewTimer(time.Second, timerID) 273 | 274 | // Klunky. More sign this is wrong. running "go" to register the 275 | // trigger doesn't make sense here. 276 | for { 277 | c.Lock() 278 | _, registered := c.triggers[timerID] 279 | c.Unlock() 280 | 281 | if registered { 282 | break 283 | } 284 | time.Sleep(time.Microsecond) 285 | } 286 | 287 | c.Unregister(timerID) 288 | timer2 := c.NewTimer(time.Second, timerID) 289 | go c.Trigger(timerID) 290 | <-timer2.Channel() 291 | 292 | } 293 | 294 | func TestContextCancel(t *testing.T) { 295 | mt := NewManual() 296 | 297 | ctx, cancelF := mt.WithTimeout(context.Background(), time.Minute, contextID) 298 | 299 | select { 300 | case <-ctx.Done(): 301 | t.Fatal("context done channel already closed") 302 | default: 303 | } 304 | 305 | if ctx.Err() != nil { 306 | t.Fatal("context error is not nil") 307 | } 308 | 309 | cancelF() 310 | 311 | select { 312 | case <-ctx.Done(): 313 | default: 314 | t.Fatal("context done channel open when it should be closed") 315 | } 316 | 317 | if ctx.Err() == nil || !errors.Is(ctx.Err(), context.Canceled) { 318 | t.Fatal("context error is not context.Canceled") 319 | } 320 | } 321 | 322 | func TestContextTrigger(t *testing.T) { 323 | mt := NewManual() 324 | 325 | ctx, cancelF := mt.WithTimeout(context.Background(), time.Minute, contextID) 326 | 327 | select { 328 | case <-ctx.Done(): 329 | t.Fatal("context done channel already closed") 330 | default: 331 | } 332 | 333 | if ctx.Err() != nil { 334 | t.Fatal("context error is not nil") 335 | } 336 | 337 | mt.Trigger(contextID) 338 | 339 | select { 340 | case <-ctx.Done(): 341 | default: 342 | t.Fatal("context done channel open when it should be closed") 343 | } 344 | 345 | if ctx.Err() == nil || !errors.Is(ctx.Err(), context.DeadlineExceeded) { 346 | t.Fatal("context error is not context.DeadlineExceeded") 347 | } 348 | 349 | cancelF() 350 | 351 | if ctx.Err() == nil || !errors.Is(ctx.Err(), context.DeadlineExceeded) { 352 | t.Fatal("context error is not context.DeadlineExceeded") 353 | } 354 | } 355 | 356 | func TestContextNestedCancel(t *testing.T) { 357 | mt := NewManual() 358 | 359 | parent, parentCancel := context.WithCancel(context.Background()) 360 | child, childCancel := mt.WithTimeout(parent, time.Minute, contextID) 361 | 362 | select { 363 | case <-child.Done(): 364 | t.Fatal("context done channel already closed") 365 | default: 366 | } 367 | 368 | if child.Err() != nil { 369 | t.Fatal("context error is not nil") 370 | } 371 | 372 | parentCancel() 373 | 374 | select { 375 | case <-child.Done(): 376 | case <-time.After(50 * time.Microsecond): 377 | t.Fatal("context done channel open when it should be closed") 378 | } 379 | 380 | if child.Err() == nil || !errors.Is(child.Err(), context.Canceled) { 381 | t.Fatal("context error is not context.Canceled") 382 | } 383 | 384 | childCancel() 385 | } 386 | 387 | func TestContextNestedTimeout(t *testing.T) { 388 | mt := NewManual() 389 | 390 | parent, parentCancel := mt.WithTimeout(context.Background(), time.Minute, contextID) 391 | child, childCancel := mt.WithTimeout(parent, time.Minute, childContextID) 392 | 393 | select { 394 | case <-child.Done(): 395 | t.Fatal("context done channel already closed") 396 | default: 397 | } 398 | 399 | if child.Err() != nil { 400 | t.Fatal("context error is not nil") 401 | } 402 | 403 | mt.Trigger(contextID) 404 | 405 | select { 406 | case <-child.Done(): 407 | case <-time.After(50 * time.Microsecond): 408 | t.Fatal("context done channel open when it should be closed") 409 | } 410 | 411 | if child.Err() == nil || !errors.Is(child.Err(), context.DeadlineExceeded) { 412 | t.Fatal("context error is not context.DeadlineExceeded") 413 | } 414 | 415 | childCancel() 416 | 417 | if child.Err() == nil || !errors.Is(child.Err(), context.DeadlineExceeded) { 418 | t.Fatal("context error is not context.DeadlineExceeded") 419 | } 420 | 421 | parentCancel() 422 | 423 | if child.Err() == nil || !errors.Is(child.Err(), context.DeadlineExceeded) { 424 | t.Fatal("context error is not context.DeadlineExceeded") 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /manual.go: -------------------------------------------------------------------------------- 1 | package abtime 2 | 3 | // In the docs, I say that "we can't distinguish between calls to After" or sleep. 4 | // A clever programmer may decide that we could, if we require them all 5 | // to use slightly different times; we could then key on times. However, 6 | // that invites excessive binding of values between the concrete code and 7 | // test suite. Plus that's just a weird binding that invites problems. 8 | 9 | import ( 10 | "context" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | // The ManualTime object implements a time object you directly control. 16 | // 17 | // This allows you to manipulate "now", and control when events occur. 18 | type ManualTime struct { 19 | now time.Time 20 | nows []time.Time 21 | triggers map[int]*triggerInfo 22 | 23 | sync.Mutex 24 | } 25 | 26 | type triggerInfo struct { 27 | // the number of times this has been Triggered without anything in 28 | // the triggers array. This accounts for when .Trigger is called 29 | // before the thing has been registered. 30 | count uint 31 | triggers []trigger 32 | } 33 | 34 | type trigger interface { 35 | // Note this is always called while the lock for *ManualTime is 36 | // held. 37 | trigger(mt *ManualTime) bool // if true, delete the token; if false, keep it. 38 | } 39 | 40 | func (mt *ManualTime) register(id int, trig trigger) { 41 | mt.Lock() 42 | defer mt.Unlock() 43 | 44 | currentTriggerInfo, present := mt.triggers[id] 45 | if !present { 46 | mt.triggers[id] = &triggerInfo{0, []trigger{trig}} 47 | return 48 | } 49 | 50 | currentTriggerInfo.triggers = append(currentTriggerInfo.triggers, trig) 51 | 52 | triggerAll(mt, currentTriggerInfo) 53 | } 54 | 55 | // NewManual returns a new ManualTime object, with the Now populated 56 | // from the time.Now(). 57 | func NewManual() *ManualTime { 58 | return &ManualTime{now: time.Now(), nows: []time.Time{}, triggers: make(map[int]*triggerInfo)} 59 | } 60 | 61 | // NewManualAtTime returns a new ManualTime object, with the Now set to the 62 | // time.Time you pass in. 63 | func NewManualAtTime(now time.Time) *ManualTime { 64 | return &ManualTime{now: now, nows: []time.Time{}, triggers: make(map[int]*triggerInfo)} 65 | } 66 | 67 | // triggerAll triggers all registered triggers count times, discarding triggers 68 | // as requested. 69 | func triggerAll(mt *ManualTime, ti *triggerInfo) { 70 | for ti.count > 0 && len(ti.triggers) > 0 { 71 | keep := []trigger{} 72 | for _, toTrigger := range ti.triggers { 73 | if !toTrigger.trigger(mt) { 74 | keep = append(keep, toTrigger) 75 | } 76 | } 77 | ti.triggers = keep 78 | ti.count-- 79 | } 80 | } 81 | 82 | // Trigger takes the given ids for time events, and causes them to "occur": 83 | // triggering messages on channels, ending sleeps, etc. 84 | // 85 | // Note this is the ONLY way to "trigger" such events. While this package 86 | // allows you to manipulate "Now" in a couple of different ways, advancing 87 | // "now" past a Trigger's set time will NOT trigger it. First, this keeps 88 | // it simple to understand when things are triggered, and second, reality 89 | // isn't so deterministic anyhow.... 90 | func (mt *ManualTime) Trigger(ids ...int) { 91 | mt.Lock() 92 | defer mt.Unlock() 93 | 94 | for _, id := range ids { 95 | triggers, hasTriggers := mt.triggers[id] 96 | if !hasTriggers { 97 | mt.triggers[id] = &triggerInfo{1, []trigger{}} 98 | continue 99 | } 100 | 101 | triggers.count++ 102 | 103 | triggerAll(mt, triggers) 104 | } 105 | } 106 | 107 | // Unregister will unregister a particular ID from the system. Normally the 108 | // first one sticks, which means if you've got code that creates multiple 109 | // timers in a loop or in multiple function calls, only the first one will 110 | // work. 111 | // 112 | // NOTE: This method indicates a design flaw in abtime. It is not yet clear 113 | // to me how to fix it in any reasonable way. 114 | func (mt *ManualTime) Unregister(ids ...int) { 115 | mt.Lock() 116 | for _, id := range ids { 117 | delete(mt.triggers, id) 118 | } 119 | mt.Unlock() 120 | } 121 | 122 | // UnregisterAll will unregister all current IDs from the manual time, 123 | // returning you to a fresh view of the created channels and timers and 124 | // such. 125 | func (mt *ManualTime) UnregisterAll() { 126 | mt.Lock() 127 | mt.triggers = map[int]*triggerInfo{} 128 | mt.Unlock() 129 | } 130 | 131 | // Now returns the ManualTime's current idea of "Now". 132 | // 133 | // If you have used QueueNow, this will advance to the next queued Now. 134 | func (mt *ManualTime) Now() time.Time { 135 | mt.Lock() 136 | defer mt.Unlock() 137 | 138 | if len(mt.nows) > 0 { 139 | mt.now = mt.nows[0] 140 | mt.nows = mt.nows[1:] 141 | return mt.now 142 | } 143 | return mt.now 144 | } 145 | 146 | // Advance advances the manual time's idea of "now" by the given 147 | // duration. 148 | // 149 | // If there is a queue of "Nows" from QueueNows, note this won't 150 | // affect any of them. 151 | func (mt *ManualTime) Advance(d time.Duration) { 152 | mt.Lock() 153 | defer mt.Unlock() 154 | 155 | mt.now = mt.now.Add(d) 156 | } 157 | 158 | // QueueNows allows you to set a number of times to be retrieved by 159 | // successive calls to "Now". Once the queue is consumed by calls to Now(), 160 | // the last time in the queue "sticks" as the new Now. 161 | // 162 | // This is useful if you have code that is timing how long something took 163 | // by successive calls to .Now, with no other place for the test code to 164 | // intercede. 165 | // 166 | // If multiple threads are accessing the Manual, it is of course 167 | // non-deterministic who gets what time. However this could still be 168 | // useful. 169 | func (mt *ManualTime) QueueNows(times ...time.Time) { 170 | mt.Lock() 171 | defer mt.Unlock() 172 | 173 | mt.nows = append(mt.nows, times...) 174 | } 175 | 176 | type afterTrigger struct { 177 | mt *ManualTime 178 | d time.Duration 179 | ch chan time.Time 180 | } 181 | 182 | func (afterT afterTrigger) trigger(mt *ManualTime) bool { 183 | go func() { afterT.ch <- afterT.mt.now.Add(afterT.d) }() 184 | return true 185 | } 186 | 187 | // After wraps time.After, and waits for the target id. 188 | func (mt *ManualTime) After(d time.Duration, id int) <-chan time.Time { 189 | timeChan := make(chan time.Time) 190 | trigger := afterTrigger{mt, d, timeChan} 191 | mt.register(id, trigger) 192 | return timeChan 193 | } 194 | 195 | type sleepTrigger struct { 196 | c chan struct{} 197 | } 198 | 199 | func (st sleepTrigger) trigger(mt *ManualTime) bool { 200 | go func() { st.c <- struct{}{} }() 201 | return true 202 | } 203 | 204 | // Sleep halts execution until you release it via Trigger. 205 | func (mt *ManualTime) Sleep(d time.Duration, id int) { 206 | ch := make(chan struct{}) 207 | 208 | mt.register(id, sleepTrigger{ch}) 209 | 210 | <-ch 211 | } 212 | 213 | type tickTrigger struct { 214 | C chan time.Time 215 | now time.Time 216 | d time.Duration 217 | stopped bool 218 | sync.Mutex 219 | } 220 | 221 | func (tt *tickTrigger) trigger(mt *ManualTime) bool { 222 | tt.Lock() 223 | defer tt.Unlock() 224 | 225 | if tt.stopped { 226 | return true 227 | } 228 | 229 | tt.now = tt.now.Add(tt.d) 230 | go func() { tt.C <- tt.now }() 231 | return false 232 | } 233 | 234 | func (tt *tickTrigger) Stop() { 235 | tt.Lock() 236 | defer tt.Unlock() 237 | 238 | tt.stopped = true 239 | } 240 | 241 | func (tt *tickTrigger) Channel() <-chan time.Time { 242 | return tt.C 243 | } 244 | 245 | func (tt *tickTrigger) Reset(time.Duration) {} 246 | 247 | // NewTicker wraps time.NewTicker. It takes a snapshot of "now" at the 248 | // point of the TickToken call, and will increment the time it returns 249 | // by the Duration of the tick. 250 | // 251 | // Note that this can cause times to arrive out of order relative to 252 | // each other if you have many of these going at once, if you manually 253 | // trigger the ticks in such a way that they will be out of order. 254 | func (mt *ManualTime) NewTicker(d time.Duration, id int) Ticker { 255 | ch := make(chan time.Time) 256 | tt := &tickTrigger{C: ch, now: mt.now, d: d} 257 | mt.register(id, tt) 258 | return tt 259 | } 260 | 261 | // Tick allows you to create a ticker. See notes on NewTicker. 262 | func (mt *ManualTime) Tick(d time.Duration, id int) <-chan time.Time { 263 | return mt.NewTicker(d, id).(*tickTrigger).C 264 | } 265 | 266 | type afterFuncTrigger struct { 267 | f func() 268 | stopped bool 269 | sync.Mutex 270 | } 271 | 272 | func (af *afterFuncTrigger) Reset(d time.Duration) bool { 273 | af.Lock() 274 | defer af.Unlock() 275 | 276 | ret := af.stopped 277 | af.stopped = false 278 | return ret 279 | } 280 | 281 | func (af *afterFuncTrigger) Stop() bool { 282 | af.Lock() 283 | defer af.Unlock() 284 | 285 | ret := !af.stopped 286 | af.stopped = true 287 | return ret 288 | } 289 | 290 | func (af *afterFuncTrigger) Channel() <-chan time.Time { 291 | return nil 292 | } 293 | 294 | func (af *afterFuncTrigger) trigger(mt *ManualTime) bool { 295 | af.Lock() 296 | defer af.Unlock() 297 | 298 | if !af.stopped { 299 | go af.f() 300 | } 301 | af.stopped = true 302 | 303 | return true 304 | } 305 | 306 | // AfterFunc fires the function in its own goroutine when the id is 307 | // .Trigger()ed. The resulting Timer object will return nil for its Channel(). 308 | func (mt *ManualTime) AfterFunc(d time.Duration, f func(), id int) Timer { 309 | af := &afterFuncTrigger{f: f, stopped: false} 310 | mt.register(id, af) 311 | return af 312 | } 313 | 314 | type timerTrigger struct { 315 | c chan time.Time 316 | initialNow time.Time 317 | duration time.Duration 318 | stopped bool 319 | sync.Mutex 320 | } 321 | 322 | func (tt *timerTrigger) Reset(d time.Duration) bool { 323 | tt.Lock() 324 | defer tt.Unlock() 325 | 326 | tt.duration = d 327 | ret := !tt.stopped 328 | tt.stopped = false 329 | return ret 330 | } 331 | 332 | func (tt *timerTrigger) Stop() bool { 333 | tt.Lock() 334 | defer tt.Unlock() 335 | 336 | ret := tt.stopped 337 | tt.stopped = true 338 | return !ret 339 | } 340 | 341 | func (tt *timerTrigger) Channel() <-chan time.Time { 342 | return tt.c 343 | } 344 | 345 | func (tt *timerTrigger) trigger(mt *ManualTime) bool { 346 | tt.Lock() 347 | if tt.stopped { 348 | tt.Unlock() 349 | return true 350 | } 351 | tt.stopped = true 352 | tt.Unlock() 353 | go func() { tt.c <- tt.initialNow.Add(tt.duration) }() 354 | return true 355 | } 356 | 357 | // NewTimer allows you to create a Ticker, which can be triggered 358 | // via the given id, and also supports the Stop operation *time.Tickers have. 359 | func (mt *ManualTime) NewTimer(d time.Duration, id int) Timer { 360 | tt := &timerTrigger{c: make(chan time.Time), initialNow: mt.now, duration: d} 361 | mt.register(id, tt) 362 | return tt 363 | } 364 | 365 | type contextTrigger struct { 366 | context.Context 367 | deadline time.Time 368 | closed bool 369 | done chan struct{} 370 | err error 371 | mu sync.Mutex 372 | } 373 | 374 | func (ct *contextTrigger) Deadline() (time.Time, bool) { 375 | return ct.deadline, true 376 | } 377 | 378 | func (ct *contextTrigger) Done() <-chan struct{} { 379 | return ct.done 380 | } 381 | 382 | func (ct *contextTrigger) Err() error { 383 | ct.mu.Lock() 384 | defer ct.mu.Unlock() 385 | return ct.err 386 | } 387 | 388 | func (ct *contextTrigger) Value(key interface{}) interface{} { 389 | return ct.Context.Value(key) 390 | } 391 | 392 | func (ct *contextTrigger) cancel(err error) { 393 | ct.mu.Lock() 394 | defer ct.mu.Unlock() 395 | if !ct.closed { 396 | close(ct.done) 397 | ct.closed = true 398 | ct.err = err 399 | } 400 | } 401 | 402 | func (ct *contextTrigger) trigger(_ *ManualTime) bool { 403 | ct.cancel(context.DeadlineExceeded) 404 | return true 405 | } 406 | 407 | // WithDeadline is a valid Context that is meant to drop in over a regular 408 | // context.WithDeadline invocation. Instead of being canceled when reaching an 409 | // actual deadline the context is canceled either by Trigger or by the returned 410 | // CancelFunc. 411 | func (mt *ManualTime) WithDeadline(parent context.Context, deadline time.Time, id int) (context.Context, context.CancelFunc) { 412 | if parent == nil { 413 | panic("cannot create context from nil parent") 414 | } 415 | ct := &contextTrigger{ 416 | Context: parent, 417 | deadline: deadline, 418 | done: make(chan struct{}), 419 | } 420 | cancelF := func() { 421 | ct.cancel(context.Canceled) 422 | } 423 | mt.register(id, ct) 424 | go func() { 425 | select { 426 | case <-parent.Done(): 427 | ct.cancel(parent.Err()) 428 | case <-ct.Done(): 429 | // do nothing 430 | } 431 | }() 432 | return ct, context.CancelFunc(cancelF) 433 | } 434 | 435 | // WithTimeout is equivalent to WithDeadline invoked on a deadline equal to the 436 | // current time plus the timeout. 437 | func (mt *ManualTime) WithTimeout(parent context.Context, timeout time.Duration, id int) (context.Context, context.CancelFunc) { 438 | return mt.WithDeadline(parent, mt.Now().Add(timeout), id) 439 | } 440 | --------------------------------------------------------------------------------