├── .gitignore ├── go.mod ├── timexmock ├── mocked.go ├── ticker.go ├── timer.go └── implementation.go ├── benchmark_test.go ├── .travis.yml ├── implementation.go ├── ticker.go ├── timer.go ├── CHANGELOG.md ├── go.sum ├── Makefile ├── timextest ├── mocked_ticker.go ├── mocked_ticker_test.go ├── mocked_timer_test.go ├── mocked_timer.go ├── test_implementation.go └── example_test.go ├── funcs_disabled.go ├── default.go ├── funcs.go ├── funcs_disabled_test.go ├── default_test.go ├── .golangci.yaml ├── funcs_test.go ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.out 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cabify/timex 2 | 3 | go 1.13 4 | 5 | require github.com/stretchr/testify v1.4.0 6 | -------------------------------------------------------------------------------- /timexmock/mocked.go: -------------------------------------------------------------------------------- 1 | package timexmock 2 | 3 | import "github.com/cabify/timex" 4 | 5 | // Mocked runs the provided function with timex mocked, and then restores 6 | // the default implementation 7 | func Mocked(f func(*Implementation)) { 8 | mocked := &Implementation{} 9 | restore := timex.Override(mocked) 10 | defer restore() 11 | f(mocked) 12 | } 13 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package timex_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/cabify/timex" 8 | ) 9 | 10 | var dontOptimizeMePlease time.Time 11 | 12 | func BenchmarkTimeNow(b *testing.B) { 13 | for i := 0; i <= b.N; i++ { 14 | dontOptimizeMePlease = time.Now() 15 | } 16 | } 17 | 18 | func BenchmarkTimexNow(b *testing.B) { 19 | for i := 0; i <= b.N; i++ { 20 | dontOptimizeMePlease = timex.Now() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.12.x" 5 | - "1.13.x" 6 | 7 | env: 8 | - GO111MODULE=on 9 | 10 | install: 11 | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.20.1 12 | 13 | script: 14 | - make test 15 | - golangci-lint run 16 | 17 | after_success: 18 | - go get github.com/mattn/goveralls 19 | - goveralls -coverprofile=coverage.out -service=travis-ci 20 | -------------------------------------------------------------------------------- /implementation.go: -------------------------------------------------------------------------------- 1 | package timex 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Implementation defines the methods we delegate 8 | type Implementation interface { 9 | Now() time.Time 10 | Since(t time.Time) time.Duration 11 | Until(t time.Time) time.Duration 12 | 13 | Sleep(d time.Duration) 14 | 15 | After(d time.Duration) <-chan time.Time 16 | AfterFunc(d time.Duration, f func()) Timer 17 | 18 | NewTicker(d time.Duration) Ticker 19 | NewTimer(d time.Duration) Timer 20 | } 21 | 22 | //go:generate mockery -case underscore -outpkg timexmock -output timexmock -name Implementation 23 | var _ Implementation = Default{} 24 | -------------------------------------------------------------------------------- /ticker.go: -------------------------------------------------------------------------------- 1 | package timex 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Ticker is an interface similar to what time.Ticker provides, but with C as a function returning the channel 8 | // instead of accessing a plain struct property, so we can mock it. 9 | // See time.Ticker docs for more details 10 | type Ticker interface { 11 | // C returns the channel where each tick will be signaled, like Ticker.C in time package 12 | C() <-chan time.Time 13 | 14 | // Stop stops the ticker, no more ticks will be received in `C()` 15 | Stop() 16 | } 17 | 18 | //go:generate mockery -case underscore -outpkg timexmock -output timexmock -name Ticker 19 | type ticker struct { 20 | *time.Ticker 21 | } 22 | 23 | func (t ticker) C() <-chan time.Time { return t.Ticker.C } 24 | -------------------------------------------------------------------------------- /timexmock/ticker.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package timexmock 4 | 5 | import ( 6 | time "time" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // Ticker is an autogenerated mock type for the Ticker type 12 | type Ticker struct { 13 | mock.Mock 14 | } 15 | 16 | // C provides a mock function with given fields: 17 | func (_m *Ticker) C() <-chan time.Time { 18 | ret := _m.Called() 19 | 20 | var r0 <-chan time.Time 21 | if rf, ok := ret.Get(0).(func() <-chan time.Time); ok { 22 | r0 = rf() 23 | } else { 24 | if ret.Get(0) != nil { 25 | r0 = ret.Get(0).(<-chan time.Time) 26 | } 27 | } 28 | 29 | return r0 30 | } 31 | 32 | // Stop provides a mock function with given fields: 33 | func (_m *Ticker) Stop() { 34 | _m.Called() 35 | } 36 | -------------------------------------------------------------------------------- /timer.go: -------------------------------------------------------------------------------- 1 | package timex 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Timer is an interface similar to what time.Timer provides, but with C as a function returning the channel 8 | // instead of accessing a plain struct property, so we can mock it. 9 | // See time.Timer docs for more details 10 | type Timer interface { 11 | // C returns the channel where the tick will be signaled, like Timer.C in time package 12 | C() <-chan time.Time 13 | 14 | // Stop stops the timer, see exact docs on time.Timer for information about channel draining when Stop returns false 15 | Stop() bool 16 | } 17 | 18 | //go:generate mockery -case underscore -outpkg timexmock -output timexmock -name Timer 19 | type timer struct { 20 | *time.Timer 21 | } 22 | 23 | func (t timer) C() <-chan time.Time { return t.Timer.C } 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.1.0] - 2020-08-03 10 | ### Added 11 | - Default implementation is now exposed as `timex.Default` [#2](https://github.com/cabify/timex/pull/2) 12 | 13 | ## [1.0.0] - 2020-01-07 14 | ### Added 15 | - Initial commit with all the previous implementation squashed into one commit. 16 | 17 | 18 | [Unreleased]: https://github.com/cabify/timex/compare/v1.1.0...HEAD 19 | [1.1.0]: https://github.com/cabify/timex/compare/v1.0.0...v1.1.0 20 | [1.0.0]: https://github.com/cabify/timex/compare/v1.0.0 21 | 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 3 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 4 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 5 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 6 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 10 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test help fmt report-coveralls benchmark lint 2 | 3 | help: ## Show the help text 4 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[93m %s\n", $$1, $$2}' 5 | 6 | test: ## Run unit tests 7 | @echo "Testing with timex_disable tag (only root package)" 8 | @go test -tags=timex_disable -race . 9 | @echo "Testing without timex_disable tag (normal)" 10 | @go test -coverprofile=coverage.out -covermode=atomic -race ./... 11 | 12 | lint: 13 | @golangci-lint run 14 | 15 | fmt: ## Format files 16 | @goimports -w $$(find . -name "*.go" -not -path "./vendor/*") 17 | 18 | benchmark: ## Run benchmarks 19 | @echo "Benchmarks with timex_disable tag" 20 | @go test -run=NONE -benchmem -benchtime=5s -bench=. -tags=timex_disable . 21 | @echo "Benchmarks without timex_disable tag (normal)" 22 | @go test -run=NONE -benchmem -benchtime=5s -bench=. . 23 | -------------------------------------------------------------------------------- /timexmock/timer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package timexmock 4 | 5 | import ( 6 | time "time" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // Timer is an autogenerated mock type for the Timer type 12 | type Timer struct { 13 | mock.Mock 14 | } 15 | 16 | // C provides a mock function with given fields: 17 | func (_m *Timer) C() <-chan time.Time { 18 | ret := _m.Called() 19 | 20 | var r0 <-chan time.Time 21 | if rf, ok := ret.Get(0).(func() <-chan time.Time); ok { 22 | r0 = rf() 23 | } else { 24 | if ret.Get(0) != nil { 25 | r0 = ret.Get(0).(<-chan time.Time) 26 | } 27 | } 28 | 29 | return r0 30 | } 31 | 32 | // Stop provides a mock function with given fields: 33 | func (_m *Timer) Stop() bool { 34 | ret := _m.Called() 35 | 36 | var r0 bool 37 | if rf, ok := ret.Get(0).(func() bool); ok { 38 | r0 = rf() 39 | } else { 40 | r0 = ret.Get(0).(bool) 41 | } 42 | 43 | return r0 44 | } 45 | -------------------------------------------------------------------------------- /timextest/mocked_ticker.go: -------------------------------------------------------------------------------- 1 | package timextest 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // MockedTicker implements a timex.Ticker and provides functions to control its behavior 8 | type MockedTicker struct { 9 | c chan time.Time 10 | stopped chan struct{} 11 | } 12 | 13 | func newMockedTicker() *MockedTicker { 14 | return &MockedTicker{ 15 | c: make(chan time.Time), 16 | stopped: make(chan struct{}), 17 | } 18 | } 19 | 20 | // C implements timex.Ticker 21 | func (mt *MockedTicker) C() <-chan time.Time { 22 | return mt.c 23 | } 24 | 25 | // Stop implements timex.Ticker, and it will close the channel provided by StoppedChan 26 | func (mt *MockedTicker) Stop() { 27 | close(mt.stopped) 28 | } 29 | 30 | // StoppedChan will be closed once Stop is called 31 | func (mt *MockedTicker) StoppedChan() chan struct{} { 32 | return mt.stopped 33 | } 34 | 35 | // Tick will send the provided time through ticker.C() 36 | func (mt *MockedTicker) Tick(t time.Time) { 37 | select { 38 | case <-mt.stopped: 39 | panic("trying to tick on a stopped ticker, does your test have a race condition?") 40 | default: 41 | mt.c <- t 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /funcs_disabled.go: -------------------------------------------------------------------------------- 1 | // +build timex_disable 2 | 3 | package timex 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | // Now can be used as a replacement of time.Now() 10 | func Now() time.Time { return time.Now() } 11 | 12 | // Since can be used as a replacement of time.Since() 13 | func Since(t time.Time) time.Duration { return time.Since(t) } 14 | 15 | // Until can be used as a replacement of time.Until() 16 | func Until(t time.Time) time.Duration { return time.Until(t) } 17 | 18 | // Sleep can be used as a replacement of time.Sleep() 19 | func Sleep(d time.Duration) { time.Sleep(d) } 20 | 21 | // After can be used a replacement of time.After() 22 | func After(d time.Duration) <-chan time.Time { return time.After(d) } 23 | 24 | // AfterFunc can be used as a replacement of time.AfterFunc() 25 | func AfterFunc(d time.Duration, f func()) Timer { return timer{time.AfterFunc(d, f)} } 26 | 27 | // NewTicker creates a new ticker as a replacement of time.NewTicker 28 | func NewTicker(d time.Duration) Ticker { return ticker{time.NewTicker(d)} } 29 | 30 | // NewTimer creates a new timer as a replacement of time.NewTimer 31 | func NewTimer(d time.Duration) Timer { return timer{time.NewTimer(d)} } 32 | -------------------------------------------------------------------------------- /timextest/mocked_ticker_test.go: -------------------------------------------------------------------------------- 1 | package timextest_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/cabify/timex" 8 | "github.com/cabify/timex/timextest" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestMockedTicker_StoppedChan(t *testing.T) { 13 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 14 | go func() { 15 | ticker := timex.NewTicker(time.Second) 16 | ticker.Stop() 17 | }() 18 | mockedTicker := <-mockedtimex.NewTickerCalls 19 | 20 | select { 21 | case <-mockedTicker.Mock.StoppedChan(): 22 | case <-time.After(time.Second): 23 | t.Errorf("Stop should have been called") 24 | } 25 | }) 26 | } 27 | 28 | func TestMockedTicker_Tick(t *testing.T) { 29 | t.Run("panics on stopped ticker", func(t *testing.T) { 30 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 31 | go func() { 32 | ticker := timex.NewTicker(time.Second) 33 | ticker.Stop() 34 | }() 35 | mockedTicker := <-mockedtimex.NewTickerCalls 36 | 37 | <-mockedTicker.Mock.StoppedChan() 38 | 39 | assert.Panics(t, func() { 40 | mockedTicker.Mock.Tick(now) 41 | }) 42 | }) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /timextest/mocked_timer_test.go: -------------------------------------------------------------------------------- 1 | package timextest_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/cabify/timex" 8 | "github.com/cabify/timex/timextest" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestMockedTimer_StoppedChan(t *testing.T) { 13 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 14 | stopValue := make(chan bool) 15 | go func() { 16 | ticker := timex.NewTimer(time.Second) 17 | stopValue <- ticker.Stop() 18 | }() 19 | mockedTicker := <-mockedtimex.NewTimerCalls 20 | mockedTicker.Mock.StopValue(true) 21 | 22 | select { 23 | case <-mockedTicker.Mock.StoppedChan(): 24 | case <-time.After(time.Second): 25 | t.Errorf("Stop should have been called") 26 | } 27 | 28 | assert.True(t, <-stopValue) 29 | }) 30 | } 31 | 32 | func TestMockedTimer_Trigger(t *testing.T) { 33 | t.Run("panics on stopped timer", func(t *testing.T) { 34 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 35 | go func() { 36 | ticker := timex.NewTimer(time.Second) 37 | ticker.Stop() 38 | }() 39 | mockedTimer := <-mockedtimex.NewTimerCalls 40 | mockedTimer.Mock.StopValue(true) 41 | 42 | <-mockedTimer.Mock.StoppedChan() 43 | 44 | assert.Panics(t, func() { 45 | mockedTimer.Mock.Trigger(now) 46 | }) 47 | }) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /default.go: -------------------------------------------------------------------------------- 1 | package timex 2 | 3 | import "time" 4 | 5 | // Default uses time package functions 6 | type Default struct{} 7 | 8 | // Now can be used as a replacement of time.Now() 9 | func (Default) Now() time.Time { 10 | return time.Now() 11 | } 12 | 13 | // Since can be used as a replacement of time.Since() 14 | func (Default) Since(t time.Time) time.Duration { 15 | return time.Since(t) 16 | } 17 | 18 | // Until can be used as a replacement of time.Until() 19 | func (Default) Until(t time.Time) time.Duration { 20 | return time.Until(t) 21 | } 22 | 23 | // AfterFunc can be used as a replacement of time.AfterFunc() 24 | func (Default) AfterFunc(d time.Duration, f func()) Timer { 25 | return timer{time.AfterFunc(d, f)} 26 | } 27 | 28 | // Sleep can be used as a replacement of time.Sleep() 29 | func (Default) Sleep(d time.Duration) { 30 | time.Sleep(d) 31 | } 32 | 33 | // After can be used a replacement of time.After() 34 | func (Default) After(d time.Duration) <-chan time.Time { 35 | return time.After(d) 36 | } 37 | 38 | // NewTicker creates a new ticker as replacement of time.NewTicker() 39 | func (Default) NewTicker(d time.Duration) Ticker { 40 | return ticker{time.NewTicker(d)} 41 | } 42 | 43 | // NewTimer creates a new timer as replacement of time.NewTimer() 44 | func (Default) NewTimer(d time.Duration) Timer { 45 | return timer{time.NewTimer(d)} 46 | } 47 | -------------------------------------------------------------------------------- /timextest/mocked_timer.go: -------------------------------------------------------------------------------- 1 | package timextest 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // MockedTimer implements a timex.Timer and provides functions to control its behavior 9 | type MockedTimer struct { 10 | c chan time.Time 11 | f func() 12 | 13 | stopped chan struct{} 14 | 15 | stopMutex sync.Mutex 16 | stopValue bool 17 | } 18 | 19 | func newMockedTimer() *MockedTimer { 20 | timer := &MockedTimer{ 21 | c: make(chan time.Time), 22 | stopped: make(chan struct{}), 23 | } 24 | timer.stopMutex.Lock() 25 | return timer 26 | } 27 | 28 | func newMockedFuncTimer(f func()) *MockedTimer { 29 | timer := &MockedTimer{ 30 | f: f, 31 | stopped: make(chan struct{}), 32 | } 33 | timer.stopMutex.Lock() 34 | return timer 35 | } 36 | 37 | // C implements timex.Timer 38 | func (mt *MockedTimer) C() <-chan time.Time { 39 | return mt.c 40 | } 41 | 42 | // Stop implements the timex.Timer, stop will not return until the value is set by StopValue() 43 | func (mt *MockedTimer) Stop() bool { 44 | close(mt.stopped) 45 | mt.stopMutex.Lock() 46 | defer mt.stopMutex.Unlock() 47 | return mt.stopValue 48 | } 49 | 50 | // StopValue sets the value to be returned by Stop(), it has to be called to allow 51 | // that method to return 52 | // It can be only called once 53 | func (mt *MockedTimer) StopValue(v bool) { 54 | mt.stopValue = v 55 | mt.stopMutex.Unlock() 56 | } 57 | 58 | // StoppedChan provides the channel that is closed when Stop() is called 59 | func (mt *MockedTimer) StoppedChan() chan struct{} { 60 | return mt.stopped 61 | } 62 | 63 | // Trigger for a usual Timer will send the provided time through timer.C(), 64 | // If this was a AfterFunc call, then trigger will ignore the time provided and will 65 | // call the scheduled function in the caller's goroutine 66 | func (mt *MockedTimer) Trigger(t time.Time) { 67 | select { 68 | case <-mt.stopped: 69 | panic("trying to tick on a stopped timer, does your test have a race condition?") 70 | default: 71 | if mt.c == nil { 72 | mt.f() 73 | } else { 74 | mt.c <- t 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /funcs.go: -------------------------------------------------------------------------------- 1 | // +build !timex_disable 2 | 3 | package timex 4 | 5 | import ( 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | // Now can be used as a replacement of time.Now() 12 | func Now() time.Time { return impl.Load().(Implementation).Now() } 13 | 14 | // Since can be used as a replacement of time.Since() 15 | func Since(t time.Time) time.Duration { return impl.Load().(Implementation).Since(t) } 16 | 17 | // Until can be used as a replacement of time.Until() 18 | func Until(t time.Time) time.Duration { return impl.Load().(Implementation).Until(t) } 19 | 20 | // Sleep can be used as a replacement of time.Sleep() 21 | func Sleep(d time.Duration) { impl.Load().(Implementation).Sleep(d) } 22 | 23 | // After can be used a replacement of time.After() 24 | func After(d time.Duration) <-chan time.Time { return impl.Load().(Implementation).After(d) } 25 | 26 | // AfterFunc can be used as a replacement of time.AfterFunc() 27 | func AfterFunc(d time.Duration, f func()) Timer { return impl.Load().(Implementation).AfterFunc(d, f) } 28 | 29 | // NewTicker creates a new ticker as a replacement of time.NewTicker 30 | func NewTicker(d time.Duration) Ticker { return impl.Load().(Implementation).NewTicker(d) } 31 | 32 | // NewTimer creates a new timer as a replacement of time.NewTimer 33 | func NewTimer(d time.Duration) Timer { return impl.Load().(Implementation).NewTimer(d) } 34 | 35 | // overridden is a lock we take when we override the timex implementation 36 | // since golang packages can run tests in different packages concurrently, 37 | // we want to make sure that there are no two implementations overriding 38 | // concurrently 39 | // https://medium.com/@xcoulon/how-to-avoid-parallel-execution-of-tests-in-golang-763d32d88eec 40 | var overridden sync.Mutex 41 | 42 | // Override replaces the global implementation used for timex 43 | // The returned function should be called to restore the default implementation 44 | func Override(implementation Implementation) func() { 45 | overridden.Lock() 46 | impl.Store(implValue{implementation}) 47 | return func() { 48 | impl.Store(implValue{Default{}}) 49 | overridden.Unlock() 50 | } 51 | } 52 | 53 | // impl stores the current implementation being used 54 | // we don't use the RWMutex to read the impl itself because atomic.Value is faster 55 | // in a _frequent read - unfrequent write_ scenario according to the docs 56 | // https://tip.golang.org/pkg/sync/atomic/#Value 57 | var impl atomic.Value 58 | 59 | type implValue struct{ Implementation } 60 | 61 | func init() { 62 | impl.Store(implValue{Default{}}) 63 | } 64 | -------------------------------------------------------------------------------- /funcs_disabled_test.go: -------------------------------------------------------------------------------- 1 | // +build timex_disable 2 | 3 | package timex_test 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | "github.com/cabify/timex" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestNow(t *testing.T) { 14 | diff := time.Since(timex.Now()) 15 | assert.True(t, diff < time.Second) 16 | assert.True(t, diff > 0) 17 | } 18 | 19 | func TestSince(t *testing.T) { 20 | diff := timex.Since(time.Now()) 21 | assert.True(t, diff < time.Second) 22 | assert.True(t, diff > 0) 23 | } 24 | 25 | func TestUntil(t *testing.T) { 26 | diff := timex.Until(time.Now()) 27 | assert.True(t, diff < 0) 28 | assert.True(t, diff > -time.Second) 29 | } 30 | 31 | func TestAfterFunc(t *testing.T) { 32 | timeout := time.After(time.Second) 33 | ok := make(chan struct{}) 34 | 35 | timex.AfterFunc(time.Millisecond, func() { close(ok) }) 36 | 37 | select { 38 | case <-ok: 39 | // ok 40 | case <-timeout: 41 | t.Errorf("Timeout waiting for AfterFunc") 42 | } 43 | } 44 | 45 | func TestSleep(t *testing.T) { 46 | timeout := time.After(time.Second) 47 | ok := make(chan struct{}) 48 | 49 | go func() { 50 | timex.Sleep(time.Millisecond) 51 | close(ok) 52 | }() 53 | 54 | select { 55 | case <-ok: 56 | // ok 57 | case <-timeout: 58 | t.Errorf("Timeout waiting for Sleep") 59 | } 60 | } 61 | 62 | func TestAfter(t *testing.T) { 63 | timeout := time.After(time.Second) 64 | ok := timex.After(time.Millisecond) 65 | 66 | select { 67 | case <-ok: 68 | // ok 69 | case <-timeout: 70 | t.Errorf("Timeout waiting for After") 71 | } 72 | } 73 | 74 | func TestNewTicker(t *testing.T) { 75 | timeout := time.After(time.Second) 76 | ticker := timex.NewTicker(100 * time.Millisecond) 77 | 78 | select { 79 | case <-ticker.C(): 80 | // ok 81 | case <-timeout: 82 | t.Errorf("Timeout waiting for Ticker") 83 | } 84 | 85 | ticker.Stop() 86 | 87 | ok := timex.After(200 * time.Millisecond) 88 | 89 | select { 90 | case <-ticker.C(): 91 | t.Errorf("Should not tick again since it's stopped") 92 | case <-ok: 93 | // ok 94 | } 95 | } 96 | 97 | func TestNewTimer(t *testing.T) { 98 | t.Run("tick", func(t *testing.T) { 99 | timeout := time.After(time.Second) 100 | timer := timex.NewTimer(100 * time.Millisecond) 101 | 102 | select { 103 | case <-timer.C(): 104 | // ok 105 | case <-timeout: 106 | t.Errorf("Timeout waiting for Mock") 107 | } 108 | }) 109 | 110 | t.Run("stop", func(t *testing.T) { 111 | timer := timex.NewTimer(100 * time.Millisecond) 112 | timer.Stop() 113 | 114 | ok := timex.After(200 * time.Millisecond) 115 | 116 | select { 117 | case <-timer.C(): 118 | t.Errorf("Should not tick since it's stopped") 119 | case <-ok: 120 | // ok 121 | } 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /default_test.go: -------------------------------------------------------------------------------- 1 | package timex 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDefault_Now(t *testing.T) { 11 | diff := time.Since(Default{}.Now()) 12 | assert.True(t, diff < time.Second) 13 | assert.True(t, diff > 0) 14 | } 15 | 16 | func TestDefault_Since(t *testing.T) { 17 | diff := Default{}.Since(time.Now()) 18 | assert.True(t, diff < time.Second) 19 | assert.True(t, diff > 0) 20 | } 21 | 22 | func TestDefault_Until(t *testing.T) { 23 | diff := Default{}.Until(time.Now()) 24 | assert.True(t, diff < 0) 25 | assert.True(t, diff > -time.Second) 26 | } 27 | 28 | func TestDefault_AfterFunc(t *testing.T) { 29 | timeout := time.After(time.Second) 30 | ok := make(chan struct{}) 31 | 32 | Default{}.AfterFunc(time.Millisecond, func() { close(ok) }) 33 | 34 | select { 35 | case <-ok: 36 | // ok 37 | case <-timeout: 38 | t.Errorf("Timeout waiting for AfterFunc") 39 | } 40 | } 41 | 42 | func TestDefault_Sleep(t *testing.T) { 43 | timeout := time.After(time.Second) 44 | ok := make(chan struct{}) 45 | 46 | go func() { 47 | Default{}.Sleep(time.Millisecond) 48 | close(ok) 49 | }() 50 | 51 | select { 52 | case <-ok: 53 | // ok 54 | case <-timeout: 55 | t.Errorf("Timeout waiting for Sleep") 56 | } 57 | } 58 | 59 | func TestDefault_After(t *testing.T) { 60 | timeout := time.After(time.Second) 61 | ok := Default{}.After(time.Millisecond) 62 | 63 | select { 64 | case <-ok: 65 | // ok 66 | case <-timeout: 67 | t.Errorf("Timeout waiting for After") 68 | } 69 | } 70 | 71 | func TestDefault_NewTicker(t *testing.T) { 72 | timeout := time.After(time.Second) 73 | ticker := Default{}.NewTicker(100 * time.Millisecond) 74 | 75 | select { 76 | case <-ticker.C(): 77 | // ok 78 | case <-timeout: 79 | t.Errorf("Timeout waiting for Ticker") 80 | } 81 | 82 | ticker.Stop() 83 | 84 | ok := Default{}.After(200 * time.Millisecond) 85 | 86 | select { 87 | case <-ticker.C(): 88 | t.Errorf("Should not tick again since it's stopped") 89 | case <-ok: 90 | // ok 91 | } 92 | } 93 | 94 | func TestDefault_NewTimer(t *testing.T) { 95 | t.Run("tick", func(t *testing.T) { 96 | timeout := time.After(time.Second) 97 | timer := Default{}.NewTimer(100 * time.Millisecond) 98 | 99 | select { 100 | case <-timer.C(): 101 | // ok 102 | case <-timeout: 103 | t.Errorf("Timeout waiting for Mock") 104 | } 105 | }) 106 | 107 | t.Run("stop", func(t *testing.T) { 108 | timer := Default{}.NewTimer(100 * time.Millisecond) 109 | timer.Stop() 110 | 111 | ok := After(200 * time.Millisecond) 112 | 113 | select { 114 | case <-timer.C(): 115 | t.Errorf("Should not tick since it's stopped") 116 | case <-ok: 117 | // ok 118 | } 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | # timeout for analysis, e.g. 30s, 5m, default is 1m 3 | timeout: 1m 4 | 5 | # include test files or not, default is true 6 | tests: true 7 | 8 | # list of build tags, all linters use it. Default is empty list. 9 | build-tags: [] 10 | 11 | # settings of specific linters 12 | linters-settings: 13 | govet: 14 | check-shadowing: false 15 | golint: 16 | min-confidence: 0 17 | gocyclo: 18 | min-complexity: 15 19 | maligned: 20 | suggest-new: true 21 | dupl: 22 | threshold: 100 23 | goconst: 24 | min-len: 2 25 | min-occurrences: 2 26 | misspell: 27 | locale: US 28 | gocritic: 29 | enabled-tags: 30 | - diagnostic 31 | - experimental 32 | - opinionated 33 | - style 34 | disabled-checks: 35 | - dupImport # https://github.com/go-critic/go-critic/issues/845 36 | - ifElseChain 37 | - octalLiteral 38 | - wrapperFunc 39 | funlen: 40 | lines: 20 41 | statements: 20 42 | 43 | linters: 44 | disable-all: true 45 | enable: 46 | - bodyclose 47 | - deadcode 48 | - depguard 49 | - dogsled 50 | - dupl 51 | - errcheck 52 | - funlen 53 | - goconst 54 | - gocritic 55 | - gocyclo 56 | - gofmt 57 | - goimports 58 | - golint 59 | - gosec 60 | - gosimple 61 | - govet 62 | - ineffassign 63 | - interfacer 64 | - lll 65 | - misspell 66 | - nakedret 67 | - scopelint 68 | - staticcheck 69 | - structcheck 70 | - stylecheck 71 | - typecheck 72 | - unconvert 73 | - unparam 74 | - unused 75 | - varcheck 76 | - whitespace 77 | 78 | # don't enable: 79 | # - gochecknoglobals 80 | # - gocognit 81 | # - godox 82 | # - maligned 83 | # - prealloc 84 | # - gochecknoinits 85 | 86 | 87 | issues: 88 | # Excluding configuration per-path, per-linter, per-text and per-source 89 | exclude-rules: 90 | # Exclude some linters from running on tests files. 91 | - path: _test\.go 92 | linters: 93 | - gocyclo 94 | - errcheck 95 | - dupl 96 | - gosec 97 | - scopelint 98 | - goconst 99 | - funlen 100 | - gocritic 101 | 102 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 103 | max-issues-per-linter: 0 104 | 105 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 106 | max-same-issues: 0 107 | 108 | # Show only new issues: if there are unstaged changes or untracked files, 109 | # only those changes are analyzed, else only changes in HEAD~ are analyzed. 110 | # It's a super-useful option for integration of golangci-lint into existing 111 | # large codebase. It's not practical to fix all existing issues at the moment 112 | # of integration: much better don't allow issues in new code. 113 | # Default is false. 114 | new: false 115 | -------------------------------------------------------------------------------- /funcs_test.go: -------------------------------------------------------------------------------- 1 | // +build !timex_disable 2 | 3 | package timex_test 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | "github.com/cabify/timex" 10 | "github.com/cabify/timex/timexmock" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | ) 14 | 15 | var ( 16 | someDate = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) 17 | someDuration = 288 * time.Second 18 | ) 19 | 20 | func TestNow(t *testing.T) { 21 | timexmock.Mocked(func(mocked *timexmock.Implementation) { 22 | mocked.On("Now").Once().Return(someDate) 23 | defer mocked.AssertExpectations(t) 24 | 25 | now := timex.Now() 26 | assert.Equal(t, someDate, now) 27 | }) 28 | } 29 | 30 | func TestSince(t *testing.T) { 31 | timexmock.Mocked(func(mocked *timexmock.Implementation) { 32 | mocked.On("Since", someDate).Once().Return(someDuration) 33 | defer mocked.AssertExpectations(t) 34 | 35 | since := timex.Since(someDate) 36 | assert.Equal(t, someDuration, since) 37 | }) 38 | } 39 | 40 | func TestUntil(t *testing.T) { 41 | timexmock.Mocked(func(mocked *timexmock.Implementation) { 42 | mocked.On("Until", someDate).Once().Return(someDuration) 43 | defer mocked.AssertExpectations(t) 44 | 45 | until := timex.Until(someDate) 46 | assert.Equal(t, someDuration, until) 47 | }) 48 | } 49 | 50 | func TestAfterFunc(t *testing.T) { 51 | timexmock.Mocked(func(mocked *timexmock.Implementation) { 52 | providedCorrectFunction := false 53 | expectedTimer := &timexmock.Timer{} 54 | mocked.On("AfterFunc", someDuration, mock.Anything).Once().Return(func(_ time.Duration, f func()) timex.Timer { 55 | f() 56 | return expectedTimer 57 | }) 58 | defer mocked.AssertExpectations(t) 59 | 60 | timer := timex.AfterFunc(someDuration, func() { providedCorrectFunction = true }) 61 | assert.Equal(t, expectedTimer, timer) 62 | assert.True(t, providedCorrectFunction) 63 | }) 64 | } 65 | 66 | func TestSleep(t *testing.T) { 67 | timexmock.Mocked(func(mocked *timexmock.Implementation) { 68 | mocked.On("Sleep", someDuration).Once() 69 | defer mocked.AssertExpectations(t) 70 | 71 | timex.Sleep(someDuration) 72 | }) 73 | } 74 | 75 | func TestAfter(t *testing.T) { 76 | timexmock.Mocked(func(mocked *timexmock.Implementation) { 77 | expectedChan := make(<-chan time.Time) 78 | mocked.On("After", someDuration).Once().Return(expectedChan) 79 | defer mocked.AssertExpectations(t) 80 | 81 | ch := timex.After(someDuration) 82 | assert.Equal(t, expectedChan, ch) 83 | }) 84 | } 85 | 86 | func TestNewTicker(t *testing.T) { 87 | timexmock.Mocked(func(mocked *timexmock.Implementation) { 88 | expectedTicker := &timexmock.Ticker{} 89 | mocked.On("NewTicker", someDuration).Once().Return(expectedTicker) 90 | defer mocked.AssertExpectations(t) 91 | 92 | ticker := timex.NewTicker(someDuration) 93 | assert.Equal(t, expectedTicker, ticker) 94 | }) 95 | } 96 | 97 | func TestNewTimer(t *testing.T) { 98 | timexmock.Mocked(func(mocked *timexmock.Implementation) { 99 | expectedTimer := &timexmock.Timer{} 100 | mocked.On("NewTimer", someDuration).Once().Return(expectedTimer) 101 | defer mocked.AssertExpectations(t) 102 | 103 | timer := timex.NewTimer(someDuration) 104 | assert.Equal(t, expectedTimer, timer) 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /timexmock/implementation.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package timexmock 4 | 5 | import ( 6 | time "time" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | 10 | timex "github.com/cabify/timex" 11 | ) 12 | 13 | // Implementation is an autogenerated mock type for the Implementation type 14 | type Implementation struct { 15 | mock.Mock 16 | } 17 | 18 | // After provides a mock function with given fields: d 19 | func (_m *Implementation) After(d time.Duration) <-chan time.Time { 20 | ret := _m.Called(d) 21 | 22 | var r0 <-chan time.Time 23 | if rf, ok := ret.Get(0).(func(time.Duration) <-chan time.Time); ok { 24 | r0 = rf(d) 25 | } else { 26 | if ret.Get(0) != nil { 27 | r0 = ret.Get(0).(<-chan time.Time) 28 | } 29 | } 30 | 31 | return r0 32 | } 33 | 34 | // AfterFunc provides a mock function with given fields: d, f 35 | func (_m *Implementation) AfterFunc(d time.Duration, f func()) timex.Timer { 36 | ret := _m.Called(d, f) 37 | 38 | var r0 timex.Timer 39 | if rf, ok := ret.Get(0).(func(time.Duration, func()) timex.Timer); ok { 40 | r0 = rf(d, f) 41 | } else { 42 | if ret.Get(0) != nil { 43 | r0 = ret.Get(0).(timex.Timer) 44 | } 45 | } 46 | 47 | return r0 48 | } 49 | 50 | // NewTicker provides a mock function with given fields: d 51 | func (_m *Implementation) NewTicker(d time.Duration) timex.Ticker { 52 | ret := _m.Called(d) 53 | 54 | var r0 timex.Ticker 55 | if rf, ok := ret.Get(0).(func(time.Duration) timex.Ticker); ok { 56 | r0 = rf(d) 57 | } else { 58 | if ret.Get(0) != nil { 59 | r0 = ret.Get(0).(timex.Ticker) 60 | } 61 | } 62 | 63 | return r0 64 | } 65 | 66 | // NewTimer provides a mock function with given fields: d 67 | func (_m *Implementation) NewTimer(d time.Duration) timex.Timer { 68 | ret := _m.Called(d) 69 | 70 | var r0 timex.Timer 71 | if rf, ok := ret.Get(0).(func(time.Duration) timex.Timer); ok { 72 | r0 = rf(d) 73 | } else { 74 | if ret.Get(0) != nil { 75 | r0 = ret.Get(0).(timex.Timer) 76 | } 77 | } 78 | 79 | return r0 80 | } 81 | 82 | // Now provides a mock function with given fields: 83 | func (_m *Implementation) Now() time.Time { 84 | ret := _m.Called() 85 | 86 | var r0 time.Time 87 | if rf, ok := ret.Get(0).(func() time.Time); ok { 88 | r0 = rf() 89 | } else { 90 | r0 = ret.Get(0).(time.Time) 91 | } 92 | 93 | return r0 94 | } 95 | 96 | // Since provides a mock function with given fields: t 97 | func (_m *Implementation) Since(t time.Time) time.Duration { 98 | ret := _m.Called(t) 99 | 100 | var r0 time.Duration 101 | if rf, ok := ret.Get(0).(func(time.Time) time.Duration); ok { 102 | r0 = rf(t) 103 | } else { 104 | r0 = ret.Get(0).(time.Duration) 105 | } 106 | 107 | return r0 108 | } 109 | 110 | // Sleep provides a mock function with given fields: d 111 | func (_m *Implementation) Sleep(d time.Duration) { 112 | _m.Called(d) 113 | } 114 | 115 | // Until provides a mock function with given fields: t 116 | func (_m *Implementation) Until(t time.Time) time.Duration { 117 | ret := _m.Called(t) 118 | 119 | var r0 time.Duration 120 | if rf, ok := ret.Get(0).(func(time.Time) time.Duration); ok { 121 | r0 = rf(t) 122 | } else { 123 | r0 = ret.Get(0).(time.Duration) 124 | } 125 | 126 | return r0 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `timex` 2 | 3 | [![Build Status](https://travis-ci.org/cabify/timex.svg?branch=master)](https://travis-ci.org/cabify/timex) 4 | [![Coverage Status](https://coveralls.io/repos/github/cabify/timex/badge.svg?branch=master)](https://coveralls.io/github/cabify/timex?branch=master) 5 | [![GoDoc](https://godoc.org/github.com/cabify/timex?status.svg)](https://godoc.org/github.com/cabify/timex) 6 | 7 | `timex` is a test-friendly replacement for the `time` package. 8 | 9 | ## Usage 10 | 11 | Just replace your `time.Now()` by a `timex.Now()` call, etc. 12 | 13 | ## Mocking 14 | 15 | Use `timex.Override(...)` to replace the current implementation by another one, and use the function it returns to restore the default implementation. You can't override from several tests at the same time. You can use an auto-generated by [`mockery`][mockery] mock from `timexmock` package, or a controlled implementation from `timextest`. 16 | 17 | ### `timexmock` 18 | 19 | There's a `timexmock.Mocked(func(mocked *timexmock.Implementation) { ... })` wrapper that automatically creates a mock, sets it as the implementation to be used and defers a tear down to set the default implementation again. 20 | 21 | Example: 22 | 23 | ```go 24 | func TestSleep(t *testing.T) { 25 | timexmock.Mocked(func(mocked *timexmock.Implementation) { 26 | mocked.On("Sleep", someDuration).Once() 27 | defer mocked.AssertExpectations(t) 28 | 29 | timex.Sleep(someDuration) 30 | }) 31 | } 32 | ``` 33 | 34 | ### `timextest` 35 | 36 | Timextest provides a more complex API useful to control the behavior of concurrent programs, it is especially useful when the code interacts with timers like `time.Ticker`. Just like `timexmock`, `timextest` also provides a `timextest.Mocked(time.Time, func(*TestImplementation))` function to make mocking easier. Few examples can be found in [`timextest/example_test.go`](./timextest/example_test.go), this is one of them: 37 | 38 | ```go 39 | func ExampleTestImplementation_NewTicker() { 40 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 41 | go func() { 42 | ticker := timex.NewTicker(time.Hour) 43 | for t := range ticker.C() { 44 | fmt.Printf("%s\n", t) 45 | } 46 | }() 47 | 48 | tickerCall := <-mockedtimex.NewTickerCalls 49 | tickerCall.Mock.Tick(now.Add(time.Second)) 50 | tickerCall.Mock.Tick(now.Add(2 * time.Second)) 51 | 52 | // Output: 53 | // 2009-11-10 23:00:01 +0000 UTC 54 | // 2009-11-10 23:00:02 +0000 UTC 55 | }) 56 | } 57 | ``` 58 | 59 | ## Drawbacks 60 | 61 | ### Performance 62 | 63 | There's an obvious performance impact caused by the indirection of the call, it's actually 20-30% slower, however, in absolute numbers we're talking about 30 nanoseconds per call, so you probably should not worry about that. Notice that the difference is so small that it's not easy to get a stable result. 64 | 65 | ``` 66 | $ go test -run=NONE -benchmem -benchtime=5s -bench=. . 67 | goos: darwin 68 | goarch: amd64 69 | pkg: github.com/cabify/timex 70 | BenchmarkTimeNow-4 49619665 112 ns/op 0 B/op 0 allocs/op 71 | BenchmarkTimexNow-4 41256012 145 ns/op 0 B/op 0 allocs/op 72 | ``` 73 | 74 | If you're really worried about performance, you can disable part of the indirection by compiling with `timex_disable` tag, which will provide results similiar to the native implemenation calls: 75 | 76 | ``` 77 | $ go test -run=NONE -benchmem -benchtime=5s -bench=. -tags=timex_disable . 78 | goos: darwin 79 | goarch: amd64 80 | pkg: github.com/cabify/timex 81 | BenchmarkTimeNow-4 49866967 116 ns/op 0 B/op 0 allocs/op 82 | BenchmarkTimexNow-4 47965780 109 ns/op 0 B/op 0 allocs/op 83 | ``` 84 | 85 | ### Dogma 86 | 87 | Oh... yes, we're changing global variables and we'll obviously burn in hell, but if you're really into DI, you can also accept `timex.Implementation` interface as a dependency, and then inject either `timex.Default{}` or a testable implementation. 88 | 89 | [mockery]: https://github.com/vektra/mockery 90 | -------------------------------------------------------------------------------- /timextest/test_implementation.go: -------------------------------------------------------------------------------- 1 | package timextest 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/cabify/timex" 8 | ) 9 | 10 | // Mock mocks/stubs the timex functions so they return constant known values 11 | // Also mocks the timex.AfterFunc, returning the calls made to it through a channel. 12 | // It mocks Sleep too: the delayed function will just unlock the caller of Sleep. 13 | func Mock(now time.Time) *TestImplementation { 14 | impl := &TestImplementation{ 15 | SleepCalls: make(chan SleepCall), 16 | AfterCalls: make(chan AfterCall), 17 | AfterFuncCalls: make(chan AfterFuncCall), 18 | NewTickerCalls: make(chan NewTickerCall), 19 | NewTimerCalls: make(chan NewTimerCall), 20 | 21 | now: now, 22 | } 23 | 24 | impl.restore = timex.Override(impl) 25 | 26 | return impl 27 | } 28 | 29 | // Mocked mocks the current time and passes it to the provided function 30 | // Afterwards, it restores the default implementation 31 | func Mocked(now time.Time, f func(mocked *TestImplementation)) { 32 | mocked := Mock(now) 33 | defer mocked.TearDown() 34 | f(mocked) 35 | } 36 | 37 | // TestImplementation implements timex.Implementation for tests 38 | type TestImplementation struct { 39 | SleepCalls chan SleepCall 40 | AfterCalls chan AfterCall 41 | AfterFuncCalls chan AfterFuncCall 42 | NewTickerCalls chan NewTickerCall 43 | NewTimerCalls chan NewTimerCall 44 | 45 | sync.RWMutex 46 | now time.Time 47 | 48 | restore func() 49 | } 50 | 51 | // TearDown closes all the channels on the test implementation. 52 | // Useful to terminate goroutines watching those channels. 53 | func (ti *TestImplementation) TearDown() { 54 | close(ti.AfterFuncCalls) 55 | close(ti.AfterCalls) 56 | close(ti.NewTickerCalls) 57 | close(ti.NewTimerCalls) 58 | ti.RestoreDefaultImplementation() 59 | } 60 | 61 | // RestoreDefaultImplementation restores timex default implementation 62 | func (ti *TestImplementation) RestoreDefaultImplementation() { 63 | ti.restore() 64 | } 65 | 66 | // SetNow updates Now() value to a newer one 67 | func (ti *TestImplementation) SetNow(now time.Time) { 68 | ti.Lock() 69 | defer ti.Unlock() 70 | ti.now = now 71 | } 72 | 73 | // Now returns always the same now 74 | func (ti *TestImplementation) Now() time.Time { 75 | ti.RLock() 76 | defer ti.RUnlock() 77 | return ti.now 78 | } 79 | 80 | // Since returns the duration elapsed since mocked `now` 81 | func (ti *TestImplementation) Since(t time.Time) time.Duration { 82 | ti.RLock() 83 | defer ti.RUnlock() 84 | return ti.now.Sub(t) 85 | } 86 | 87 | // Until returns the duration until mocked `now` 88 | func (ti *TestImplementation) Until(t time.Time) time.Duration { 89 | ti.RLock() 90 | defer ti.RUnlock() 91 | return t.Sub(ti.now) 92 | } 93 | 94 | // Sleep sleeps until WakeUp is called 95 | func (ti *TestImplementation) Sleep(d time.Duration) { 96 | var wg sync.WaitGroup 97 | wg.Add(1) 98 | ti.SleepCalls <- SleepCall{Duration: d, WakeUp: wg.Done} 99 | wg.Wait() 100 | } 101 | 102 | // After returns a mocked timer 103 | func (ti *TestImplementation) After(d time.Duration) <-chan time.Time { 104 | timer := newMockedTimer() 105 | ti.AfterCalls <- AfterCall{Mock: timer, Duration: d} 106 | return timer.C() 107 | } 108 | 109 | // AfterFunc allows the AfterFunc mocking by pushing calls into ti.AfterFuncCalls 110 | func (ti *TestImplementation) AfterFunc(d time.Duration, f func()) timex.Timer { 111 | timer := newMockedFuncTimer(f) 112 | ti.AfterFuncCalls <- AfterFuncCall{Duration: d, Function: f, Mock: timer} 113 | return timer 114 | } 115 | 116 | // NewTicker returns a mocked ticker 117 | func (ti *TestImplementation) NewTicker(d time.Duration) timex.Ticker { 118 | ticker := newMockedTicker() 119 | ti.NewTickerCalls <- NewTickerCall{Duration: d, Mock: ticker} 120 | return ticker 121 | } 122 | 123 | // NewTimer returns a mocked timer 124 | func (ti *TestImplementation) NewTimer(d time.Duration) timex.Timer { 125 | timer := newMockedTimer() 126 | ti.NewTimerCalls <- NewTimerCall{Duration: d, Mock: timer} 127 | return timer 128 | } 129 | 130 | // AfterCall is a call to a mocked After 131 | // It relies on a MockedTimer which will never be stopped 132 | type AfterCall struct { 133 | Mock *MockedTimer 134 | Duration time.Duration 135 | } 136 | 137 | // NewTickerCall is a call to a mocked NewTicker 138 | type NewTickerCall struct { 139 | Mock *MockedTicker 140 | Duration time.Duration 141 | } 142 | 143 | // NewTimerCall is a call to a mocked NewTimer 144 | type NewTimerCall struct { 145 | Mock *MockedTimer 146 | Duration time.Duration 147 | } 148 | 149 | // AfterFuncCall represents a call made to timex.AfterFunc 150 | type AfterFuncCall struct { 151 | // Mock provides the underlying timer. Calling Trigger() on it will execute the function provided 152 | // in the calling goroutine 153 | Mock *MockedTimer 154 | Duration time.Duration 155 | // Function is the function provided, probably useless 156 | Function func() 157 | } 158 | 159 | // SleepCall represents a call made to timex.Sleep 160 | type SleepCall struct { 161 | // WakeUp allows calling goroutine to continue execution 162 | WakeUp func() 163 | Duration time.Duration 164 | } 165 | -------------------------------------------------------------------------------- /timextest/example_test.go: -------------------------------------------------------------------------------- 1 | package timextest_test 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/cabify/timex" 10 | "github.com/cabify/timex/timextest" 11 | ) 12 | 13 | var now = time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC) 14 | 15 | func ExampleTestImplementation_SetNow() { 16 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 17 | mockedtimex.SetNow(time.Unix(1, 0).UTC()) 18 | fmt.Println(timex.Now()) 19 | // Output: 20 | // 1970-01-01 00:00:01 +0000 UTC 21 | }) 22 | } 23 | 24 | func ExampleTestImplementation_Now() { 25 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 26 | fmt.Println(timex.Now()) 27 | // Output: 28 | // 2009-11-10 23:00:00 +0000 UTC 29 | }) 30 | } 31 | 32 | func ExampleTestImplementation_Since() { 33 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 34 | fmt.Println(timex.Since(now.Add(-time.Hour))) 35 | // Output: 36 | // 1h0m0s 37 | }) 38 | } 39 | 40 | func ExampleTestImplementation_Until() { 41 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 42 | fmt.Println(timex.Until(now.Add(time.Minute))) 43 | // Output: 44 | // 1m0s 45 | }) 46 | } 47 | 48 | // ExampleTestImplementation_Sleep observes the execution of a tempSwitch that uses timex.Sleep 49 | // To change the state of the program just temporarily 50 | // This way the assertions are deterministic and we don't have to wait the real amount of time 51 | func ExampleTestImplementation_Sleep() { 52 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 53 | sw := tempSwitch{new(int64)} 54 | done := make(chan struct{}) 55 | 56 | // Check it's turned off 57 | fmt.Printf("First state of switch is %t\n", sw.IsTurnedOn()) 58 | 59 | go func() { 60 | sw.TurnOn() 61 | close(done) 62 | }() 63 | 64 | // Wait until the program is sleeping 65 | sleepCall := <-mockedtimex.SleepCalls 66 | 67 | // Check it's turned on 68 | fmt.Printf("Then it's temporarily %t\n", sw.IsTurnedOn()) 69 | 70 | // Let it wake up and wait until TurnOn call finishes execution 71 | sleepCall.WakeUp() 72 | <-done 73 | 74 | // Check it's turned off again 75 | fmt.Printf("And then it's %t again\n", sw.IsTurnedOn()) 76 | 77 | // Output: 78 | // First state of switch is false 79 | // Then it's temporarily true 80 | // And then it's false again 81 | }) 82 | } 83 | 84 | func ExampleTestImplementation_After() { 85 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 86 | var value bool 87 | 88 | wg := sync.WaitGroup{} 89 | wg.Add(1) 90 | go func() { 91 | defer wg.Done() 92 | <-timex.After(time.Minute) 93 | value = false 94 | }() 95 | 96 | // Note that the race detector would usually complain about this read because it would be 97 | // racy with the test 98 | value = true 99 | 100 | (<-mockedtimex.AfterCalls).Mock.Trigger(time.Time{}) 101 | wg.Wait() 102 | 103 | fmt.Println(value) 104 | 105 | // Output: 106 | // false 107 | }) 108 | } 109 | 110 | func ExampleTestImplementation_AfterFunc() { 111 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 112 | go func() { 113 | timex.AfterFunc(time.Second, func() { fmt.Println("This happens a second later") }) 114 | timex.AfterFunc(time.Hour, func() { fmt.Println("This happens an hour later") }) 115 | }() 116 | 117 | firstCall := <-mockedtimex.AfterFuncCalls 118 | fmt.Printf("First function is scheduled for %s\n", firstCall.Duration) 119 | 120 | secondCall := <-mockedtimex.AfterFuncCalls 121 | fmt.Printf("Second function is scheduled for %s\n", secondCall.Duration) 122 | 123 | firstCall.Mock.Trigger(now) 124 | secondCall.Mock.Trigger(now) 125 | 126 | // Output: 127 | // First function is scheduled for 1s 128 | // Second function is scheduled for 1h0m0s 129 | // This happens a second later 130 | // This happens an hour later 131 | }) 132 | } 133 | 134 | func ExampleTestImplementation_NewTicker() { 135 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 136 | go func() { 137 | ticker := timex.NewTicker(time.Hour) 138 | for t := range ticker.C() { 139 | fmt.Printf("%s\n", t) 140 | } 141 | }() 142 | 143 | tickerCall := <-mockedtimex.NewTickerCalls 144 | tickerCall.Mock.Tick(now.Add(time.Second)) 145 | tickerCall.Mock.Tick(now.Add(2 * time.Second)) 146 | 147 | // Output: 148 | // 2009-11-10 23:00:01 +0000 UTC 149 | // 2009-11-10 23:00:02 +0000 UTC 150 | }) 151 | } 152 | 153 | func ExampleTestImplementation_NewTimer() { 154 | timextest.Mocked(now, func(mockedtimex *timextest.TestImplementation) { 155 | wg := sync.WaitGroup{} 156 | wg.Add(1) 157 | go func(d time.Duration) { 158 | defer wg.Done() 159 | ticker := timex.NewTimer(d) 160 | t := <-ticker.C() 161 | fmt.Println(t) 162 | }(time.Minute) 163 | 164 | timer := <-mockedtimex.NewTimerCalls 165 | timer.Mock.Trigger(now.Add(timer.Duration)) 166 | wg.Wait() 167 | 168 | // Output: 169 | // 2009-11-10 23:01:00 +0000 UTC 170 | }) 171 | } 172 | 173 | type tempSwitch struct{ val *int64 } 174 | 175 | func (ts tempSwitch) TurnOn() { 176 | atomic.AddInt64(ts.val, 1) 177 | timex.Sleep(time.Hour) 178 | atomic.AddInt64(ts.val, -1) 179 | } 180 | 181 | func (ts tempSwitch) IsTurnedOn() bool { 182 | return atomic.LoadInt64(ts.val) == 1 183 | } 184 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Maxi Mobility Spain SL and contributors. 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | ------------------------------------------------------------------------- 17 | Apache License 18 | Version 2.0, January 2004 19 | http://www.apache.org/licenses/ 20 | 21 | 22 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 23 | 24 | 1. Definitions. 25 | 26 | "License" shall mean the terms and conditions for use, reproduction, 27 | and distribution as defined by Sections 1 through 9 of this document. 28 | 29 | "Licensor" shall mean the copyright owner or entity authorized by 30 | the copyright owner that is granting the License. 31 | 32 | "Legal Entity" shall mean the union of the acting entity and all 33 | other entities that control, are controlled by, or are under common 34 | control with that entity. For the purposes of this definition, 35 | "control" means (i) the power, direct or indirect, to cause the 36 | direction or management of such entity, whether by contract or 37 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 38 | outstanding shares, or (iii) beneficial ownership of such entity. 39 | 40 | "You" (or "Your") shall mean an individual or Legal Entity 41 | exercising permissions granted by this License. 42 | 43 | "Source" form shall mean the preferred form for making modifications, 44 | including but not limited to software source code, documentation 45 | source, and configuration files. 46 | 47 | "Object" form shall mean any form resulting from mechanical 48 | transformation or translation of a Source form, including but 49 | not limited to compiled object code, generated documentation, 50 | and conversions to other media types. 51 | 52 | "Work" shall mean the work of authorship, whether in Source or 53 | Object form, made available under the License, as indicated by a 54 | copyright notice that is included in or attached to the work 55 | (an example is provided in the Appendix below). 56 | 57 | "Derivative Works" shall mean any work, whether in Source or Object 58 | form, that is based on (or derived from) the Work and for which the 59 | editorial revisions, annotations, elaborations, or other modifications 60 | represent, as a whole, an original work of authorship. For the purposes 61 | of this License, Derivative Works shall not include works that remain 62 | separable from, or merely link (or bind by name) to the interfaces of, 63 | the Work and Derivative Works thereof. 64 | 65 | "Contribution" shall mean any work of authorship, including 66 | the original version of the Work and any modifications or additions 67 | to that Work or Derivative Works thereof, that is intentionally 68 | submitted to Licensor for inclusion in the Work by the copyright owner 69 | or by an individual or Legal Entity authorized to submit on behalf of 70 | the copyright owner. For the purposes of this definition, "submitted" 71 | means any form of electronic, verbal, or written communication sent 72 | to the Licensor or its representatives, including but not limited to 73 | communication on electronic mailing lists, source code control systems, 74 | and issue tracking systems that are managed by, or on behalf of, the 75 | Licensor for the purpose of discussing and improving the Work, but 76 | excluding communication that is conspicuously marked or otherwise 77 | designated in writing by the copyright owner as "Not a Contribution." 78 | 79 | "Contributor" shall mean Licensor and any individual or Legal Entity 80 | on behalf of whom a Contribution has been received by Licensor and 81 | subsequently incorporated within the Work. 82 | 83 | 2. Grant of Copyright License. Subject to the terms and conditions of 84 | this License, each Contributor hereby grants to You a perpetual, 85 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 86 | copyright license to reproduce, prepare Derivative Works of, 87 | publicly display, publicly perform, sublicense, and distribute the 88 | Work and such Derivative Works in Source or Object form. 89 | 90 | 3. Grant of Patent License. Subject to the terms and conditions of 91 | this License, each Contributor hereby grants to You a perpetual, 92 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 93 | (except as stated in this section) patent license to make, have made, 94 | use, offer to sell, sell, import, and otherwise transfer the Work, 95 | where such license applies only to those patent claims licensable 96 | by such Contributor that are necessarily infringed by their 97 | Contribution(s) alone or by combination of their Contribution(s) 98 | with the Work to which such Contribution(s) was submitted. If You 99 | institute patent litigation against any entity (including a 100 | cross-claim or counterclaim in a lawsuit) alleging that the Work 101 | or a Contribution incorporated within the Work constitutes direct 102 | or contributory patent infringement, then any patent licenses 103 | granted to You under this License for that Work shall terminate 104 | as of the date such litigation is filed. 105 | 106 | 4. Redistribution. You may reproduce and distribute copies of the 107 | Work or Derivative Works thereof in any medium, with or without 108 | modifications, and in Source or Object form, provided that You 109 | meet the following conditions: 110 | 111 | (a) You must give any other recipients of the Work or 112 | Derivative Works a copy of this License; and 113 | 114 | (b) You must cause any modified files to carry prominent notices 115 | stating that You changed the files; and 116 | 117 | (c) You must retain, in the Source form of any Derivative Works 118 | that You distribute, all copyright, patent, trademark, and 119 | attribution notices from the Source form of the Work, 120 | excluding those notices that do not pertain to any part of 121 | the Derivative Works; and 122 | 123 | (d) If the Work includes a "NOTICE" text file as part of its 124 | distribution, then any Derivative Works that You distribute must 125 | include a readable copy of the attribution notices contained 126 | within such NOTICE file, excluding those notices that do not 127 | pertain to any part of the Derivative Works, in at least one 128 | of the following places: within a NOTICE text file distributed 129 | as part of the Derivative Works; within the Source form or 130 | documentation, if provided along with the Derivative Works; or, 131 | within a display generated by the Derivative Works, if and 132 | wherever such third-party notices normally appear. The contents 133 | of the NOTICE file are for informational purposes only and 134 | do not modify the License. You may add Your own attribution 135 | notices within Derivative Works that You distribute, alongside 136 | or as an addendum to the NOTICE text from the Work, provided 137 | that such additional attribution notices cannot be construed 138 | as modifying the License. 139 | 140 | You may add Your own copyright statement to Your modifications and 141 | may provide additional or different license terms and conditions 142 | for use, reproduction, or distribution of Your modifications, or 143 | for any such Derivative Works as a whole, provided Your use, 144 | reproduction, and distribution of the Work otherwise complies with 145 | the conditions stated in this License. 146 | 147 | 5. Submission of Contributions. Unless You explicitly state otherwise, 148 | any Contribution intentionally submitted for inclusion in the Work 149 | by You to the Licensor shall be under the terms and conditions of 150 | this License, without any additional terms or conditions. 151 | Notwithstanding the above, nothing herein shall supersede or modify 152 | the terms of any separate license agreement you may have executed 153 | with Licensor regarding such Contributions. 154 | 155 | 6. Trademarks. This License does not grant permission to use the trade 156 | names, trademarks, service marks, or product names of the Licensor, 157 | except as required for reasonable and customary use in describing the 158 | origin of the Work and reproducing the content of the NOTICE file. 159 | 160 | 7. Disclaimer of Warranty. Unless required by applicable law or 161 | agreed to in writing, Licensor provides the Work (and each 162 | Contributor provides its Contributions) on an "AS IS" BASIS, 163 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 164 | implied, including, without limitation, any warranties or conditions 165 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 166 | PARTICULAR PURPOSE. You are solely responsible for determining the 167 | appropriateness of using or redistributing the Work and assume any 168 | risks associated with Your exercise of permissions under this License. 169 | 170 | 8. Limitation of Liability. In no event and under no legal theory, 171 | whether in tort (including negligence), contract, or otherwise, 172 | unless required by applicable law (such as deliberate and grossly 173 | negligent acts) or agreed to in writing, shall any Contributor be 174 | liable to You for damages, including any direct, indirect, special, 175 | incidental, or consequential damages of any character arising as a 176 | result of this License or out of the use or inability to use the 177 | Work (including but not limited to damages for loss of goodwill, 178 | work stoppage, computer failure or malfunction, or any and all 179 | other commercial damages or losses), even if such Contributor 180 | has been advised of the possibility of such damages. 181 | 182 | 9. Accepting Warranty or Additional Liability. While redistributing 183 | the Work or Derivative Works thereof, You may choose to offer, 184 | and charge a fee for, acceptance of support, warranty, indemnity, 185 | or other liability obligations and/or rights consistent with this 186 | License. However, in accepting such obligations, You may act only 187 | on Your own behalf and on Your sole responsibility, not on behalf 188 | of any other Contributor, and only if You agree to indemnify, 189 | defend, and hold each Contributor harmless for any liability 190 | incurred by, or claims asserted against, such Contributor by reason 191 | of your accepting any such warranty or additional liability. 192 | 193 | END OF TERMS AND CONDITIONS 194 | --------------------------------------------------------------------------------