1
0

initial implementation

This commit is contained in:
Arpad Ryszka 2026-03-04 21:11:49 +01:00
commit ac9cdf9564
9 changed files with 443 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.cover

28
Makefile Normal file
View 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
View File

@ -0,0 +1,3 @@
module code.squareroundforest.org/arpio/times
go 1.25.0

43
lib.go Normal file
View 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
View 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
View 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.

0
readme.md Normal file
View File

13
sys.go Normal file
View 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
View 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)
}