initial implementation
This commit is contained in:
commit
ac9cdf9564
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.cover
|
||||||
28
Makefile
Normal file
28
Makefile
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
sources = $(shell find . -name "*.go")
|
||||||
|
|
||||||
|
default: build
|
||||||
|
|
||||||
|
build: $(sources)
|
||||||
|
go build
|
||||||
|
|
||||||
|
fmt: $(sources)
|
||||||
|
go fmt
|
||||||
|
|
||||||
|
check: $(sources)
|
||||||
|
go test -count 1
|
||||||
|
|
||||||
|
.cover: $(sources)
|
||||||
|
go test -count 1 -coverprofile .cover
|
||||||
|
|
||||||
|
cover: .cover
|
||||||
|
go tool cover -func .cover
|
||||||
|
|
||||||
|
showcover: .cover
|
||||||
|
go tool cover -html .cover
|
||||||
|
|
||||||
|
bench: $(sources)
|
||||||
|
go test -bench Benchmark -run ^$
|
||||||
|
|
||||||
|
clean:
|
||||||
|
go clean
|
||||||
|
rm .cover
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module code.squareroundforest.org/arpio/times
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
43
lib.go
Normal file
43
lib.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package times
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Clock interface {
|
||||||
|
Now() time.Time
|
||||||
|
After(time.Duration) <-chan time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestClock struct{
|
||||||
|
clock test
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sys() Clock {
|
||||||
|
return sys{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test() TestClock {
|
||||||
|
var zero time.Time
|
||||||
|
return TestFrom(zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFrom(t time.Time) TestClock {
|
||||||
|
return TestClock{clock: makeTestClock(t)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c TestClock) Now() time.Time {
|
||||||
|
return c.clock.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c TestClock) After(d time.Duration) <-chan time.Time {
|
||||||
|
return c.clock.after(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// negative pass not allowed
|
||||||
|
func (c TestClock) Pass(d time.Duration) {
|
||||||
|
c.clock.pass(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// backwards jump not allowed
|
||||||
|
func (c TestClock) Jump(t time.Time) {
|
||||||
|
c.clock.jump(t)
|
||||||
|
}
|
||||||
232
lib_test.go
Normal file
232
lib_test.go
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
package times_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"code.squareroundforest.org/arpio/times"
|
||||||
|
"time"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSys(t *testing.T) {
|
||||||
|
t.Run("now", func(t *testing.T) {
|
||||||
|
c := times.Sys()
|
||||||
|
t0 := time.Now()
|
||||||
|
tt := c.Now()
|
||||||
|
t1 := time.Now()
|
||||||
|
if tt.Before(t0) || tt.After(t1) {
|
||||||
|
t.Fatal(tt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("after", func(t *testing.T) {
|
||||||
|
c := times.Sys()
|
||||||
|
t0 := time.Now()
|
||||||
|
ct := c.After(time.Millisecond)
|
||||||
|
tt := <-ct
|
||||||
|
if tt.Before(t0.Add(time.Millisecond)) {
|
||||||
|
t.Fatal(tt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTest(t *testing.T) {
|
||||||
|
t.Run("now default", func(t *testing.T) {
|
||||||
|
c := times.Test()
|
||||||
|
t0 := c.Now()
|
||||||
|
t1 := c.Now()
|
||||||
|
t2 := c.Now()
|
||||||
|
if t0.After(t1) || t1.After(t2) {
|
||||||
|
t.Fatal(t0, t1, t2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("now custom initial", func(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
c := times.TestFrom(now)
|
||||||
|
t0 := c.Now()
|
||||||
|
t1 := c.Now()
|
||||||
|
t2 := c.Now()
|
||||||
|
if t0.Before(now) || t0.After(t1) || t1.After(t2) {
|
||||||
|
t.Fatal(now, t0, t1, t2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("now after pass", func(t *testing.T) {
|
||||||
|
c := times.Test()
|
||||||
|
t0 := c.Now()
|
||||||
|
c.Pass(time.Millisecond)
|
||||||
|
t1 := c.Now()
|
||||||
|
if t1.Before(t0.Add(time.Millisecond)) {
|
||||||
|
t.Fatal(t0, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("now after zero pass", func(t *testing.T) {
|
||||||
|
c := times.Test()
|
||||||
|
t0 := c.Now()
|
||||||
|
c.Pass(0)
|
||||||
|
t1 := c.Now()
|
||||||
|
if t1.Before(t0) {
|
||||||
|
t.Fatal(t0, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("now after negative pass", func(t *testing.T) {
|
||||||
|
c := times.Test()
|
||||||
|
t0 := c.Now()
|
||||||
|
c.Pass(-1000)
|
||||||
|
t1 := c.Now()
|
||||||
|
if t1.Before(t0) {
|
||||||
|
t.Fatal(t0, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("now after jump", func(t *testing.T) {
|
||||||
|
c := times.Test()
|
||||||
|
t0 := c.Now()
|
||||||
|
c.Jump(t0.Add(time.Millisecond))
|
||||||
|
t1 := c.Now()
|
||||||
|
if t1.Before(t0.Add(time.Millisecond)) {
|
||||||
|
t.Fatal(t0, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("now after jump present", func(t *testing.T) {
|
||||||
|
c := times.Test()
|
||||||
|
t0 := c.Now()
|
||||||
|
c.Jump(t0)
|
||||||
|
t1 := c.Now()
|
||||||
|
if t1.Before(t0) {
|
||||||
|
t.Fatal(t0, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("now after jump to past", func(t *testing.T) {
|
||||||
|
c := times.Test()
|
||||||
|
t0 := c.Now()
|
||||||
|
c.Jump(t0.Add(-1000))
|
||||||
|
t1 := c.Now()
|
||||||
|
if t1.Before(t0) {
|
||||||
|
t.Fatal(t0, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("after 0 default", func(t *testing.T) {
|
||||||
|
c := times.Test()
|
||||||
|
a := c.After(0)
|
||||||
|
t0 := c.Now()
|
||||||
|
tt := <-a
|
||||||
|
t1 := c.Now()
|
||||||
|
if t0.After(tt) || t1.Before(tt) {
|
||||||
|
t.Fatal(t0, tt, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("after 0 custom initial", func(t *testing.T) {
|
||||||
|
c := times.TestFrom(time.Now())
|
||||||
|
a := c.After(0)
|
||||||
|
t0 := c.Now()
|
||||||
|
tt := <-a
|
||||||
|
t1 := c.Now()
|
||||||
|
if t0.After(tt) || t1.Before(tt) {
|
||||||
|
t.Fatal(t0, tt, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("after negative default", func(t *testing.T) {
|
||||||
|
c := times.Test()
|
||||||
|
a := c.After(-1000)
|
||||||
|
t0 := c.Now()
|
||||||
|
tt := <-a
|
||||||
|
t1 := c.Now()
|
||||||
|
if t0.After(tt) || t1.Before(tt) {
|
||||||
|
t.Fatal(t0, tt, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("after negative custom initial", func(t *testing.T) {
|
||||||
|
c := times.TestFrom(time.Now())
|
||||||
|
a := c.After(-1000)
|
||||||
|
t0 := c.Now()
|
||||||
|
tt := <-a
|
||||||
|
t1 := c.Now()
|
||||||
|
if t0.After(tt) || t1.Before(tt) {
|
||||||
|
t.Fatal(t0, tt, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("after on pass", func(t *testing.T) {
|
||||||
|
c := times.Test()
|
||||||
|
a := c.After(2 * time.Millisecond)
|
||||||
|
select {
|
||||||
|
case <-a:
|
||||||
|
t.Fatal()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Pass(time.Millisecond)
|
||||||
|
select {
|
||||||
|
case <-a:
|
||||||
|
t.Fatal()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Pass(time.Millisecond)
|
||||||
|
t0 := c.Now()
|
||||||
|
tt := <-a
|
||||||
|
t1 := c.Now()
|
||||||
|
if t0.After(tt) || t1.Before(tt) {
|
||||||
|
t.Fatal(t0, tt, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("after on jump", func(t *testing.T) {
|
||||||
|
start := time.Now()
|
||||||
|
c := times.TestFrom(start)
|
||||||
|
a := c.After(2 * time.Millisecond)
|
||||||
|
select {
|
||||||
|
case <-a:
|
||||||
|
t.Fatal()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Jump(start.Add(time.Millisecond))
|
||||||
|
select {
|
||||||
|
case <-a:
|
||||||
|
t.Fatal()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Jump(start.Add(2 * time.Millisecond))
|
||||||
|
t0 := c.Now()
|
||||||
|
tt := <-a
|
||||||
|
t1 := c.Now()
|
||||||
|
if t0.After(tt) || t1.Before(tt) {
|
||||||
|
t.Fatal(t0, tt, t1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple afters set in varying order and only part trigger", func(t *testing.T) {
|
||||||
|
initOrder := []time.Duration{2, 1, 2, 3, 2, 3}
|
||||||
|
c := times.Test()
|
||||||
|
|
||||||
|
var a []<-chan time.Time
|
||||||
|
for _, d := range initOrder {
|
||||||
|
a = append(a, c.After(d * time.Millisecond))
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(a, func(i, j int) bool { return initOrder[i] < initOrder[j] })
|
||||||
|
sort.Slice(initOrder, func(i, j int) bool { return initOrder[i] < initOrder[j] })
|
||||||
|
|
||||||
|
var last time.Duration
|
||||||
|
for len(initOrder) > 0 {
|
||||||
|
if initOrder[0] > last {
|
||||||
|
c.Pass((initOrder[0] - last) * time.Millisecond)
|
||||||
|
last = initOrder[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
<-a[0]
|
||||||
|
a, initOrder = a[1:], initOrder[1:]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
21
license
Normal file
21
license
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Arpad Ryszka
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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.
|
||||||
13
sys.go
Normal file
13
sys.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package times
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type sys struct {}
|
||||||
|
|
||||||
|
func (sys) Now() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys) After(d time.Duration) <-chan time.Time {
|
||||||
|
return time.After(d)
|
||||||
|
}
|
||||||
102
test.go
Normal file
102
test.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package times
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type clockState struct {
|
||||||
|
current time.Time
|
||||||
|
chans map[time.Time][]chan<- time.Time
|
||||||
|
chantlist []time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type test struct {
|
||||||
|
state chan clockState
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTestClock(initial time.Time) test {
|
||||||
|
s := make(chan clockState, 1)
|
||||||
|
s <- clockState{
|
||||||
|
current: initial,
|
||||||
|
chans: make(map[time.Time][]chan<- time.Time),
|
||||||
|
}
|
||||||
|
|
||||||
|
return test{state: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func triggerChans(s clockState) clockState {
|
||||||
|
for len(s.chantlist) > 0 && !s.chantlist[0].After(s.current) {
|
||||||
|
var ct time.Time
|
||||||
|
ct, s.chantlist = s.chantlist[0], s.chantlist[1:]
|
||||||
|
for _, c := range s.chans[ct] {
|
||||||
|
c <- s.current
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.chans, ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t test) now() time.Time {
|
||||||
|
s := <-t.state
|
||||||
|
defer func() {
|
||||||
|
t.state <- s
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s.current
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t test) after(d time.Duration) <-chan time.Time {
|
||||||
|
s := <-t.state
|
||||||
|
defer func() {
|
||||||
|
t.state <- s
|
||||||
|
}()
|
||||||
|
|
||||||
|
c := make(chan time.Time, 1)
|
||||||
|
ct := s.current.Add(d)
|
||||||
|
if !ct.After(s.current) {
|
||||||
|
c <- s.current
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ctset := s.chans[ct]
|
||||||
|
s.chans[ct] = append(s.chans[ct], c)
|
||||||
|
if !ctset {
|
||||||
|
s.chantlist = append(s.chantlist, ct)
|
||||||
|
sort.Slice(s.chantlist, func(i, j int) bool {
|
||||||
|
return s.chantlist[i].Before(s.chantlist[j])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t test) pass(d time.Duration) {
|
||||||
|
s := <-t.state
|
||||||
|
defer func() {
|
||||||
|
t.state <- s
|
||||||
|
}()
|
||||||
|
|
||||||
|
if d < 0 {
|
||||||
|
d = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
s.current = s.current.Add(d)
|
||||||
|
s = triggerChans(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t test) jump(to time.Time) {
|
||||||
|
s := <-t.state
|
||||||
|
defer func() {
|
||||||
|
t.state <- s
|
||||||
|
}()
|
||||||
|
|
||||||
|
if to.Before(s.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.current = to
|
||||||
|
s = triggerChans(s)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user