├── .gitignore ├── go.mod ├── version.go ├── realticker_reset.go ├── realticker_reset_114.go ├── flextime_test.go ├── Makefile ├── fix.go ├── offset.go ├── faketicker_test.go ├── bench_test.go ├── faketimer_test.go ├── LICENSE ├── func.go ├── clock_test.go ├── .github └── workflows │ └── test.yaml ├── doc.go ├── faketicker.go ├── realclock.go ├── CHANGELOG.md ├── clock.go ├── fakeclock.go ├── flextime.go ├── README.md ├── faketimer.go ├── realclock_test.go └── func_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.gitignore 3 | !.github 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Songmu/flextime 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package flextime 2 | 3 | const version = "0.1.0" 4 | -------------------------------------------------------------------------------- /realticker_reset.go: -------------------------------------------------------------------------------- 1 | // +build go1.15 2 | 3 | package flextime 4 | 5 | import "time" 6 | 7 | func (t *realTicker) Reset(d time.Duration) { 8 | t.t.Reset(d) 9 | } 10 | -------------------------------------------------------------------------------- /realticker_reset_114.go: -------------------------------------------------------------------------------- 1 | // +build !go1.15 2 | 3 | package flextime 4 | 5 | import ( 6 | "log" 7 | "time" 8 | ) 9 | 10 | func (t *realTicker) Reset(d time.Duration) { 11 | log.Println("can't call ticker.Reset before Go 1.14") 12 | } 13 | -------------------------------------------------------------------------------- /flextime_test.go: -------------------------------------------------------------------------------- 1 | package flextime_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/Songmu/flextime" 8 | ) 9 | 10 | func TestRestore(t *testing.T) { 11 | flextime.Fix(baseDate) 12 | almostSameTime(t, flextime.Now(), baseDate) 13 | 14 | flextime.Restore() 15 | almostSameTime(t, flextime.Now(), time.Now()) 16 | } 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | u := $(if $(update),-u) 2 | 3 | export GO111MODULE=on 4 | 5 | .PHONY: deps 6 | deps: 7 | go get ${u} -d 8 | go mod tidy 9 | 10 | .PHONY: devel-deps 11 | devel-deps: 12 | sh -c '\ 13 | tmpdir=$$(mktemp -d); \ 14 | cd $$tmpdir; \ 15 | go get ${u} \ 16 | golang.org/x/lint/golint \ 17 | github.com/Songmu/godzil/cmd/godzil; \ 18 | rm -rf $$tmpdir' 19 | 20 | .PHONY: test 21 | test: 22 | go test -race 23 | 24 | .PHONY: lint 25 | lint: devel-deps 26 | golint -set_exit_status 27 | 28 | .PHONY: release 29 | release: devel-deps 30 | godzil release 31 | -------------------------------------------------------------------------------- /fix.go: -------------------------------------------------------------------------------- 1 | package flextime 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | func newFixedClock(t time.Time) Clock { 9 | return NewFakeClock(&fixedNS{t: t}) 10 | } 11 | 12 | type fixedNS struct { 13 | t time.Time 14 | mu sync.RWMutex 15 | } 16 | 17 | var _ NowSleeper = (*fixedNS)(nil) 18 | 19 | func (fi *fixedNS) Now() time.Time { 20 | fi.mu.RLock() 21 | defer fi.mu.RUnlock() 22 | return fi.t 23 | } 24 | 25 | func (fi *fixedNS) Sleep(d time.Duration) { 26 | if d <= 0 { 27 | return 28 | } 29 | fi.mu.Lock() 30 | defer fi.mu.Unlock() 31 | fi.t = fi.t.Add(d) 32 | } 33 | -------------------------------------------------------------------------------- /offset.go: -------------------------------------------------------------------------------- 1 | package flextime 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | func newOffsetClock(t time.Time) Clock { 9 | return NewFakeClock(&offsetNS{ 10 | offset: t.Sub(time.Now()), 11 | loc: t.Location(), 12 | }) 13 | } 14 | 15 | type offsetNS struct { 16 | offset time.Duration 17 | loc *time.Location 18 | 19 | mu sync.RWMutex 20 | } 21 | 22 | var _ NowSleeper = (*offsetNS)(nil) 23 | 24 | func (oc *offsetNS) Now() time.Time { 25 | oc.mu.RLock() 26 | defer oc.mu.RUnlock() 27 | return time.Now().Add(oc.offset).In(oc.loc) 28 | } 29 | 30 | func (oc *offsetNS) Sleep(d time.Duration) { 31 | if d <= 0 { 32 | return 33 | } 34 | oc.mu.Lock() 35 | defer oc.mu.Unlock() 36 | oc.offset = oc.offset + d 37 | } 38 | -------------------------------------------------------------------------------- /faketicker_test.go: -------------------------------------------------------------------------------- 1 | package flextime_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/Songmu/flextime" 8 | ) 9 | 10 | func TestFakeTicker_Reset(t *testing.T) { 11 | now := time.Now() 12 | restore := flextime.Fix(now) 13 | defer restore() 14 | ti := flextime.NewTicker(time.Second) 15 | ti.Reset(10 * time.Millisecond) 16 | expect := now.Add(1*time.Second + 10*time.Millisecond) 17 | almostSameTime(t, <-ti.C, expect) 18 | } 19 | 20 | func TestFakeTicker_Reset_panic(t *testing.T) { 21 | restore := flextime.Fix(time.Now()) 22 | expect := "non-positive interval for NewTicker" 23 | defer func() { 24 | err := recover() 25 | if err.(error).Error() != expect { 26 | t.Errorf("got %v\nwant %s", err, expect) 27 | } 28 | restore() 29 | }() 30 | flextime.NewTicker(-1) 31 | } 32 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package flextime_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "github.com/Songmu/flextime" 9 | ) 10 | 11 | func BenchmarkFlextime_Now(b *testing.B) { 12 | b.ResetTimer() 13 | 14 | for i := 0; i < b.N; i++ { 15 | flextime.Now() 16 | } 17 | } 18 | 19 | func BenchmarkStd_Now(b *testing.B) { 20 | b.ResetTimer() 21 | 22 | for i := 0; i < b.N; i++ { 23 | time.Now() 24 | } 25 | } 26 | 27 | func BenchmarkFlextime_Now_concur(b *testing.B) { 28 | var wg sync.WaitGroup 29 | wg.Add(b.N) 30 | b.ResetTimer() 31 | 32 | for i := 0; i < b.N; i++ { 33 | go func() { 34 | flextime.Now() 35 | wg.Done() 36 | }() 37 | } 38 | wg.Wait() 39 | } 40 | 41 | func BenchmarkStd_Now_concur(b *testing.B) { 42 | var wg sync.WaitGroup 43 | wg.Add(b.N) 44 | b.ResetTimer() 45 | 46 | for i := 0; i < b.N; i++ { 47 | go func() { 48 | time.Now() 49 | wg.Done() 50 | }() 51 | } 52 | wg.Wait() 53 | } 54 | -------------------------------------------------------------------------------- /faketimer_test.go: -------------------------------------------------------------------------------- 1 | package flextime_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "github.com/Songmu/flextime" 9 | ) 10 | 11 | type ns struct { 12 | } 13 | 14 | var _ flextime.NowSleeper = (*ns)(nil) 15 | 16 | func (n *ns) Now() time.Time { 17 | return time.Now() 18 | } 19 | 20 | func (n *ns) Sleep(d time.Duration) { 21 | time.Sleep(d) 22 | } 23 | 24 | func TestTimer_Stop(t *testing.T) { 25 | restore := flextime.Switch(flextime.NewFakeClock(&ns{})) 26 | defer restore() 27 | 28 | var ( 29 | ti = flextime.NewTimer(time.Second) 30 | fired = time.Now().Add(500 * time.Millisecond) 31 | trial = 5 32 | wg = sync.WaitGroup{} 33 | done = make(chan struct{}) 34 | ) 35 | wg.Add(trial) 36 | for i := 0; i < trial; i++ { 37 | // Call Stop() several times at the same time 38 | time.AfterFunc(time.Until(fired), func() { 39 | defer wg.Done() 40 | ti.Stop() 41 | }) 42 | } 43 | 44 | go func() { 45 | wg.Wait() 46 | close(done) 47 | }() 48 | 49 | select { 50 | case <-done: 51 | case <-time.After(2 * time.Second): 52 | t.Errorf("Timer.Stop() should not be blocked") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Songmu 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /func.go: -------------------------------------------------------------------------------- 1 | package flextime 2 | 3 | import "time" 4 | 5 | // Fix switches backend Clock and fixes the current time to the specified time. This differs 6 | // from `Set` in that the time does not change even if the time elapses during the test. 7 | // However, when Sleep is called, the virtual time is passed without actually pausing. 8 | // It returns a restore func and we can restore time behavior by calling it after the test. 9 | func Fix(t time.Time) (restore func()) { 10 | return Switch(newFixedClock(t)) 11 | } 12 | 13 | // Set switches backend Clock and sets the current time to the specified time. 14 | // It internally holds the offset and is affected by the passage of time during the test. 15 | // When Sleep is called, the virtual time is passed without actually pausing. 16 | // It returns a restore func and we can restore time behavior by calling it after the test. 17 | func Set(t time.Time) (restore func()) { 18 | return Switch(newOffsetClock(t)) 19 | } 20 | 21 | // NowFunc simply replace the function to get faked current time 22 | func NowFunc(now func() time.Time) (restore func()) { 23 | ns := newNowSleeper(now, time.Sleep) 24 | clock := NewFakeClock(ns) 25 | return Switch(clock) 26 | } 27 | -------------------------------------------------------------------------------- /clock_test.go: -------------------------------------------------------------------------------- 1 | package flextime_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/Songmu/flextime" 8 | ) 9 | 10 | type virtualNS struct { 11 | } 12 | 13 | var _ flextime.NowSleeper = (*virtualNS)(nil) 14 | 15 | func (vns *virtualNS) Now() time.Time { 16 | return time.Now() 17 | } 18 | 19 | func (nvs *virtualNS) Sleep(d time.Duration) { 20 | time.Sleep(d) 21 | } 22 | 23 | func TestClock_NewTimer(t *testing.T) { 24 | restore := flextime.Switch(flextime.NewFakeClock(&virtualNS{})) 25 | defer restore() 26 | 27 | ti := flextime.NewTimer(10 * time.Millisecond) 28 | if !ti.Stop() { 29 | t.Errorf("ti.Stop() should be true (active)") 30 | } 31 | if ti.Stop() { 32 | t.Errorf("ti.Stop() should be false after Stop() called once") 33 | } 34 | 35 | var blocked bool 36 | select { 37 | case <-ti.C: 38 | default: 39 | blocked = true 40 | } 41 | if !blocked { 42 | t.Errorf("channel of stopped Timer should be blocked") 43 | } 44 | 45 | if ti.Reset(50 * time.Millisecond) { 46 | t.Errorf("ti.Reset() should be false with stopped timer") 47 | } 48 | if !ti.Reset(50 * time.Millisecond) { 49 | t.Errorf("ti.Reset() should be true with active timer") 50 | } 51 | 52 | select { 53 | case <-time.After(70 * time.Millisecond): 54 | t.Errorf("ti.C should not be blocked but blocked") 55 | case <-ti.C: 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - "**" 6 | pull_request: {} 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: 14 | - ubuntu-latest 15 | - macOS-latest 16 | - windows-latest 17 | steps: 18 | - name: setup go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.x 22 | - name: checkout 23 | uses: actions/checkout@v2 24 | - name: lint 25 | run: | 26 | go get golang.org/x/lint/golint 27 | golint -set_exit_status ./... 28 | if: "matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'" 29 | - name: test 30 | run: go test -coverprofile coverage.out -covermode atomic ./... 31 | - name: Send coverage 32 | uses: shogo82148/actions-goveralls@v1 33 | with: 34 | github-token: ${{ secrets.github_token }} 35 | path-to-profile: coverage.out 36 | parallel: true 37 | job-number: ${{ strategy.job-index }} 38 | finish: 39 | runs-on: ubuntu-latest 40 | needs: test 41 | steps: 42 | - name: finish coverage report 43 | uses: shogo82148/actions-goveralls@v1 44 | with: 45 | github-token: ${{ secrets.github_token }} 46 | parallel-finished: true 47 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package flextime improves time testability by replacing the backend clock flexibly. 3 | 4 | It has a set of following 9 functions similar to the standard time package, making it easy to migrate 5 | from standard time package. 6 | 7 | now := flextime.Now() 8 | flextime.Sleep() 9 | d := flextime.Until(date) 10 | d := flextime.Since(date) 11 | <-flextime.After(5*time.Second) 12 | flextime.AfterFunc(5*time.Second, func() { fmt.Println("Done") }) 13 | timer := flextime.NewTimer(10*time.Second) 14 | ticker := flextime.NewTicker(10*time.Second) 15 | ch := flextime.Tick(3*time.Second) 16 | 17 | By default, it behaves the same as the standard time package, but allows us to change or fix 18 | the current time by using `Fix` and `Set` function. 19 | 20 | func () { // Set time 21 | restore := flextime.Set(time.Date(2001, time.May, 1, 10, 10, 10, 0, time.UTC)) 22 | defer restore() 23 | 24 | now = flextime.Now() // returned set time 25 | }() 26 | 27 | func () { // Fix time 28 | restore := flextime.Fix(time.Date(2001, time.May, 1, 10, 10, 10, 0, time.UTC)) 29 | defer restore() 30 | 31 | now = flextime.Now() // returned fixed time 32 | }() 33 | 34 | Also, we can replace the backend clock by implementing our own `Clock` interface and combining 35 | it with the Switch function. 36 | 37 | restore := flextime.Switch(clock) 38 | defer restore() 39 | */ 40 | package flextime 41 | -------------------------------------------------------------------------------- /faketicker.go: -------------------------------------------------------------------------------- 1 | package flextime 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type fakeTicker struct { 10 | Timer timerIface 11 | 12 | ch chan time.Time 13 | done chan struct{} 14 | 15 | dur time.Duration 16 | durMu sync.RWMutex 17 | } 18 | 19 | var _ tickerIface = (*fakeTicker)(nil) 20 | 21 | func newFakeTicker(t timerIface, d time.Duration) *Ticker { 22 | if d <= 0 { 23 | // I don't want to panic, but the standard package is too. 24 | panic(errors.New("non-positive interval for NewTicker")) 25 | } 26 | ftick := &fakeTicker{ 27 | Timer: t, 28 | ch: make(chan time.Time, 1), 29 | done: make(chan struct{}), 30 | dur: d, 31 | } 32 | go func() { 33 | c := ftick.Timer.C() 34 | for { 35 | select { 36 | case ti := <-c: 37 | ftick.ch <- ti 38 | ftick.Timer.Reset(ftick.getDur()) 39 | case <-ftick.done: 40 | return 41 | } 42 | } 43 | }() 44 | return createTicker(ftick) 45 | } 46 | 47 | func (ftick *fakeTicker) C() <-chan time.Time { 48 | return ftick.ch 49 | } 50 | 51 | func (ftick *fakeTicker) Stop() { 52 | ftick.Timer.Stop() 53 | close(ftick.done) 54 | } 55 | 56 | func (ftick *fakeTicker) Reset(d time.Duration) { 57 | ftick.setDur(d) 58 | ftick.Timer.Reset(ftick.getDur()) 59 | } 60 | 61 | func (ftick *fakeTicker) setDur(d time.Duration) { 62 | ftick.durMu.Lock() 63 | defer ftick.durMu.Unlock() 64 | ftick.dur = d 65 | } 66 | 67 | func (ftick *fakeTicker) getDur() time.Duration { 68 | ftick.durMu.RLock() 69 | defer ftick.durMu.RUnlock() 70 | return ftick.dur 71 | } 72 | -------------------------------------------------------------------------------- /realclock.go: -------------------------------------------------------------------------------- 1 | package flextime 2 | 3 | import "time" 4 | 5 | type realClock struct{} 6 | 7 | var _ Clock = (*realClock)(nil) 8 | 9 | func (clock *realClock) Now() time.Time { 10 | return time.Now() 11 | } 12 | 13 | func (clock *realClock) Since(t time.Time) time.Duration { 14 | return time.Since(t) 15 | } 16 | 17 | func (clock *realClock) Until(t time.Time) time.Duration { 18 | return time.Until(t) 19 | } 20 | 21 | func (clock *realClock) Sleep(d time.Duration) { 22 | time.Sleep(d) 23 | } 24 | 25 | func (clock *realClock) After(d time.Duration) <-chan time.Time { 26 | return clock.NewTimer(d).C 27 | } 28 | 29 | func (clock *realClock) AfterFunc(d time.Duration, f func()) *Timer { 30 | t := time.AfterFunc(d, f) 31 | return createTimer(&realTimer{ 32 | t: t, 33 | }) 34 | } 35 | 36 | func (clock *realClock) NewTimer(d time.Duration) *Timer { 37 | return createTimer(&realTimer{ 38 | t: time.NewTimer(d), 39 | }) 40 | } 41 | 42 | func (clock *realClock) NewTicker(d time.Duration) *Ticker { 43 | return createTicker(&realTicker{ 44 | t: time.NewTicker(d), 45 | }) 46 | } 47 | 48 | func (clock *realClock) Tick(d time.Duration) <-chan time.Time { 49 | return time.Tick(d) 50 | } 51 | 52 | type realTimer struct { 53 | t *time.Timer 54 | } 55 | 56 | func (t *realTimer) C() <-chan time.Time { 57 | return t.t.C 58 | } 59 | 60 | func (t *realTimer) Reset(d time.Duration) bool { 61 | return t.t.Reset(d) 62 | } 63 | 64 | func (t *realTimer) Stop() bool { 65 | return t.t.Stop() 66 | } 67 | 68 | type realTicker struct { 69 | t *time.Ticker 70 | } 71 | 72 | func (t *realTicker) C() <-chan time.Time { 73 | return t.t.C 74 | } 75 | 76 | func (t *realTicker) Stop() { 77 | t.t.Stop() 78 | } 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.1.0](https://github.com/Songmu/flextime/compare/v0.0.7...v0.1.0) (2020-08-30) 4 | 5 | * add ticker.Reset to follow Go 1.15 [#12](https://github.com/Songmu/flextime/pull/12) ([Songmu](https://github.com/Songmu)) 6 | * add flextime.NowFunc interface [#13](https://github.com/Songmu/flextime/pull/13) ([Songmu](https://github.com/Songmu)) 7 | 8 | ## [v0.0.7](https://github.com/Songmu/flextime/compare/v0.0.6...v0.0.7) (2020-07-06) 9 | 10 | * fix race [#11](https://github.com/Songmu/flextime/pull/11) ([Songmu](https://github.com/Songmu)) 11 | * add benchmark [#9](https://github.com/Songmu/flextime/pull/9) ([Songmu](https://github.com/Songmu)) 12 | 13 | ## [v0.0.6](https://github.com/Songmu/flextime/compare/v0.0.5...v0.0.6) (2020-01-20) 14 | 15 | * use RWMutex in offsetClock [#7](https://github.com/Songmu/flextime/pull/7) ([Songmu](https://github.com/Songmu)) 16 | 17 | ## [v0.0.5](https://github.com/Songmu/flextime/compare/v0.0.4...v0.0.5) (2020-01-19) 18 | 19 | * make capacity of faketicker.C 1 [#6](https://github.com/Songmu/flextime/pull/6) ([Songmu](https://github.com/Songmu)) 20 | 21 | ## [v0.0.4](https://github.com/Songmu/flextime/compare/v0.0.3...v0.0.4) (2020-01-19) 22 | 23 | * add test for panic [#5](https://github.com/Songmu/flextime/pull/5) ([Songmu](https://github.com/Songmu)) 24 | * capacity of Timer.C should be 1 [#4](https://github.com/Songmu/flextime/pull/4) ([Songmu](https://github.com/Songmu)) 25 | 26 | ## [v0.0.3](https://github.com/Songmu/flextime/compare/v0.0.2...v0.0.3) (2020-01-19) 27 | 28 | * Fix goroutine leak [#3](https://github.com/Songmu/flextime/pull/3) ([Songmu](https://github.com/Songmu)) 29 | 30 | ## [v0.0.2](https://github.com/Songmu/flextime/compare/v0.0.1...v0.0.2) (2020-01-18) 31 | 32 | * enhance testing [#2](https://github.com/Songmu/flextime/pull/2) ([Songmu](https://github.com/Songmu)) 33 | * make Timer and Ticker struct type instead of interface [#1](https://github.com/Songmu/flextime/pull/1) ([Songmu](https://github.com/Songmu)) 34 | 35 | ## [v0.0.1](https://github.com/Songmu/flextime/compare/fd5b95be9b8e...v0.0.1) (2020-01-16) 36 | 37 | -------------------------------------------------------------------------------- /clock.go: -------------------------------------------------------------------------------- 1 | package flextime 2 | 3 | import "time" 4 | 5 | // Clock is an interface that implements the functions of the standard time package 6 | type Clock interface { 7 | Now() time.Time 8 | Sleep(d time.Duration) 9 | Since(t time.Time) time.Duration 10 | Until(t time.Time) time.Duration 11 | 12 | After(d time.Duration) <-chan time.Time 13 | AfterFunc(d time.Duration, f func()) *Timer 14 | NewTimer(d time.Duration) *Timer 15 | NewTicker(d time.Duration) *Ticker 16 | Tick(d time.Duration) <-chan time.Time 17 | } 18 | 19 | // The Timer type represents a single event. It has same API with time.Timer 20 | type Timer struct { 21 | C <-chan time.Time 22 | timer timerIface 23 | } 24 | 25 | func createTimer(ti timerIface) *Timer { 26 | return &Timer{ 27 | C: ti.C(), 28 | timer: ti, 29 | } 30 | } 31 | 32 | // Stop prevents the Timer from firing. 33 | func (ti *Timer) Stop() bool { 34 | return ti.timer.Stop() 35 | } 36 | 37 | // Reset changes the timer to expire after duration d. 38 | func (ti *Timer) Reset(d time.Duration) bool { 39 | return ti.timer.Reset(d) 40 | } 41 | 42 | // timerIface has an interface similar to the standard time.Timer 43 | type timerIface interface { 44 | C() <-chan time.Time 45 | Reset(d time.Duration) bool 46 | Stop() bool 47 | } 48 | 49 | // A Ticker holds a channel that delivers `ticks' of a clock at intervals. 50 | // It has same API with time.Ticker 51 | type Ticker struct { 52 | C <-chan time.Time 53 | ticker tickerIface 54 | } 55 | 56 | func createTicker(ti tickerIface) *Ticker { 57 | return &Ticker{ 58 | C: ti.C(), 59 | ticker: ti, 60 | } 61 | } 62 | 63 | // Reset stops a ticker and resets its period to the specified duration 64 | // The next tick will arrive after the new period elapses. 65 | func (ti *Ticker) Reset(d time.Duration) { 66 | ti.ticker.Reset(d) 67 | } 68 | 69 | // Stop turns off a ticker. 70 | func (ti *Ticker) Stop() { 71 | ti.ticker.Stop() 72 | } 73 | 74 | // tickerIface has an interface similar to the standard time.Ticker 75 | type tickerIface interface { 76 | C() <-chan time.Time 77 | Reset(d time.Duration) 78 | Stop() 79 | } 80 | -------------------------------------------------------------------------------- /fakeclock.go: -------------------------------------------------------------------------------- 1 | package flextime 2 | 3 | import "time" 4 | 5 | // NowSleeper is, as the name implies, an interface with Now and Sleep methods. 6 | // By simply implementing these two methods, we can create an object with a Clock 7 | // interface by combining it with the NewFakeClock function. 8 | type NowSleeper interface { 9 | Now() time.Time 10 | Sleep(d time.Duration) 11 | } 12 | 13 | type nowSleeperImpl struct { 14 | now func() time.Time 15 | sleep func(d time.Duration) 16 | } 17 | 18 | var _ NowSleeper = (*nowSleeperImpl)(nil) 19 | 20 | func (ns *nowSleeperImpl) Now() time.Time { 21 | return ns.now() 22 | } 23 | 24 | func (ns *nowSleeperImpl) Sleep(d time.Duration) { 25 | ns.sleep(d) 26 | } 27 | 28 | func newNowSleeper(now func() time.Time, sleep func(d time.Duration)) NowSleeper { 29 | return &nowSleeperImpl{now: now, sleep: sleep} 30 | } 31 | 32 | type fakeClock struct { 33 | ns NowSleeper 34 | } 35 | 36 | // NewFakeClock accepts a NowSleeper interface and returns an object with a Clock interface. 37 | func NewFakeClock(ns NowSleeper) Clock { 38 | return &fakeClock{ns: ns} 39 | } 40 | 41 | var _ Clock = (*fakeClock)(nil) 42 | 43 | func (fc *fakeClock) Now() time.Time { 44 | return fc.ns.Now() 45 | } 46 | 47 | func (fc *fakeClock) Sleep(d time.Duration) { 48 | fc.ns.Sleep(d) 49 | } 50 | 51 | func (fc *fakeClock) Since(t time.Time) time.Duration { 52 | return fc.Now().Sub(t) 53 | } 54 | 55 | func (fc *fakeClock) Until(t time.Time) time.Duration { 56 | return t.Sub(fc.Now()) 57 | } 58 | 59 | func (fc *fakeClock) After(d time.Duration) <-chan time.Time { 60 | return fc.NewTimer(d).C 61 | } 62 | 63 | func (fc *fakeClock) AfterFunc(d time.Duration, f func()) *Timer { 64 | return createTimer(newFakeTimer(fc.ns, d, f)) 65 | } 66 | 67 | func (fc *fakeClock) NewTicker(d time.Duration) *Ticker { 68 | ti := newFakeTimer(fc.ns, d, nil) 69 | ti.IsTicker = true 70 | return newFakeTicker(ti, d) 71 | } 72 | 73 | func (fc *fakeClock) NewTimer(d time.Duration) *Timer { 74 | return createTimer(newFakeTimer(fc.ns, d, nil)) 75 | } 76 | 77 | func (fc *fakeClock) Tick(d time.Duration) <-chan time.Time { 78 | if d <= 0 { 79 | return nil 80 | } 81 | return fc.NewTicker(d).C 82 | } 83 | -------------------------------------------------------------------------------- /flextime.go: -------------------------------------------------------------------------------- 1 | package flextime 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | var ( 9 | backend Clock = &realClock{} 10 | backendMu sync.RWMutex 11 | ) 12 | 13 | func getBackend() Clock { 14 | backendMu.RLock() 15 | defer backendMu.RUnlock() 16 | return backend 17 | } 18 | 19 | // Switch switches backend Clock. Is it useful for testing. 20 | func Switch(c Clock) (restore func()) { 21 | backendMu.Lock() 22 | defer backendMu.Unlock() 23 | orig := backend 24 | backend = c 25 | return func() { Switch(orig) } 26 | } 27 | 28 | // Restore the default real Clock 29 | func Restore() { 30 | Switch(&realClock{}) 31 | } 32 | 33 | // Now returns the current time from backend Clock. 34 | func Now() time.Time { 35 | return getBackend().Now() 36 | } 37 | 38 | // Sleep pauses the current process for at least the duration d using backend Clock. 39 | func Sleep(d time.Duration) { 40 | getBackend().Sleep(d) 41 | } 42 | 43 | // Since returns the time elapsed since t using backend Clock. 44 | func Since(t time.Time) time.Duration { 45 | return getBackend().Since(t) 46 | } 47 | 48 | // Until returns the duration until t using backend Clock. 49 | func Until(t time.Time) time.Duration { 50 | return getBackend().Until(t) 51 | } 52 | 53 | // After waits for the duration to elapse and then sends the current time on the returned 54 | // channel using backend Clock. 55 | func After(d time.Duration) <-chan time.Time { 56 | return getBackend().After(d) 57 | } 58 | 59 | // AfterFunc waits for the duration to elapse and then calls f in its own goroutine using 60 | // backend Clock. It returns a Timer that can be used to cancel the call using its Stop method. 61 | func AfterFunc(d time.Duration, f func()) *Timer { 62 | return getBackend().AfterFunc(d, f) 63 | } 64 | 65 | // NewTimer creates a new Timer that will send the current time on its channel after at 66 | // least duration d using backend Clock. 67 | func NewTimer(d time.Duration) *Timer { 68 | return getBackend().NewTimer(d) 69 | } 70 | 71 | // NewTicker returns a new Ticker containing a channel that will send the time with a period 72 | // specified by the duration argument using backend Clock. 73 | func NewTicker(d time.Duration) *Ticker { 74 | return getBackend().NewTicker(d) 75 | } 76 | 77 | // Tick is a convenience wrapper for NewTicker providing access to the ticking channel only 78 | // using backend Clock. 79 | func Tick(d time.Duration) <-chan time.Time { 80 | return getBackend().Tick(d) 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | flextime 2 | ======= 3 | 4 | [![Test Status](https://github.com/Songmu/flextime/workflows/test/badge.svg?branch=master)][actions] 5 | [![Coverage Status](https://coveralls.io/repos/Songmu/flextime/badge.svg?branch=master)][coveralls] 6 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)][license] 7 | [![GoDoc](https://godoc.org/github.com/Songmu/flextime?status.svg)][godoc] 8 | 9 | [actions]: https://github.com/Songmu/flextime/actions?workflow=test 10 | [coveralls]: https://coveralls.io/r/Songmu/flextime?branch=master 11 | [license]: https://github.com/Songmu/flextime/blob/master/LICENSE 12 | [godoc]: https://godoc.org/github.com/Songmu/flextime 13 | 14 | flextime improves time testability by replacing the backend clock flexibly. 15 | 16 | ## Synopsis 17 | 18 | ```go 19 | import "github.com/Songmu/flextime" 20 | 21 | now := flextime.Now() // returned normal current time by default 22 | flextime.Sleep() 23 | d := flextime.Until(date) 24 | d := flextime.Since(date) 25 | <-flextime.After(5*time.Second) 26 | flextime.AfterFunc(5*time.Second, func() { fmt.Println("Done") }) 27 | timer := flextime.NewTimer(10*time.Second) 28 | ticker := flextime.NewTicker(10*time.Second) 29 | ch := flextime.Tick(3*time.Second) 30 | 31 | func () { // Set time 32 | restore := flextime.Set(time.Date(2001, time.May, 1, 10, 10, 10, 0, time.UTC)) 33 | defer restore() 34 | 35 | now = flextime.Now() // returned set time 36 | }() 37 | 38 | func () { // Fix time 39 | restore := flextime.Fix(time.Date(2001, time.May, 1, 10, 10, 10, 0, time.UTC)) 40 | defer restore() 41 | 42 | now = flextime.Now() // returned fixed time 43 | }() 44 | ``` 45 | 46 | ## Description 47 | 48 | The flextime improves time testability by replacing the backend clock flexibly. 49 | 50 | It has a set of functions similar to the standard time package, making it easy to migrate 51 | from standard time package. 52 | 53 | By default, it behaves the same as the standard time package, but allows us to change or fix 54 | the current time by using `Fix` and `Set` function. 55 | 56 | Also, we can replace the backend clock by implementing our own `Clock` interface and combining 57 | it with the Switch function. 58 | 59 | ## Installation 60 | 61 | ```console 62 | % go get github.com/Songmu/flextime 63 | ``` 64 | 65 | ## Migration 66 | 67 | You can almost migrate from standard time package to Songmu/flextime with the following command. 68 | 69 | ```console 70 | % go get github.com/Songmu/flextime 71 | % find . -name '*.go' | xargs perl -i -pe 's/\btime\.((?:N(?:ewTi(?:ck|m)er|ow)|After(?:Func)?|Sleep|Until|Tick))/flextime.$1/g' 72 | % goimport -w . 73 | ``` 74 | 75 | ## Author 76 | 77 | [Songmu](https://github.com/Songmu) 78 | -------------------------------------------------------------------------------- /faketimer.go: -------------------------------------------------------------------------------- 1 | package flextime 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type fakeTimer struct { 9 | T NowSleeper 10 | IsTicker bool 11 | fun func() 12 | 13 | resetMu sync.Mutex 14 | 15 | ch, inch chan time.Time 16 | stop, done chan struct{} 17 | doneMu sync.RWMutex 18 | triggerAt time.Time 19 | } 20 | 21 | var _ timerIface = (*fakeTimer)(nil) 22 | 23 | func newFakeTimer(c NowSleeper, d time.Duration, f func()) *fakeTimer { 24 | fti := &fakeTimer{ 25 | T: c, 26 | ch: make(chan time.Time, 1), 27 | inch: make(chan time.Time), 28 | stop: make(chan struct{}, 1), 29 | done: make(chan struct{}), 30 | fun: f, 31 | } 32 | close(fti.done) 33 | fti.Reset(d) 34 | return fti 35 | } 36 | 37 | func (fti *fakeTimer) doneCh() chan struct{} { 38 | fti.doneMu.RLock() 39 | defer fti.doneMu.RUnlock() 40 | return fti.done 41 | } 42 | 43 | func (fti *fakeTimer) renewDone() chan struct{} { 44 | fti.doneMu.Lock() 45 | defer fti.doneMu.Unlock() 46 | fti.done = make(chan struct{}) 47 | return fti.done 48 | } 49 | 50 | func (fti *fakeTimer) isActive() bool { 51 | select { 52 | case <-fti.doneCh(): 53 | return false 54 | default: 55 | return true 56 | } 57 | } 58 | 59 | func (fti *fakeTimer) C() <-chan time.Time { 60 | return fti.ch 61 | } 62 | 63 | // The `send` is called only inside `Reset` and exclusive control is performed on the `Reset` side, 64 | // so the `send` itself need not do exclusive control. 65 | func (fti *fakeTimer) send() { 66 | done := fti.renewDone() 67 | 68 | go func() { 69 | select { 70 | case t := <-fti.inch: 71 | if fti.fun != nil { 72 | go fti.fun() 73 | } else { 74 | fti.ch <- t 75 | } 76 | case <-done: 77 | } 78 | }() 79 | 80 | go func() { 81 | select { 82 | case fti.inch <- func() time.Time { 83 | fti.T.Sleep(fti.triggerAt.Sub(fti.T.Now())) 84 | return fti.triggerAt 85 | }(): 86 | case <-fti.stop: 87 | } 88 | close(done) 89 | }() 90 | } 91 | 92 | func (fti *fakeTimer) Reset(d time.Duration) bool { 93 | fti.resetMu.Lock() 94 | defer fti.resetMu.Unlock() 95 | if d < 0 { 96 | d = 0 97 | } 98 | active := fti.Stop() 99 | if fti.IsTicker && !fti.triggerAt.IsZero() { 100 | // to keep base time 101 | now := fti.T.Now() 102 | nextDur := d - (now.Sub(fti.triggerAt) % d) 103 | fti.triggerAt = now.Add(nextDur) 104 | } else { 105 | fti.triggerAt = fti.T.Now().Add(d) 106 | } 107 | fti.send() 108 | return active 109 | } 110 | 111 | func (fti *fakeTimer) Stop() bool { 112 | active := fti.isActive() 113 | // If multiple `Reset` are called concurrently, this termination process would run at the same time 114 | // and it returns `true` for each call, but it is no problem because time.Timer of the standard package 115 | // behaves like that. 116 | if active { 117 | fti.stop <- struct{}{} 118 | <-fti.doneCh() 119 | // The Timer may be fired at the same timing as the Stop. Also, the multiple `Reset` may be called 120 | // concurrently. In that case, `struct{}{}` could be accumulated in the channel, so drain it here. 121 | select { 122 | case <-fti.stop: 123 | default: 124 | } 125 | } 126 | return active 127 | } 128 | -------------------------------------------------------------------------------- /realclock_test.go: -------------------------------------------------------------------------------- 1 | package flextime_test 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | 8 | "github.com/Songmu/flextime" 9 | ) 10 | 11 | func TestRealClock(t *testing.T) { 12 | t.Run("Tick", func(t *testing.T) { 13 | interval := 123 * time.Millisecond 14 | ch := flextime.Tick(interval) 15 | 16 | t1 := <-ch 17 | almostNow(t, t1) 18 | 19 | t2 := <-ch 20 | almostNow(t, t2) 21 | 22 | if g, e := t2.Sub(t1), 100*time.Millisecond; g < e { 23 | t.Errorf("t2.Sub(t1) less than %s: %s", e, g) 24 | } 25 | 26 | if flextime.Tick(-1) != nil { 27 | t.Errorf("Tick with negative value should return nil but not") 28 | } 29 | }) 30 | 31 | base := time.Now() 32 | t.Run("Now", func(t *testing.T) { 33 | almostNow(t, flextime.Now()) 34 | }) 35 | 36 | sleep := 2 * time.Second 37 | t.Run("Sleep", func(t *testing.T) { 38 | flextime.Sleep(sleep) 39 | almostNow(t, flextime.Now()) 40 | }) 41 | 42 | t.Run("Since", func(t *testing.T) { 43 | since := flextime.Since(base) 44 | almostSameDuration(t, since, sleep) 45 | }) 46 | 47 | t.Run("Until", func(t *testing.T) { 48 | until := flextime.Until(base) 49 | almostSameDuration(t, -until, sleep) 50 | }) 51 | 52 | t.Run("After", func(t *testing.T) { 53 | got := <-flextime.After(200 * time.Microsecond) 54 | almostNow(t, got) 55 | }) 56 | 57 | t.Run("AfterFunc", func(t *testing.T) { 58 | after := 300 * time.Microsecond 59 | var ( 60 | fired bool 61 | done = make(chan struct{}) 62 | ) 63 | ti := flextime.AfterFunc(after, func() { 64 | fired = true 65 | done <- struct{}{} 66 | }) 67 | <-done 68 | if !fired { 69 | t.Errorf("AfterFunc not fired") 70 | } 71 | var blocked bool 72 | select { 73 | case <-ti.C: 74 | default: 75 | blocked = true 76 | } 77 | if !blocked { 78 | t.Errorf("Timer.C with AfterFunc should be blocked") 79 | } 80 | 81 | t.Run("Reset", func(t *testing.T) { 82 | fired = false 83 | after := 500 * time.Microsecond 84 | ti.Reset(after) 85 | <-done 86 | if !fired { 87 | t.Errorf("AfterFunc not fired") 88 | } 89 | }) 90 | 91 | t.Run("Stop", func(t *testing.T) { 92 | if ti.Stop() { 93 | t.Errorf("Timer should be stopped but active") 94 | } 95 | }) 96 | }) 97 | 98 | t.Run("NewTimer", func(t *testing.T) { 99 | after := 700 * time.Microsecond 100 | ti := flextime.NewTimer(after) 101 | got := <-ti.C 102 | almostNow(t, got) 103 | 104 | var blocked bool 105 | select { 106 | case <-ti.C: 107 | default: 108 | blocked = true 109 | } 110 | if !blocked { 111 | t.Errorf("drained Timer.C should be blocked") 112 | } 113 | 114 | t.Run("Reset", func(t *testing.T) { 115 | after := 600 * time.Microsecond 116 | ti.Reset(after) 117 | got := <-ti.C 118 | almostNow(t, got) 119 | 120 | // A negative or zero duration return immediately 121 | ti.Reset(-after) 122 | got = <-ti.C 123 | almostNow(t, got) 124 | }) 125 | 126 | t.Run("Stop", func(t *testing.T) { 127 | if ti.Stop() { 128 | t.Errorf("Timer should be stopped but active") 129 | } 130 | }) 131 | }) 132 | 133 | t.Run("NewTicker", func(t *testing.T) { 134 | interval := 10 * time.Millisecond 135 | ti := flextime.NewTicker(interval) 136 | ti.Reset(interval) 137 | almostNow(t, <-ti.C) 138 | ti.Stop() 139 | select { 140 | case <-ti.C: 141 | t.Errorf("ti.C should be blocked but not") 142 | default: 143 | } 144 | }) 145 | 146 | } 147 | 148 | func almostNow(t *testing.T, g time.Time) { 149 | e := time.Now() 150 | t.Helper() 151 | if time.Duration(math.Abs(float64(g.Sub(e)))) > time.Millisecond || 152 | g.Location().String() != e.Location().String() { 153 | 154 | t.Errorf("got: %s, expect: %s", g, e) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /func_test.go: -------------------------------------------------------------------------------- 1 | package flextime_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/Songmu/flextime" 8 | ) 9 | 10 | var baseDate = time.Date(2080, time.June, 5, 22, 10, 10, 0, time.UTC) 11 | 12 | func runTests(t *testing.T, fn func(t time.Time) func()) { 13 | t.Run("Tick", func(t *testing.T) { 14 | restore := fn(baseDate) 15 | defer restore() 16 | expect := baseDate 17 | 18 | interval := 13 * time.Second 19 | ch := flextime.Tick(interval) 20 | expect = expect.Add(interval) 21 | almostSameTime(t, <-ch, expect) 22 | 23 | expect = expect.Add(interval) 24 | almostSameTime(t, <-ch, expect) 25 | 26 | if flextime.Tick(-1) != nil { 27 | t.Errorf("Tick with negative value should return nil but not") 28 | } 29 | }) 30 | 31 | restore := fn(baseDate) 32 | defer restore() 33 | 34 | expect := baseDate 35 | t.Run("Now", func(t *testing.T) { 36 | almostSameTime(t, flextime.Now(), expect) 37 | }) 38 | 39 | sleep := 2 * time.Second 40 | t.Run("Sleep", func(t *testing.T) { 41 | flextime.Sleep(sleep) 42 | almostSameTime(t, flextime.Now(), expect.Add(sleep)) 43 | }) 44 | 45 | t.Run("Since", func(t *testing.T) { 46 | since := flextime.Since(expect) 47 | almostSameDuration(t, since, sleep) 48 | }) 49 | 50 | t.Run("Until", func(t *testing.T) { 51 | until := flextime.Until(expect) 52 | almostSameDuration(t, -until, sleep) 53 | }) 54 | 55 | expect = expect.Add(sleep) 56 | 57 | t.Run("After", func(t *testing.T) { 58 | after := 3 * time.Second 59 | got := <-flextime.After(after) 60 | expect = expect.Add(after) 61 | almostSameTime(t, got, expect) 62 | }) 63 | 64 | t.Run("AfterFunc", func(t *testing.T) { 65 | after := 5 * time.Second 66 | var ( 67 | fired bool 68 | done = make(chan struct{}) 69 | ) 70 | ti := flextime.AfterFunc(after, func() { 71 | fired = true 72 | done <- struct{}{} 73 | }) 74 | expect = expect.Add(after) 75 | <-done 76 | if !fired { 77 | t.Errorf("AfterFunc not fired") 78 | } 79 | var blocked bool 80 | select { 81 | case <-ti.C: 82 | default: 83 | blocked = true 84 | } 85 | if !blocked { 86 | t.Errorf("Timer.C with AfterFunc should be blocked") 87 | } 88 | 89 | t.Run("Reset", func(t *testing.T) { 90 | fired = false 91 | after := 7 * time.Second 92 | ti.Reset(after) 93 | expect = expect.Add(after) 94 | <-done 95 | if !fired { 96 | t.Errorf("AfterFunc not fired") 97 | } 98 | }) 99 | 100 | t.Run("Stop", func(t *testing.T) { 101 | time.Sleep(10 * time.Millisecond) 102 | if ti.Stop() { 103 | t.Errorf("Timer should be stopped but active") 104 | } 105 | }) 106 | }) 107 | 108 | t.Run("NewTimer", func(t *testing.T) { 109 | after := 4 * time.Second 110 | ti := flextime.NewTimer(after) 111 | expect = expect.Add(after) 112 | got := <-ti.C 113 | almostSameTime(t, got, expect) 114 | 115 | var blocked bool 116 | select { 117 | case <-ti.C: 118 | default: 119 | blocked = true 120 | } 121 | if !blocked { 122 | t.Errorf("drained Timer.C should be blocked") 123 | } 124 | 125 | t.Run("Reset", func(t *testing.T) { 126 | after := 6 * time.Second 127 | ti.Reset(after) 128 | expect = expect.Add(after) 129 | got := <-ti.C 130 | almostSameTime(t, got, expect) 131 | 132 | // A negative or zero duration return immediately 133 | ti.Reset(-after) 134 | got = <-ti.C 135 | almostSameTime(t, got, expect) 136 | }) 137 | 138 | t.Run("Stop", func(t *testing.T) { 139 | time.Sleep(10 * time.Millisecond) 140 | if ti.Stop() { 141 | t.Errorf("Timer should be stopped but active") 142 | } 143 | }) 144 | }) 145 | 146 | t.Run("NewTicker", func(t *testing.T) { 147 | interval := 11 * time.Second 148 | ti := flextime.NewTicker(interval) 149 | expect = expect.Add(interval) 150 | almostSameTime(t, <-ti.C, expect) 151 | ti.Stop() 152 | select { 153 | case <-ti.C: 154 | default: 155 | } 156 | }) 157 | } 158 | 159 | func almostSameDuration(t *testing.T, g, e time.Duration) { 160 | t.Helper() 161 | if int(g.Seconds()) != int(e.Seconds()) { 162 | t.Errorf("got: %s, expect: %s", g, e) 163 | } 164 | } 165 | 166 | func almostSame(g, e time.Time) bool { 167 | return g.Unix() != e.Unix() || g.Location().String() != e.Location().String() 168 | } 169 | 170 | func almostSameTime(t *testing.T, g, e time.Time) { 171 | t.Helper() 172 | if almostSame(g, e) { 173 | t.Errorf("\n got: %s\nexpect: %s", g, e) 174 | } 175 | } 176 | 177 | func TestFix(t *testing.T) { 178 | runTests(t, flextime.Fix) 179 | } 180 | 181 | func TestSet(t *testing.T) { 182 | runTests(t, flextime.Set) 183 | } 184 | 185 | func TestFix_NewTicker_withSleep(t *testing.T) { 186 | restore := flextime.Fix(baseDate) 187 | defer restore() 188 | 189 | expect := baseDate 190 | interval := 11 * time.Second 191 | ti := flextime.NewTicker(interval) 192 | sleep := 25 * time.Second 193 | flextime.Sleep(sleep) 194 | almostSameTime(t, <-ti.C, expect.Add(interval)) 195 | expect = expect.Add(3 * interval) // 11x3 keep base time 196 | expect2 := expect.Add(interval) 197 | got := <-ti.C 198 | if !almostSame(got, expect) && !almostSame(got, expect2) { 199 | t.Errorf("got: %s, expect: %s", got, expect) 200 | } 201 | } 202 | 203 | func TestFix_fix(t *testing.T) { 204 | restore := flextime.Fix(baseDate) 205 | defer restore() 206 | 207 | time.Sleep(10 * time.Microsecond) 208 | if !flextime.Now().Equal(baseDate) { 209 | t.Errorf("time doesn't fixed") 210 | } 211 | } 212 | 213 | func TestSet_slide(t *testing.T) { 214 | restore := flextime.Set(baseDate) 215 | defer restore() 216 | 217 | offset := 10 * time.Microsecond 218 | time.Sleep(offset) 219 | if flextime.Now().Sub(baseDate.Add(offset)) <= 0 { 220 | t.Errorf("time doesn't slide") 221 | } 222 | } 223 | 224 | func TestNowFunc(t *testing.T) { 225 | ti := time.Date(2020, 8, 300, 17, 47, 9, 0, time.UTC) 226 | restore := flextime.NowFunc(func() time.Time { 227 | return ti 228 | }) 229 | defer restore() 230 | 231 | now := flextime.Now() 232 | almostSameTime(t, now, ti) 233 | 234 | flextime.Sleep(0) 235 | now = flextime.Now() 236 | almostSameTime(t, now, ti) 237 | } 238 | --------------------------------------------------------------------------------