Skip to content

Commit b254b20

Browse files
committed
Initial commit.
0 parents  commit b254b20

File tree

6 files changed

+212
-0
lines changed

6 files changed

+212
-0
lines changed

.github/workflows/test.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Test
2+
on: [push, pull_request]
3+
jobs:
4+
test:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v4
8+
9+
- name: Install Go
10+
uses: actions/setup-go@v5
11+
with:
12+
go-version: "stable"
13+
14+
- name: Run test suite
15+
run: go test -v -coverprofile=profile.cov ./...
16+
17+
- name: Coveralls
18+
uses: coverallsapp/github-action@v2
19+
with:
20+
file: profile.cov

LICENSE.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2025 Nathan Osman
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
## mocktime
2+
3+
[![Build Status](https://github.com/nitroshare/mocktime/actions/workflows/test.yml/badge.svg)](https://github.com/nitroshare/mocktime/actions/workflows/test.yml)
4+
[![Coverage Status](https://coveralls.io/repos/github/nitroshare/mocktime/badge.svg?branch=main)](https://coveralls.io/github/nitroshare/mocktime?branch=main)
5+
[![Go Reference](https://pkg.go.dev/badge/github.com/nitroshare/mocktime.svg)](https://pkg.go.dev/github.com/nitroshare/mocktime)
6+
[![MIT License](https://img.shields.io/badge/license-MIT-9370d8.svg?style=flat)](https://opensource.org/licenses/MIT)
7+
8+
This package provides an easy way to mock specific functions in the `time` package:
9+
10+
```golang
11+
import "github.com/nitroshare/mocktime"
12+
13+
// Same as time.Now()
14+
mocktime.Now()
15+
16+
// Mock Now() and After()
17+
mocktime.Mock()
18+
defer mocktime.Unmock()
19+
20+
// All calls to Now() will return the same time...
21+
mocktime.Now()
22+
23+
// ...until the time is advanced
24+
mocktime.Advance(5 * time.Second)
25+
26+
// ...or explicitly set
27+
mocktime.Set(time.Date(2025, time.May, 1, 0, 0, 0, 0, time.UTC))
28+
29+
// Works with After() as well - this example will attempt to wait for five
30+
// seconds but will only wait one second since the mocked time is advanced ten
31+
// seconds in the goroutine, triggering the channel send.
32+
go func() {
33+
time.Sleep(1 * time.Second)
34+
fmt.Println("Advancing mocked time by 10 seconds")
35+
time.Advance(10 * time.Second)
36+
}()
37+
fmt.Println("Waiting 5 mocked seconds...")
38+
<-mocktime.After(5 * time.Second)
39+
fmt.Println("...time elapsed!")
40+
```

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/nitroshare/mocktime
2+
3+
go 1.23.0

mocktime.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package mocktime
2+
3+
import (
4+
"time"
5+
)
6+
7+
type afterChanData struct {
8+
expiry time.Time
9+
ch chan<- time.Time
10+
}
11+
12+
var (
13+
mockTime time.Time
14+
afterChan []*afterChanData
15+
)
16+
17+
// MockNow returns the current mocked time. Although this can be set by
18+
// reassigning Now, this is typically handled automatically by Mock.
19+
func MockNow() time.Time {
20+
return mockTime
21+
}
22+
23+
func MockAfter(d time.Duration) <-chan time.Time {
24+
ch := make(chan time.Time)
25+
afterChan = append(afterChan, &afterChanData{
26+
expiry: mockTime.Add(d),
27+
ch: ch,
28+
})
29+
return ch
30+
}
31+
32+
// Set explicitly sets the mocked time.
33+
func Set(t time.Time) {
34+
mockTime = t
35+
expInd := 0
36+
for i, v := range afterChan {
37+
if v.expiry.After(mockTime) {
38+
expInd = i
39+
break
40+
}
41+
go func() {
42+
v.ch <- v.expiry
43+
}()
44+
}
45+
afterChan = afterChan[expInd:]
46+
}
47+
48+
// Advance advances the mocked time by the specified duration.
49+
func Advance(d time.Duration) {
50+
Set(mockTime.Add(d))
51+
}
52+
53+
var (
54+
55+
// Now normally points to time.Now but can also be pointed to with MockNow.
56+
Now func() time.Time
57+
58+
// After normally points to time.After but can also be pointed to with MockAfter.
59+
After func(d time.Duration) <-chan time.Time
60+
)
61+
62+
func set() {
63+
Now = MockNow
64+
After = MockAfter
65+
}
66+
67+
func reset() {
68+
Now = time.Now
69+
After = time.After
70+
}
71+
72+
func init() {
73+
reset()
74+
}
75+
76+
// Mock replaces the time functions in this package with their mocked equivalents.
77+
func Mock() {
78+
set()
79+
}
80+
81+
// Unmock replaces the mocked time functions with their original equivalents.
82+
func Unmock() {
83+
reset()
84+
}

mocktime_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package mocktime
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func compare[T comparable](t *testing.T, v1, v2 T, same bool) {
9+
t.Helper()
10+
if same {
11+
if v1 != v2 {
12+
t.Fatalf("%v != %v", v1, v2)
13+
}
14+
} else {
15+
if v1 == v2 {
16+
t.Fatalf("%v == %v", v1, v2)
17+
}
18+
}
19+
}
20+
21+
func TestMockUnmock(t *testing.T) {
22+
compare(t, Now(), time.Time{}, false)
23+
Mock()
24+
compare(t, Now(), time.Time{}, true)
25+
Unmock()
26+
compare(t, Now(), time.Time{}, false)
27+
}
28+
29+
func TestSetAdvance(t *testing.T) {
30+
Mock()
31+
defer Unmock()
32+
v := time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
33+
Set(v)
34+
compare(t, Now(), v, true)
35+
d := 24 * time.Hour
36+
Advance(d)
37+
compare(t, Now(), v.Add(d), true)
38+
}
39+
40+
func TestAfter(t *testing.T) {
41+
Mock()
42+
defer Unmock()
43+
ch := After(2 * time.Second)
44+
Advance(1 * time.Second)
45+
select {
46+
case <-ch:
47+
t.Fatalf("unexpected read on channel")
48+
case <-time.After(10 * time.Millisecond):
49+
}
50+
Advance(2 * time.Second)
51+
select {
52+
case <-ch:
53+
case <-time.After(10 * time.Millisecond):
54+
t.Fatalf("unexpected block on channel")
55+
}
56+
}

0 commit comments

Comments
 (0)