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