detached 0db93ca

refactor(ports): add Clock port for timers owned by goroutines

Owner loops get timers from ports.Clock instead of the time package, so
tests fire them instead of sleeping. Scale persistence, the DRM output
apply deadline and wayland idle notifications use it.
commit
0db93ca66198d5cf2208e7fa740a2f813549e0d0
author
brice · · 2026-09-28 11:54 UTC
committer
brice · · 2026-09-28 14:12 UTC
parent
355ab73
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +737 -47
M .mockery.yml +3 -0
@@ -13,10 +13,13 @@ packages:
1313 pkgname: portsmocks
1414 filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go'
1515 interfaces:
16+ Clock:
1617 Notifier:
1718 OutputScaleStore:
1819 Renderer:
1920 Seat:
21+ Ticker:
22+ Timer:
2023 # Unexported adapter seams: mocks live next to the code, in test files.
2124 github.com/bnema/neferwl/internal/adapters/drm:
2225 interfaces:
@@ -13,10 +13,13 @@ packages:
13 pkgname: portsmocks13 pkgname: portsmocks
14 filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go'14 filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go'
15 interfaces:15 interfaces:
16+ Clock:
16 Notifier:17 Notifier:
17 OutputScaleStore:18 OutputScaleStore:
18 Renderer:19 Renderer:
19 Seat:20 Seat:
21+ Ticker:
22+ Timer:
20 # Unexported adapter seams: mocks live next to the code, in test files.23 # Unexported adapter seams: mocks live next to the code, in test files.
21 github.com/bnema/neferwl/internal/adapters/drm:24 github.com/bnema/neferwl/internal/adapters/drm:
22 interfaces:25 interfaces:
A internal/adapters/clock/clock.go +32 -0
@@ -0,0 +1,32 @@
1+// Package clock is the wall clock behind ports.Clock.
2+package clock
3+
4+import (
5+ "time"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// System is the real clock.
11+type System struct{}
12+
13+func (System) Now() time.Time { return time.Now() }
14+
15+func (System) NewTimer(d time.Duration) ports.Timer { return timer{time.NewTimer(d)} }
16+
17+func (System) NewTicker(d time.Duration) ports.Ticker { return ticker{time.NewTicker(d)} }
18+
19+func (System) AfterFunc(d time.Duration, f func()) ports.Timer {
20+ return timer{time.AfterFunc(d, f)}
21+}
22+
23+type timer struct{ t *time.Timer }
24+
25+func (t timer) C() <-chan time.Time { return t.t.C }
26+func (t timer) Stop() bool { return t.t.Stop() }
27+func (t timer) Reset(d time.Duration) bool { return t.t.Reset(d) }
28+
29+type ticker struct{ t *time.Ticker }
30+
31+func (t ticker) C() <-chan time.Time { return t.t.C }
32+func (t ticker) Stop() { t.t.Stop() }
@@ -0,0 +1,32 @@
1+// Package clock is the wall clock behind ports.Clock.
2+package clock
3+
4+import (
5+ "time"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// System is the real clock.
11+type System struct{}
12+
13+func (System) Now() time.Time { return time.Now() }
14+
15+func (System) NewTimer(d time.Duration) ports.Timer { return timer{time.NewTimer(d)} }
16+
17+func (System) NewTicker(d time.Duration) ports.Ticker { return ticker{time.NewTicker(d)} }
18+
19+func (System) AfterFunc(d time.Duration, f func()) ports.Timer {
20+ return timer{time.AfterFunc(d, f)}
21+}
22+
23+type timer struct{ t *time.Timer }
24+
25+func (t timer) C() <-chan time.Time { return t.t.C }
26+func (t timer) Stop() bool { return t.t.Stop() }
27+func (t timer) Reset(d time.Duration) bool { return t.t.Reset(d) }
28+
29+type ticker struct{ t *time.Ticker }
30+
31+func (t ticker) C() <-chan time.Time { return t.t.C }
32+func (t ticker) Stop() { t.t.Stop() }
M internal/adapters/wayland/idle.go +2 -2
@@ -22,7 +22,7 @@ type idleNotification struct {
2222 timeout time.Duration
2323 input bool // get_input_idle_notification: ignores idle inhibitors
2424 idle bool // idled sent, resumed not yet
25- timer *time.Timer
25+ timer ports.Timer
2626 gen uint64 // bumped on stop: a firing timer of an older gen is stale
2727 }
2828
@@ -93,7 +93,7 @@ func (s *Server) armIdle(n *idleNotification) {
9393 gen := n.gen
9494 // Core reports activity at most once per ActivityInterval: a shorter
9595 // timeout would fire while the user is still typing.
96- n.timer = time.AfterFunc(max(n.timeout, ports.ActivityInterval), func() {
96+ n.timer = s.clock.AfterFunc(max(n.timeout, ports.ActivityInterval), func() {
9797 s.display.Do(func() {
9898 // A timer stopped or replaced while it waited for Do is stale.
9999 if n.gen != gen || !n.res.Resource.Alive() || n.idle || s.idleHeld(n) {
@@ -22,7 +22,7 @@ type idleNotification struct {
22 timeout time.Duration22 timeout time.Duration
23 input bool // get_input_idle_notification: ignores idle inhibitors23 input bool // get_input_idle_notification: ignores idle inhibitors
24 idle bool // idled sent, resumed not yet24 idle bool // idled sent, resumed not yet
25- timer *time.Timer25+ timer ports.Timer
26 gen uint64 // bumped on stop: a firing timer of an older gen is stale26 gen uint64 // bumped on stop: a firing timer of an older gen is stale
27 }27 }
28 28
@@ -93,7 +93,7 @@ func (s *Server) armIdle(n *idleNotification) {
93 gen := n.gen93 gen := n.gen
94 // Core reports activity at most once per ActivityInterval: a shorter94 // Core reports activity at most once per ActivityInterval: a shorter
95 // timeout would fire while the user is still typing.95 // timeout would fire while the user is still typing.
96- n.timer = time.AfterFunc(max(n.timeout, ports.ActivityInterval), func() {96+ n.timer = s.clock.AfterFunc(max(n.timeout, ports.ActivityInterval), func() {
97 s.display.Do(func() {97 s.display.Do(func() {
98 // A timer stopped or replaced while it waited for Do is stale.98 // A timer stopped or replaced while it waited for Do is stale.
99 if n.gen != gen || !n.res.Resource.Alive() || n.idle || s.idleHeld(n) {99 if n.gen != gen || !n.res.Resource.Alive() || n.idle || s.idleHeld(n) {
M internal/adapters/wayland/idle_test.go +67 -11
@@ -1,14 +1,18 @@
11 package wayland
22
33 import (
4+ "context"
45 "testing"
56 "time"
67
8+ "github.com/bnema/neferwl/internal/logging"
9+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
710 "github.com/bnema/neferwl/internal/ports"
811 "github.com/bnema/purego-libwayland/protocol/extidlenotify"
912 "github.com/bnema/purego-libwayland/protocol/idleinhibit"
1013 "github.com/bnema/purego-libwayland/protocol/wlroutputpower"
1114 "github.com/bnema/wlturbo"
15+ "github.com/stretchr/testify/mock"
1216 )
1317
1418 // eventProxy records the opcodes, and first uint argument, of its events.
@@ -169,28 +173,80 @@ func TestOutputPower(t *testing.T) {
169173 }
170174 }
171175
176+// armed is one idle timer started through the clock.
177+type armed struct {
178+ d time.Duration
179+ fire func()
180+}
181+
182+// idleClockServer runs a server whose idle timers fire only when the test
183+// calls them; every started timer is sent on the returned channel.
184+func idleClockServer(t *testing.T) (*Server, chan ports.ClientCommand, string, chan armed) {
185+ t.Helper()
186+ dir := t.TempDir()
187+ commands := make(chan ports.ClientCommand, 16)
188+ timers := make(chan armed, 16)
189+ clock := portsmocks.NewMockClock(t)
190+ clock.EXPECT().AfterFunc(mock.Anything, mock.Anything).RunAndReturn(func(d time.Duration, f func()) ports.Timer {
191+ timer := portsmocks.NewMockTimer(t)
192+ timer.EXPECT().Stop().Return(true).Maybe()
193+ timers <- armed{d, f}
194+ return timer
195+ }).Maybe()
196+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, Clock: clock}, Channels{Commands: commands}, logging.For(context.Background(), "wayland"))
197+ if err != nil {
198+ t.Fatal(err)
199+ }
200+ ctx, cancel := context.WithCancel(context.Background())
201+ done := make(chan error, 1)
202+ go func() { done <- s.Run(ctx) }()
203+ t.Cleanup(func() { cancel(); <-done })
204+ return s, commands, dir, timers
205+}
206+
207+func nextArmed(t *testing.T, timers <-chan armed) armed {
208+ t.Helper()
209+ select {
210+ case a := <-timers:
211+ return a
212+ case <-time.After(2 * time.Second):
213+ t.Fatal("no idle timer started")
214+ return armed{}
215+ }
216+}
217+
172218 // A timeout shorter than the activity interval still waits for it, so
173-// continuous input keeps the notification from idling.
219+// continuous input keeps the notification from idling; a timer replaced
220+// by user activity is stale when it fires.
174221 func TestIdleShortTimeout(t *testing.T) {
175- s, _, commands, _, dir := contentServer(t)
222+ s, commands, dir, timers := idleClockServer(t)
176223 c := protocolClient(t, s, dir)
177224 seat := bindProtocol(t, c, "wl_seat")
178225 registerProtocol(t, c, seat)
179226 notifier := bindVersion(t, c, "ext_idle_notifier_v1", 2)
180227 id, n := newEventProxy(c)
181- start := time.Now()
182228 requestProtocol(t, c, notifier, extidlenotify.ExtIdleNotifierV1RequestGetIdleNotification, id, uint32(0), seat)
183- n.next(t, c, 2*time.Second)
184- if d := time.Since(start); d < ports.ActivityInterval {
185- t.Fatalf("idled after %v, before the activity interval", d)
229+ first := nextArmed(t, timers)
230+ if first.d != ports.ActivityInterval {
231+ t.Fatalf("timer %v, want the activity interval", first.d)
232+ }
233+ first.fire()
234+ if e := n.next(t, c, 2*time.Second); e[0] != uint32(extidlenotify.ExtIdleNotificationV1EventIdled) {
235+ t.Fatalf("event %v, want idled", e)
186236 }
187237 commands <- ports.UserActivity{}
188- n.next(t, c, 2*time.Second) // resumed
189- for range 4 {
190- time.Sleep(ports.ActivityInterval / 2)
191- commands <- ports.UserActivity{}
238+ if e := n.next(t, c, 2*time.Second); e[0] != uint32(extidlenotify.ExtIdleNotificationV1EventResumed) {
239+ t.Fatalf("event %v, want resumed", e)
240+ }
241+ stale := nextArmed(t, timers)
242+ commands <- ports.UserActivity{}
243+ current := nextArmed(t, timers)
244+ stale.fire()
245+ n.none(t, c, 50*time.Millisecond)
246+ current.fire()
247+ if e := n.next(t, c, 2*time.Second); e[0] != uint32(extidlenotify.ExtIdleNotificationV1EventIdled) {
248+ t.Fatalf("event %v, want idled", e)
192249 }
193- n.none(t, c, ports.ActivityInterval/2)
194250 }
195251
196252 // An unknown mode is a protocol error.
@@ -1,14 +1,18 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "context"
4 "testing"5 "testing"
5 "time"6 "time"
6 7
8+ "github.com/bnema/neferwl/internal/logging"
9+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
7 "github.com/bnema/neferwl/internal/ports"10 "github.com/bnema/neferwl/internal/ports"
8 "github.com/bnema/purego-libwayland/protocol/extidlenotify"11 "github.com/bnema/purego-libwayland/protocol/extidlenotify"
9 "github.com/bnema/purego-libwayland/protocol/idleinhibit"12 "github.com/bnema/purego-libwayland/protocol/idleinhibit"
10 "github.com/bnema/purego-libwayland/protocol/wlroutputpower"13 "github.com/bnema/purego-libwayland/protocol/wlroutputpower"
11 "github.com/bnema/wlturbo"14 "github.com/bnema/wlturbo"
15+ "github.com/stretchr/testify/mock"
12 )16 )
13 17
14 // eventProxy records the opcodes, and first uint argument, of its events.18 // eventProxy records the opcodes, and first uint argument, of its events.
@@ -169,28 +173,80 @@ func TestOutputPower(t *testing.T) {
169 }173 }
170 }174 }
171 175
176+// armed is one idle timer started through the clock.
177+type armed struct {
178+ d time.Duration
179+ fire func()
180+}
181+
182+// idleClockServer runs a server whose idle timers fire only when the test
183+// calls them; every started timer is sent on the returned channel.
184+func idleClockServer(t *testing.T) (*Server, chan ports.ClientCommand, string, chan armed) {
185+ t.Helper()
186+ dir := t.TempDir()
187+ commands := make(chan ports.ClientCommand, 16)
188+ timers := make(chan armed, 16)
189+ clock := portsmocks.NewMockClock(t)
190+ clock.EXPECT().AfterFunc(mock.Anything, mock.Anything).RunAndReturn(func(d time.Duration, f func()) ports.Timer {
191+ timer := portsmocks.NewMockTimer(t)
192+ timer.EXPECT().Stop().Return(true).Maybe()
193+ timers <- armed{d, f}
194+ return timer
195+ }).Maybe()
196+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, Clock: clock}, Channels{Commands: commands}, logging.For(context.Background(), "wayland"))
197+ if err != nil {
198+ t.Fatal(err)
199+ }
200+ ctx, cancel := context.WithCancel(context.Background())
201+ done := make(chan error, 1)
202+ go func() { done <- s.Run(ctx) }()
203+ t.Cleanup(func() { cancel(); <-done })
204+ return s, commands, dir, timers
205+}
206+
207+func nextArmed(t *testing.T, timers <-chan armed) armed {
208+ t.Helper()
209+ select {
210+ case a := <-timers:
211+ return a
212+ case <-time.After(2 * time.Second):
213+ t.Fatal("no idle timer started")
214+ return armed{}
215+ }
216+}
217+
172 // A timeout shorter than the activity interval still waits for it, so218 // A timeout shorter than the activity interval still waits for it, so
173-// continuous input keeps the notification from idling.219+// continuous input keeps the notification from idling; a timer replaced
220+// by user activity is stale when it fires.
174 func TestIdleShortTimeout(t *testing.T) {221 func TestIdleShortTimeout(t *testing.T) {
175- s, _, commands, _, dir := contentServer(t)222+ s, commands, dir, timers := idleClockServer(t)
176 c := protocolClient(t, s, dir)223 c := protocolClient(t, s, dir)
177 seat := bindProtocol(t, c, "wl_seat")224 seat := bindProtocol(t, c, "wl_seat")
178 registerProtocol(t, c, seat)225 registerProtocol(t, c, seat)
179 notifier := bindVersion(t, c, "ext_idle_notifier_v1", 2)226 notifier := bindVersion(t, c, "ext_idle_notifier_v1", 2)
180 id, n := newEventProxy(c)227 id, n := newEventProxy(c)
181- start := time.Now()
182 requestProtocol(t, c, notifier, extidlenotify.ExtIdleNotifierV1RequestGetIdleNotification, id, uint32(0), seat)228 requestProtocol(t, c, notifier, extidlenotify.ExtIdleNotifierV1RequestGetIdleNotification, id, uint32(0), seat)
183- n.next(t, c, 2*time.Second)229+ first := nextArmed(t, timers)
184- if d := time.Since(start); d < ports.ActivityInterval {230+ if first.d != ports.ActivityInterval {
185- t.Fatalf("idled after %v, before the activity interval", d)231+ t.Fatalf("timer %v, want the activity interval", first.d)
232+ }
233+ first.fire()
234+ if e := n.next(t, c, 2*time.Second); e[0] != uint32(extidlenotify.ExtIdleNotificationV1EventIdled) {
235+ t.Fatalf("event %v, want idled", e)
186 }236 }
187 commands <- ports.UserActivity{}237 commands <- ports.UserActivity{}
188- n.next(t, c, 2*time.Second) // resumed238+ if e := n.next(t, c, 2*time.Second); e[0] != uint32(extidlenotify.ExtIdleNotificationV1EventResumed) {
189- for range 4 {239+ t.Fatalf("event %v, want resumed", e)
190- time.Sleep(ports.ActivityInterval / 2)240+ }
191- commands <- ports.UserActivity{}241+ stale := nextArmed(t, timers)
242+ commands <- ports.UserActivity{}
243+ current := nextArmed(t, timers)
244+ stale.fire()
245+ n.none(t, c, 50*time.Millisecond)
246+ current.fire()
247+ if e := n.next(t, c, 2*time.Second); e[0] != uint32(extidlenotify.ExtIdleNotificationV1EventIdled) {
248+ t.Fatalf("event %v, want idled", e)
192 }249 }
193- n.none(t, c, ports.ActivityInterval/2)
194 }250 }
195 251
196 // An unknown mode is a protocol error.252 // An unknown mode is a protocol error.
M internal/adapters/wayland/server.go +8 -0
@@ -9,6 +9,7 @@ import (
99 "sync"
1010 "time"
1111
12+ "github.com/bnema/neferwl/internal/adapters/clock"
1213 "github.com/bnema/neferwl/internal/ports"
1314 "github.com/bnema/purego-libwayland/protocol/fractionalscale"
1415 "github.com/bnema/purego-libwayland/protocol/wayland"
@@ -30,6 +31,8 @@ type Options struct {
3031 SyncobjNode string
3132 Keymap string
3233 RepeatRate, RepeatDelay int
34+ // Clock runs idle notification timers; nil is the system clock.
35+ Clock ports.Clock
3336 // syncDev replaces the render node's syncobj interface (tests).
3437 syncDev syncobjDevice
3538 }
@@ -161,6 +164,7 @@ type Server struct {
161164 // idleNotes and powers are the idle notifications and output power
162165 // objects (idle.go); outputsOff the outputs core turned off.
163166 idleNotes []*idleNotification
167+ clock ports.Clock
164168 powers []*outputPower
165169 outputsOff map[string]bool
166170 contentNotify chan struct{}
@@ -233,6 +237,10 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
233237 return nil, err
234238 }
235239 s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
240+ s.clock = opts.Clock
241+ if s.clock == nil {
242+ s.clock = clock.System{}
243+ }
236244 s.captureReplies = map[uint64]func(ports.CaptureDone){}
237245 s.outputReplies = map[uint64]*outputConfiguration{}
238246 s.managementSerial = 1
@@ -9,6 +9,7 @@ import (
9 "sync"9 "sync"
10 "time"10 "time"
11 11
12+ "github.com/bnema/neferwl/internal/adapters/clock"
12 "github.com/bnema/neferwl/internal/ports"13 "github.com/bnema/neferwl/internal/ports"
13 "github.com/bnema/purego-libwayland/protocol/fractionalscale"14 "github.com/bnema/purego-libwayland/protocol/fractionalscale"
14 "github.com/bnema/purego-libwayland/protocol/wayland"15 "github.com/bnema/purego-libwayland/protocol/wayland"
@@ -30,6 +31,8 @@ type Options struct {
30 SyncobjNode string31 SyncobjNode string
31 Keymap string32 Keymap string
32 RepeatRate, RepeatDelay int33 RepeatRate, RepeatDelay int
34+ // Clock runs idle notification timers; nil is the system clock.
35+ Clock ports.Clock
33 // syncDev replaces the render node's syncobj interface (tests).36 // syncDev replaces the render node's syncobj interface (tests).
34 syncDev syncobjDevice37 syncDev syncobjDevice
35 }38 }
@@ -161,6 +164,7 @@ type Server struct {
161 // idleNotes and powers are the idle notifications and output power164 // idleNotes and powers are the idle notifications and output power
162 // objects (idle.go); outputsOff the outputs core turned off.165 // objects (idle.go); outputsOff the outputs core turned off.
163 idleNotes []*idleNotification166 idleNotes []*idleNotification
167+ clock ports.Clock
164 powers []*outputPower168 powers []*outputPower
165 outputsOff map[string]bool169 outputsOff map[string]bool
166 contentNotify chan struct{}170 contentNotify chan struct{}
@@ -233,6 +237,10 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
233 return nil, err237 return nil, err
234 }238 }
235 s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}239 s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
240+ s.clock = opts.Clock
241+ if s.clock == nil {
242+ s.clock = clock.System{}
243+ }
236 s.captureReplies = map[uint64]func(ports.CaptureDone){}244 s.captureReplies = map[uint64]func(ports.CaptureDone){}
237 s.outputReplies = map[uint64]*outputConfiguration{}245 s.outputReplies = map[uint64]*outputConfiguration{}
238 s.managementSerial = 1246 s.managementSerial = 1
M internal/app/app.go +2 -1
@@ -11,6 +11,7 @@ import (
1111 "sync"
1212 "time"
1313
14+ "github.com/bnema/neferwl/internal/adapters/clock"
1415 "github.com/bnema/neferwl/internal/adapters/config"
1516 "github.com/bnema/neferwl/internal/adapters/drm"
1617 "github.com/bnema/neferwl/internal/adapters/headlessinput"
@@ -161,7 +162,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
161162 workers.Add(1)
162163 go func() {
163164 defer workers.Done()
164- core.PersistScales(ctx, scales, config.ScaleStore{Path: path}, launcher.NewNotifier(ctx, childEnv, logging.For(ctx, "launcher")), time.Second)
165+ core.PersistScales(ctx, scales, config.ScaleStore{Path: path}, launcher.NewNotifier(ctx, childEnv, logging.For(ctx, "launcher")), clock.System{}, time.Second)
165166 }()
166167 }
167168 filtered := make(chan ports.ConfigChanged, 8)
@@ -11,6 +11,7 @@ import (
11 "sync"11 "sync"
12 "time"12 "time"
13 13
14+ "github.com/bnema/neferwl/internal/adapters/clock"
14 "github.com/bnema/neferwl/internal/adapters/config"15 "github.com/bnema/neferwl/internal/adapters/config"
15 "github.com/bnema/neferwl/internal/adapters/drm"16 "github.com/bnema/neferwl/internal/adapters/drm"
16 "github.com/bnema/neferwl/internal/adapters/headlessinput"17 "github.com/bnema/neferwl/internal/adapters/headlessinput"
@@ -161,7 +162,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
161 workers.Add(1)162 workers.Add(1)
162 go func() {163 go func() {
163 defer workers.Done()164 defer workers.Done()
164- core.PersistScales(ctx, scales, config.ScaleStore{Path: path}, launcher.NewNotifier(ctx, childEnv, logging.For(ctx, "launcher")), time.Second)165+ core.PersistScales(ctx, scales, config.ScaleStore{Path: path}, launcher.NewNotifier(ctx, childEnv, logging.For(ctx, "launcher")), clock.System{}, time.Second)
165 }()166 }()
166 }167 }
167 filtered := make(chan ports.ConfigChanged, 8)168 filtered := make(chan ports.ConfigChanged, 8)
M internal/app/drm.go +19 -20
@@ -10,6 +10,7 @@ import (
1010 "sync"
1111 "time"
1212
13+ "github.com/bnema/neferwl/internal/adapters/clock"
1314 "github.com/bnema/neferwl/internal/adapters/config"
1415 "github.com/bnema/neferwl/internal/adapters/drm"
1516 "github.com/bnema/neferwl/internal/adapters/seat"
@@ -20,10 +21,14 @@ import (
2021
2122 var errTimeout = errors.New("timeout")
2223
24+// applyTimeout bounds how long outputs take to apply a configuration.
25+const applyTimeout = 5 * time.Second
26+
2327 // drmBackend is the seat and the cards with at least one usable output.
2428 type drmBackend struct {
2529 seat *seat.Seat
2630 cards []*drmCard
31+ clock ports.Clock
2732 }
2833
2934 type drmCard struct {
@@ -73,7 +78,7 @@ func openDRM(ctx context.Context, cfg ports.Config) (*drmBackend, error) {
7378 want := wantFromConfig(cfg)
7479 paths, _ := filepath.Glob("/dev/dri/card[0-9]*")
7580 sort.Strings(paths)
76- b := &drmBackend{seat: s}
81+ b := &drmBackend{seat: s, clock: clock.System{}}
7782 var errs []error
7883 for _, path := range paths {
7984 fd, err := s.OpenDevice(path)
@@ -180,17 +185,18 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
180185 err error
181186 }
182187 ready := make(chan readyResult)
183- deadline := make(chan uint64, 1)
184- var timer *time.Timer
188+ // deadline is the pending apply's timer channel, nil while none is pending.
189+ timer := b.clock.NewTimer(applyTimeout)
190+ timer.Stop()
191+ defer timer.Stop()
192+ var deadline <-chan time.Time
185193 var op uint64
186194 complete := func(d applyDecision) {
187195 if !d.reply {
188196 return
189197 }
190- if timer != nil {
191- timer.Stop()
192- timer = nil
193- }
198+ timer.Stop()
199+ deadline = nil
194200 apply.finished(d.err)
195201 }
196202 publishInventory := func() {
@@ -291,10 +297,8 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
291297 configure := func(cfg ports.Config) {
292298 currentConfig = cfg
293299 op++
294- if timer != nil {
295- timer.Stop()
296- timer = nil
297- }
300+ timer.Stop()
301+ deadline = nil
298302 required := map[string]bool{}
299303 // Desired enabled heads, not the inventory's current mode, require a first modeset.
300304 w := want(currentConfig)
@@ -316,13 +320,8 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
316320 }
317321 d := progress.scanned(running)
318322 if progress.active {
319- id := op
320- timer = time.AfterFunc(5*time.Second, func() {
321- select {
322- case deadline <- id:
323- case <-ctx.Done():
324- }
325- })
323+ timer.Reset(applyTimeout)
324+ deadline = timer.C()
326325 }
327326 complete(d)
328327 }
@@ -362,8 +361,8 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
362361 apply.headsSent()
363362 case result := <-ready:
364363 complete(progress.readyEvent(result.name, result.source, readySources[result.name], result.err))
365- case id := <-deadline:
366- complete(progress.timeout(id))
364+ case <-deadline:
365+ complete(progress.timeout(op))
367366 case s := <-ch.scenes:
368367 set.scenes(s)
369368 case c := <-ch.contents:
@@ -10,6 +10,7 @@ import (
10 "sync"10 "sync"
11 "time"11 "time"
12 12
13+ "github.com/bnema/neferwl/internal/adapters/clock"
13 "github.com/bnema/neferwl/internal/adapters/config"14 "github.com/bnema/neferwl/internal/adapters/config"
14 "github.com/bnema/neferwl/internal/adapters/drm"15 "github.com/bnema/neferwl/internal/adapters/drm"
15 "github.com/bnema/neferwl/internal/adapters/seat"16 "github.com/bnema/neferwl/internal/adapters/seat"
@@ -20,10 +21,14 @@ import (
20 21
21 var errTimeout = errors.New("timeout")22 var errTimeout = errors.New("timeout")
22 23
24+// applyTimeout bounds how long outputs take to apply a configuration.
25+const applyTimeout = 5 * time.Second
26+
23 // drmBackend is the seat and the cards with at least one usable output.27 // drmBackend is the seat and the cards with at least one usable output.
24 type drmBackend struct {28 type drmBackend struct {
25 seat *seat.Seat29 seat *seat.Seat
26 cards []*drmCard30 cards []*drmCard
31+ clock ports.Clock
27 }32 }
28 33
29 type drmCard struct {34 type drmCard struct {
@@ -73,7 +78,7 @@ func openDRM(ctx context.Context, cfg ports.Config) (*drmBackend, error) {
73 want := wantFromConfig(cfg)78 want := wantFromConfig(cfg)
74 paths, _ := filepath.Glob("/dev/dri/card[0-9]*")79 paths, _ := filepath.Glob("/dev/dri/card[0-9]*")
75 sort.Strings(paths)80 sort.Strings(paths)
76- b := &drmBackend{seat: s}81+ b := &drmBackend{seat: s, clock: clock.System{}}
77 var errs []error82 var errs []error
78 for _, path := range paths {83 for _, path := range paths {
79 fd, err := s.OpenDevice(path)84 fd, err := s.OpenDevice(path)
@@ -180,17 +185,18 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
180 err error185 err error
181 }186 }
182 ready := make(chan readyResult)187 ready := make(chan readyResult)
183- deadline := make(chan uint64, 1)188+ // deadline is the pending apply's timer channel, nil while none is pending.
184- var timer *time.Timer189+ timer := b.clock.NewTimer(applyTimeout)
190+ timer.Stop()
191+ defer timer.Stop()
192+ var deadline <-chan time.Time
185 var op uint64193 var op uint64
186 complete := func(d applyDecision) {194 complete := func(d applyDecision) {
187 if !d.reply {195 if !d.reply {
188 return196 return
189 }197 }
190- if timer != nil {198+ timer.Stop()
191- timer.Stop()199+ deadline = nil
192- timer = nil
193- }
194 apply.finished(d.err)200 apply.finished(d.err)
195 }201 }
196 publishInventory := func() {202 publishInventory := func() {
@@ -291,10 +297,8 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
291 configure := func(cfg ports.Config) {297 configure := func(cfg ports.Config) {
292 currentConfig = cfg298 currentConfig = cfg
293 op++299 op++
294- if timer != nil {300+ timer.Stop()
295- timer.Stop()301+ deadline = nil
296- timer = nil
297- }
298 required := map[string]bool{}302 required := map[string]bool{}
299 // Desired enabled heads, not the inventory's current mode, require a first modeset.303 // Desired enabled heads, not the inventory's current mode, require a first modeset.
300 w := want(currentConfig)304 w := want(currentConfig)
@@ -316,13 +320,8 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
316 }320 }
317 d := progress.scanned(running)321 d := progress.scanned(running)
318 if progress.active {322 if progress.active {
319- id := op323+ timer.Reset(applyTimeout)
320- timer = time.AfterFunc(5*time.Second, func() {324+ deadline = timer.C()
321- select {
322- case deadline <- id:
323- case <-ctx.Done():
324- }
325- })
326 }325 }
327 complete(d)326 complete(d)
328 }327 }
@@ -362,8 +361,8 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm
362 apply.headsSent()361 apply.headsSent()
363 case result := <-ready:362 case result := <-ready:
364 complete(progress.readyEvent(result.name, result.source, readySources[result.name], result.err))363 complete(progress.readyEvent(result.name, result.source, readySources[result.name], result.err))
365- case id := <-deadline:364+ case <-deadline:
366- complete(progress.timeout(id))365+ complete(progress.timeout(op))
367 case s := <-ch.scenes:366 case s := <-ch.scenes:
368 set.scenes(s)367 set.scenes(s)
369 case c := <-ch.contents:368 case c := <-ch.contents:
M internal/core/persist.go +3 -3
@@ -14,9 +14,9 @@ import (
1414 // debounce, so a burst of Cmd+= presses writes the config once, and tells
1515 // the user through notes. It runs until ctx ends; pending scales are then
1616 // dropped.
17-func PersistScales(ctx context.Context, changes <-chan ports.ScaleChanged, store ports.OutputScaleStore, notes ports.Notifier, debounce time.Duration) {
17+func PersistScales(ctx context.Context, changes <-chan ports.ScaleChanged, store ports.OutputScaleStore, notes ports.Notifier, clock ports.Clock, debounce time.Duration) {
1818 pending := map[string]float64{}
19- timer := time.NewTimer(debounce)
19+ timer := clock.NewTimer(debounce)
2020 timer.Stop()
2121 for {
2222 select {
@@ -26,7 +26,7 @@ func PersistScales(ctx context.Context, changes <-chan ports.ScaleChanged, store
2626 case ev := <-changes:
2727 pending[ev.Output] = ev.Scale
2828 timer.Reset(debounce)
29- case <-timer.C:
29+ case <-timer.C():
3030 // Sorted: several outputs save and notify in a stable order.
3131 for _, name := range slices.Sorted(maps.Keys(pending)) {
3232 scale := pending[name]
@@ -14,9 +14,9 @@ import (
14 // debounce, so a burst of Cmd+= presses writes the config once, and tells14 // debounce, so a burst of Cmd+= presses writes the config once, and tells
15 // the user through notes. It runs until ctx ends; pending scales are then15 // the user through notes. It runs until ctx ends; pending scales are then
16 // dropped.16 // dropped.
17-func PersistScales(ctx context.Context, changes <-chan ports.ScaleChanged, store ports.OutputScaleStore, notes ports.Notifier, debounce time.Duration) {17+func PersistScales(ctx context.Context, changes <-chan ports.ScaleChanged, store ports.OutputScaleStore, notes ports.Notifier, clock ports.Clock, debounce time.Duration) {
18 pending := map[string]float64{}18 pending := map[string]float64{}
19- timer := time.NewTimer(debounce)19+ timer := clock.NewTimer(debounce)
20 timer.Stop()20 timer.Stop()
21 for {21 for {
22 select {22 select {
@@ -26,7 +26,7 @@ func PersistScales(ctx context.Context, changes <-chan ports.ScaleChanged, store
26 case ev := <-changes:26 case ev := <-changes:
27 pending[ev.Output] = ev.Scale27 pending[ev.Output] = ev.Scale
28 timer.Reset(debounce)28 timer.Reset(debounce)
29- case <-timer.C:29+ case <-timer.C():
30 // Sorted: several outputs save and notify in a stable order.30 // Sorted: several outputs save and notify in a stable order.
31 for _, name := range slices.Sorted(maps.Keys(pending)) {31 for _, name := range slices.Sorted(maps.Keys(pending)) {
32 scale := pending[name]32 scale := pending[name]
M internal/core/persist_test.go +25 -10
@@ -12,24 +12,39 @@ import (
1212 "github.com/bnema/neferwl/internal/ports"
1313 )
1414
15+// persistClock returns a clock whose debounce timer fires when the test
16+// sends on the returned channel. Each change restarts the timer.
17+func persistClock(t *testing.T, debounce time.Duration, changes int) (*portsmocks.MockClock, chan time.Time) {
18+ t.Helper()
19+ fire := make(chan time.Time)
20+ timer := portsmocks.NewMockTimer(t)
21+ timer.EXPECT().Stop().Return(true)
22+ timer.EXPECT().Reset(debounce).Return(false).Times(changes)
23+ timer.EXPECT().C().Return(fire)
24+ clock := portsmocks.NewMockClock(t)
25+ clock.EXPECT().NewTimer(debounce).Return(timer).Once()
26+ return clock, fire
27+}
28+
1529 // A burst of scale changes saves only the last scale of each output, once.
1630 func TestPersistScalesDebounces(t *testing.T) {
1731 store := portsmocks.NewMockOutputScaleStore(t)
1832 notes := portsmocks.NewMockNotifier(t)
19- done := make(chan struct{}, 2)
2033 store.EXPECT().SaveOutputScale("DP-1", 1.25).Return(nil).Once()
2134 store.EXPECT().SaveOutputScale("DP-2", 2.0).Return(nil).Once()
22- notes.EXPECT().Notify("DP-1 scale saved", "Scale 1.25 is kept after restart").Run(func(string, string) { done <- struct{}{} }).Once()
23- notes.EXPECT().Notify("DP-2 scale saved", "Scale 2 is kept after restart").Run(func(string, string) { done <- struct{}{} }).Once()
35+ notes.EXPECT().Notify("DP-1 scale saved", "Scale 1.25 is kept after restart").Once()
36+ notes.EXPECT().Notify("DP-2 scale saved", "Scale 2 is kept after restart").Once()
37+ clock, fire := persistClock(t, time.Second, 4)
2438 changes := make(chan ports.ScaleChanged)
2539 ctx, cancel := context.WithCancel(context.Background())
2640 stopped := make(chan struct{})
27- go func() { core.PersistScales(ctx, changes, store, notes, 20*time.Millisecond); close(stopped) }()
41+ go func() { core.PersistScales(ctx, changes, store, notes, clock, time.Second); close(stopped) }()
2842 for _, ev := range []ports.ScaleChanged{{Output: "DP-2", Scale: 1.5}, {Output: "DP-1", Scale: 1.25}, {Output: "DP-2", Scale: 2}} {
2943 changes <- ev
3044 }
31- receive(t, done)
32- receive(t, done)
45+ fire <- time.Time{}
46+ // The next change is received only after the fired batch is saved.
47+ changes <- ports.ScaleChanged{Output: "DP-1", Scale: 1.25}
3348 cancel()
3449 <-stopped
3550 }
@@ -38,15 +53,15 @@ func TestPersistScalesDebounces(t *testing.T) {
3853 func TestPersistScalesReportsFailure(t *testing.T) {
3954 store := portsmocks.NewMockOutputScaleStore(t)
4055 notes := portsmocks.NewMockNotifier(t)
41- done := make(chan struct{}, 1)
4256 store.EXPECT().SaveOutputScale("DP-1", 1.5).Return(errors.New("read-only file system")).Once()
43- notes.EXPECT().Notify("DP-1 scale not saved", "read-only file system").Run(func(string, string) { done <- struct{}{} }).Once()
57+ notes.EXPECT().Notify("DP-1 scale not saved", "read-only file system").Once()
58+ clock, fire := persistClock(t, time.Second, 1)
4459 changes := make(chan ports.ScaleChanged)
4560 ctx, cancel := context.WithCancel(context.Background())
4661 stopped := make(chan struct{})
47- go func() { core.PersistScales(ctx, changes, store, notes, time.Millisecond); close(stopped) }()
62+ go func() { core.PersistScales(ctx, changes, store, notes, clock, time.Second); close(stopped) }()
4863 changes <- ports.ScaleChanged{Output: "DP-1", Scale: 1.5}
49- receive(t, done)
64+ fire <- time.Time{}
5065 cancel()
5166 <-stopped
5267 }
@@ -12,24 +12,39 @@ import (
12 "github.com/bnema/neferwl/internal/ports"12 "github.com/bnema/neferwl/internal/ports"
13 )13 )
14 14
15+// persistClock returns a clock whose debounce timer fires when the test
16+// sends on the returned channel. Each change restarts the timer.
17+func persistClock(t *testing.T, debounce time.Duration, changes int) (*portsmocks.MockClock, chan time.Time) {
18+ t.Helper()
19+ fire := make(chan time.Time)
20+ timer := portsmocks.NewMockTimer(t)
21+ timer.EXPECT().Stop().Return(true)
22+ timer.EXPECT().Reset(debounce).Return(false).Times(changes)
23+ timer.EXPECT().C().Return(fire)
24+ clock := portsmocks.NewMockClock(t)
25+ clock.EXPECT().NewTimer(debounce).Return(timer).Once()
26+ return clock, fire
27+}
28+
15 // A burst of scale changes saves only the last scale of each output, once.29 // A burst of scale changes saves only the last scale of each output, once.
16 func TestPersistScalesDebounces(t *testing.T) {30 func TestPersistScalesDebounces(t *testing.T) {
17 store := portsmocks.NewMockOutputScaleStore(t)31 store := portsmocks.NewMockOutputScaleStore(t)
18 notes := portsmocks.NewMockNotifier(t)32 notes := portsmocks.NewMockNotifier(t)
19- done := make(chan struct{}, 2)
20 store.EXPECT().SaveOutputScale("DP-1", 1.25).Return(nil).Once()33 store.EXPECT().SaveOutputScale("DP-1", 1.25).Return(nil).Once()
21 store.EXPECT().SaveOutputScale("DP-2", 2.0).Return(nil).Once()34 store.EXPECT().SaveOutputScale("DP-2", 2.0).Return(nil).Once()
22- notes.EXPECT().Notify("DP-1 scale saved", "Scale 1.25 is kept after restart").Run(func(string, string) { done <- struct{}{} }).Once()35+ notes.EXPECT().Notify("DP-1 scale saved", "Scale 1.25 is kept after restart").Once()
23- notes.EXPECT().Notify("DP-2 scale saved", "Scale 2 is kept after restart").Run(func(string, string) { done <- struct{}{} }).Once()36+ notes.EXPECT().Notify("DP-2 scale saved", "Scale 2 is kept after restart").Once()
37+ clock, fire := persistClock(t, time.Second, 4)
24 changes := make(chan ports.ScaleChanged)38 changes := make(chan ports.ScaleChanged)
25 ctx, cancel := context.WithCancel(context.Background())39 ctx, cancel := context.WithCancel(context.Background())
26 stopped := make(chan struct{})40 stopped := make(chan struct{})
27- go func() { core.PersistScales(ctx, changes, store, notes, 20*time.Millisecond); close(stopped) }()41+ go func() { core.PersistScales(ctx, changes, store, notes, clock, time.Second); close(stopped) }()
28 for _, ev := range []ports.ScaleChanged{{Output: "DP-2", Scale: 1.5}, {Output: "DP-1", Scale: 1.25}, {Output: "DP-2", Scale: 2}} {42 for _, ev := range []ports.ScaleChanged{{Output: "DP-2", Scale: 1.5}, {Output: "DP-1", Scale: 1.25}, {Output: "DP-2", Scale: 2}} {
29 changes <- ev43 changes <- ev
30 }44 }
31- receive(t, done)45+ fire <- time.Time{}
32- receive(t, done)46+ // The next change is received only after the fired batch is saved.
47+ changes <- ports.ScaleChanged{Output: "DP-1", Scale: 1.25}
33 cancel()48 cancel()
34 <-stopped49 <-stopped
35 }50 }
@@ -38,15 +53,15 @@ func TestPersistScalesDebounces(t *testing.T) {
38 func TestPersistScalesReportsFailure(t *testing.T) {53 func TestPersistScalesReportsFailure(t *testing.T) {
39 store := portsmocks.NewMockOutputScaleStore(t)54 store := portsmocks.NewMockOutputScaleStore(t)
40 notes := portsmocks.NewMockNotifier(t)55 notes := portsmocks.NewMockNotifier(t)
41- done := make(chan struct{}, 1)
42 store.EXPECT().SaveOutputScale("DP-1", 1.5).Return(errors.New("read-only file system")).Once()56 store.EXPECT().SaveOutputScale("DP-1", 1.5).Return(errors.New("read-only file system")).Once()
43- notes.EXPECT().Notify("DP-1 scale not saved", "read-only file system").Run(func(string, string) { done <- struct{}{} }).Once()57+ notes.EXPECT().Notify("DP-1 scale not saved", "read-only file system").Once()
58+ clock, fire := persistClock(t, time.Second, 1)
44 changes := make(chan ports.ScaleChanged)59 changes := make(chan ports.ScaleChanged)
45 ctx, cancel := context.WithCancel(context.Background())60 ctx, cancel := context.WithCancel(context.Background())
46 stopped := make(chan struct{})61 stopped := make(chan struct{})
47- go func() { core.PersistScales(ctx, changes, store, notes, time.Millisecond); close(stopped) }()62+ go func() { core.PersistScales(ctx, changes, store, notes, clock, time.Second); close(stopped) }()
48 changes <- ports.ScaleChanged{Output: "DP-1", Scale: 1.5}63 changes <- ports.ScaleChanged{Output: "DP-1", Scale: 1.5}
49- receive(t, done)64+ fire <- time.Time{}
50 cancel()65 cancel()
51 <-stopped66 <-stopped
52 }67 }
A internal/mocks/ports/clock_mock.go +549 -0
@@ -0,0 +1,549 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "time"
9+
10+ "github.com/bnema/neferwl/internal/ports"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockClock creates a new instance of MockClock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockClock(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockClock {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &MockClock{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// MockClock is an autogenerated mock type for the Clock type
38+type MockClock struct {
39+ mock.Mock
40+}
41+
42+type MockClock_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *MockClock) EXPECT() *MockClock_Expecter {
47+ return &MockClock_Expecter{mock: &_m.Mock}
48+}
49+
50+// AfterFunc provides a mock function for the type MockClock
51+func (_mock *MockClock) AfterFunc(d time.Duration, f func()) ports.Timer {
52+ ret := _mock.Called(d, f)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for AfterFunc")
56+ }
57+
58+ var r0 ports.Timer
59+ if returnFunc, ok := ret.Get(0).(func(time.Duration, func()) ports.Timer); ok {
60+ r0 = returnFunc(d, f)
61+ } else {
62+ if ret.Get(0) != nil {
63+ r0 = ret.Get(0).(ports.Timer)
64+ }
65+ }
66+ return r0
67+}
68+
69+// MockClock_AfterFunc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AfterFunc'
70+type MockClock_AfterFunc_Call struct {
71+ *mock.Call
72+}
73+
74+// AfterFunc is a helper method to define mock.On call
75+// - d time.Duration
76+// - f func()
77+func (_e *MockClock_Expecter) AfterFunc(d any, f any) *MockClock_AfterFunc_Call {
78+ return &MockClock_AfterFunc_Call{Call: _e.mock.On("AfterFunc", d, f)}
79+}
80+
81+func (_c *MockClock_AfterFunc_Call) Run(run func(d time.Duration, f func())) *MockClock_AfterFunc_Call {
82+ _c.Call.Run(func(args mock.Arguments) {
83+ var arg0 time.Duration
84+ if args[0] != nil {
85+ arg0 = args[0].(time.Duration)
86+ }
87+ var arg1 func()
88+ if args[1] != nil {
89+ arg1 = args[1].(func())
90+ }
91+ run(
92+ arg0,
93+ arg1,
94+ )
95+ })
96+ return _c
97+}
98+
99+func (_c *MockClock_AfterFunc_Call) Return(timer ports.Timer) *MockClock_AfterFunc_Call {
100+ _c.Call.Return(timer)
101+ return _c
102+}
103+
104+func (_c *MockClock_AfterFunc_Call) RunAndReturn(run func(d time.Duration, f func()) ports.Timer) *MockClock_AfterFunc_Call {
105+ _c.Call.Return(run)
106+ return _c
107+}
108+
109+// NewTicker provides a mock function for the type MockClock
110+func (_mock *MockClock) NewTicker(d time.Duration) ports.Ticker {
111+ ret := _mock.Called(d)
112+
113+ if len(ret) == 0 {
114+ panic("no return value specified for NewTicker")
115+ }
116+
117+ var r0 ports.Ticker
118+ if returnFunc, ok := ret.Get(0).(func(time.Duration) ports.Ticker); ok {
119+ r0 = returnFunc(d)
120+ } else {
121+ if ret.Get(0) != nil {
122+ r0 = ret.Get(0).(ports.Ticker)
123+ }
124+ }
125+ return r0
126+}
127+
128+// MockClock_NewTicker_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewTicker'
129+type MockClock_NewTicker_Call struct {
130+ *mock.Call
131+}
132+
133+// NewTicker is a helper method to define mock.On call
134+// - d time.Duration
135+func (_e *MockClock_Expecter) NewTicker(d any) *MockClock_NewTicker_Call {
136+ return &MockClock_NewTicker_Call{Call: _e.mock.On("NewTicker", d)}
137+}
138+
139+func (_c *MockClock_NewTicker_Call) Run(run func(d time.Duration)) *MockClock_NewTicker_Call {
140+ _c.Call.Run(func(args mock.Arguments) {
141+ var arg0 time.Duration
142+ if args[0] != nil {
143+ arg0 = args[0].(time.Duration)
144+ }
145+ run(
146+ arg0,
147+ )
148+ })
149+ return _c
150+}
151+
152+func (_c *MockClock_NewTicker_Call) Return(ticker ports.Ticker) *MockClock_NewTicker_Call {
153+ _c.Call.Return(ticker)
154+ return _c
155+}
156+
157+func (_c *MockClock_NewTicker_Call) RunAndReturn(run func(d time.Duration) ports.Ticker) *MockClock_NewTicker_Call {
158+ _c.Call.Return(run)
159+ return _c
160+}
161+
162+// NewTimer provides a mock function for the type MockClock
163+func (_mock *MockClock) NewTimer(d time.Duration) ports.Timer {
164+ ret := _mock.Called(d)
165+
166+ if len(ret) == 0 {
167+ panic("no return value specified for NewTimer")
168+ }
169+
170+ var r0 ports.Timer
171+ if returnFunc, ok := ret.Get(0).(func(time.Duration) ports.Timer); ok {
172+ r0 = returnFunc(d)
173+ } else {
174+ if ret.Get(0) != nil {
175+ r0 = ret.Get(0).(ports.Timer)
176+ }
177+ }
178+ return r0
179+}
180+
181+// MockClock_NewTimer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewTimer'
182+type MockClock_NewTimer_Call struct {
183+ *mock.Call
184+}
185+
186+// NewTimer is a helper method to define mock.On call
187+// - d time.Duration
188+func (_e *MockClock_Expecter) NewTimer(d any) *MockClock_NewTimer_Call {
189+ return &MockClock_NewTimer_Call{Call: _e.mock.On("NewTimer", d)}
190+}
191+
192+func (_c *MockClock_NewTimer_Call) Run(run func(d time.Duration)) *MockClock_NewTimer_Call {
193+ _c.Call.Run(func(args mock.Arguments) {
194+ var arg0 time.Duration
195+ if args[0] != nil {
196+ arg0 = args[0].(time.Duration)
197+ }
198+ run(
199+ arg0,
200+ )
201+ })
202+ return _c
203+}
204+
205+func (_c *MockClock_NewTimer_Call) Return(timer ports.Timer) *MockClock_NewTimer_Call {
206+ _c.Call.Return(timer)
207+ return _c
208+}
209+
210+func (_c *MockClock_NewTimer_Call) RunAndReturn(run func(d time.Duration) ports.Timer) *MockClock_NewTimer_Call {
211+ _c.Call.Return(run)
212+ return _c
213+}
214+
215+// Now provides a mock function for the type MockClock
216+func (_mock *MockClock) Now() time.Time {
217+ ret := _mock.Called()
218+
219+ if len(ret) == 0 {
220+ panic("no return value specified for Now")
221+ }
222+
223+ var r0 time.Time
224+ if returnFunc, ok := ret.Get(0).(func() time.Time); ok {
225+ r0 = returnFunc()
226+ } else {
227+ r0 = ret.Get(0).(time.Time)
228+ }
229+ return r0
230+}
231+
232+// MockClock_Now_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Now'
233+type MockClock_Now_Call struct {
234+ *mock.Call
235+}
236+
237+// Now is a helper method to define mock.On call
238+func (_e *MockClock_Expecter) Now() *MockClock_Now_Call {
239+ return &MockClock_Now_Call{Call: _e.mock.On("Now")}
240+}
241+
242+func (_c *MockClock_Now_Call) Run(run func()) *MockClock_Now_Call {
243+ _c.Call.Run(func(args mock.Arguments) {
244+ run()
245+ })
246+ return _c
247+}
248+
249+func (_c *MockClock_Now_Call) Return(time1 time.Time) *MockClock_Now_Call {
250+ _c.Call.Return(time1)
251+ return _c
252+}
253+
254+func (_c *MockClock_Now_Call) RunAndReturn(run func() time.Time) *MockClock_Now_Call {
255+ _c.Call.Return(run)
256+ return _c
257+}
258+
259+// NewMockTimer creates a new instance of MockTimer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
260+// The first argument is typically a *testing.T value.
261+func NewMockTimer(t interface {
262+ mock.TestingT
263+ Cleanup(func())
264+}) *MockTimer {
265+ if helper, ok := t.(interface{ Helper() }); ok {
266+ helper.Helper()
267+ }
268+
269+ mock := &MockTimer{}
270+ mock.Mock.Test(t)
271+
272+ t.Cleanup(func() {
273+ if helper, ok := t.(interface{ Helper() }); ok {
274+ helper.Helper()
275+ }
276+ mock.AssertExpectations(t)
277+ })
278+
279+ return mock
280+}
281+
282+// MockTimer is an autogenerated mock type for the Timer type
283+type MockTimer struct {
284+ mock.Mock
285+}
286+
287+type MockTimer_Expecter struct {
288+ mock *mock.Mock
289+}
290+
291+func (_m *MockTimer) EXPECT() *MockTimer_Expecter {
292+ return &MockTimer_Expecter{mock: &_m.Mock}
293+}
294+
295+// C provides a mock function for the type MockTimer
296+func (_mock *MockTimer) C() <-chan time.Time {
297+ ret := _mock.Called()
298+
299+ if len(ret) == 0 {
300+ panic("no return value specified for C")
301+ }
302+
303+ var r0 <-chan time.Time
304+ if returnFunc, ok := ret.Get(0).(func() <-chan time.Time); ok {
305+ r0 = returnFunc()
306+ } else {
307+ if ret.Get(0) != nil {
308+ r0 = ret.Get(0).(<-chan time.Time)
309+ }
310+ }
311+ return r0
312+}
313+
314+// MockTimer_C_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'C'
315+type MockTimer_C_Call struct {
316+ *mock.Call
317+}
318+
319+// C is a helper method to define mock.On call
320+func (_e *MockTimer_Expecter) C() *MockTimer_C_Call {
321+ return &MockTimer_C_Call{Call: _e.mock.On("C")}
322+}
323+
324+func (_c *MockTimer_C_Call) Run(run func()) *MockTimer_C_Call {
325+ _c.Call.Run(func(args mock.Arguments) {
326+ run()
327+ })
328+ return _c
329+}
330+
331+func (_c *MockTimer_C_Call) Return(timeCh <-chan time.Time) *MockTimer_C_Call {
332+ _c.Call.Return(timeCh)
333+ return _c
334+}
335+
336+func (_c *MockTimer_C_Call) RunAndReturn(run func() <-chan time.Time) *MockTimer_C_Call {
337+ _c.Call.Return(run)
338+ return _c
339+}
340+
341+// Reset provides a mock function for the type MockTimer
342+func (_mock *MockTimer) Reset(d time.Duration) bool {
343+ ret := _mock.Called(d)
344+
345+ if len(ret) == 0 {
346+ panic("no return value specified for Reset")
347+ }
348+
349+ var r0 bool
350+ if returnFunc, ok := ret.Get(0).(func(time.Duration) bool); ok {
351+ r0 = returnFunc(d)
352+ } else {
353+ r0 = ret.Get(0).(bool)
354+ }
355+ return r0
356+}
357+
358+// MockTimer_Reset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reset'
359+type MockTimer_Reset_Call struct {
360+ *mock.Call
361+}
362+
363+// Reset is a helper method to define mock.On call
364+// - d time.Duration
365+func (_e *MockTimer_Expecter) Reset(d any) *MockTimer_Reset_Call {
366+ return &MockTimer_Reset_Call{Call: _e.mock.On("Reset", d)}
367+}
368+
369+func (_c *MockTimer_Reset_Call) Run(run func(d time.Duration)) *MockTimer_Reset_Call {
370+ _c.Call.Run(func(args mock.Arguments) {
371+ var arg0 time.Duration
372+ if args[0] != nil {
373+ arg0 = args[0].(time.Duration)
374+ }
375+ run(
376+ arg0,
377+ )
378+ })
379+ return _c
380+}
381+
382+func (_c *MockTimer_Reset_Call) Return(b bool) *MockTimer_Reset_Call {
383+ _c.Call.Return(b)
384+ return _c
385+}
386+
387+func (_c *MockTimer_Reset_Call) RunAndReturn(run func(d time.Duration) bool) *MockTimer_Reset_Call {
388+ _c.Call.Return(run)
389+ return _c
390+}
391+
392+// Stop provides a mock function for the type MockTimer
393+func (_mock *MockTimer) Stop() bool {
394+ ret := _mock.Called()
395+
396+ if len(ret) == 0 {
397+ panic("no return value specified for Stop")
398+ }
399+
400+ var r0 bool
401+ if returnFunc, ok := ret.Get(0).(func() bool); ok {
402+ r0 = returnFunc()
403+ } else {
404+ r0 = ret.Get(0).(bool)
405+ }
406+ return r0
407+}
408+
409+// MockTimer_Stop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stop'
410+type MockTimer_Stop_Call struct {
411+ *mock.Call
412+}
413+
414+// Stop is a helper method to define mock.On call
415+func (_e *MockTimer_Expecter) Stop() *MockTimer_Stop_Call {
416+ return &MockTimer_Stop_Call{Call: _e.mock.On("Stop")}
417+}
418+
419+func (_c *MockTimer_Stop_Call) Run(run func()) *MockTimer_Stop_Call {
420+ _c.Call.Run(func(args mock.Arguments) {
421+ run()
422+ })
423+ return _c
424+}
425+
426+func (_c *MockTimer_Stop_Call) Return(b bool) *MockTimer_Stop_Call {
427+ _c.Call.Return(b)
428+ return _c
429+}
430+
431+func (_c *MockTimer_Stop_Call) RunAndReturn(run func() bool) *MockTimer_Stop_Call {
432+ _c.Call.Return(run)
433+ return _c
434+}
435+
436+// NewMockTicker creates a new instance of MockTicker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
437+// The first argument is typically a *testing.T value.
438+func NewMockTicker(t interface {
439+ mock.TestingT
440+ Cleanup(func())
441+}) *MockTicker {
442+ if helper, ok := t.(interface{ Helper() }); ok {
443+ helper.Helper()
444+ }
445+
446+ mock := &MockTicker{}
447+ mock.Mock.Test(t)
448+
449+ t.Cleanup(func() {
450+ if helper, ok := t.(interface{ Helper() }); ok {
451+ helper.Helper()
452+ }
453+ mock.AssertExpectations(t)
454+ })
455+
456+ return mock
457+}
458+
459+// MockTicker is an autogenerated mock type for the Ticker type
460+type MockTicker struct {
461+ mock.Mock
462+}
463+
464+type MockTicker_Expecter struct {
465+ mock *mock.Mock
466+}
467+
468+func (_m *MockTicker) EXPECT() *MockTicker_Expecter {
469+ return &MockTicker_Expecter{mock: &_m.Mock}
470+}
471+
472+// C provides a mock function for the type MockTicker
473+func (_mock *MockTicker) C() <-chan time.Time {
474+ ret := _mock.Called()
475+
476+ if len(ret) == 0 {
477+ panic("no return value specified for C")
478+ }
479+
480+ var r0 <-chan time.Time
481+ if returnFunc, ok := ret.Get(0).(func() <-chan time.Time); ok {
482+ r0 = returnFunc()
483+ } else {
484+ if ret.Get(0) != nil {
485+ r0 = ret.Get(0).(<-chan time.Time)
486+ }
487+ }
488+ return r0
489+}
490+
491+// MockTicker_C_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'C'
492+type MockTicker_C_Call struct {
493+ *mock.Call
494+}
495+
496+// C is a helper method to define mock.On call
497+func (_e *MockTicker_Expecter) C() *MockTicker_C_Call {
498+ return &MockTicker_C_Call{Call: _e.mock.On("C")}
499+}
500+
501+func (_c *MockTicker_C_Call) Run(run func()) *MockTicker_C_Call {
502+ _c.Call.Run(func(args mock.Arguments) {
503+ run()
504+ })
505+ return _c
506+}
507+
508+func (_c *MockTicker_C_Call) Return(timeCh <-chan time.Time) *MockTicker_C_Call {
509+ _c.Call.Return(timeCh)
510+ return _c
511+}
512+
513+func (_c *MockTicker_C_Call) RunAndReturn(run func() <-chan time.Time) *MockTicker_C_Call {
514+ _c.Call.Return(run)
515+ return _c
516+}
517+
518+// Stop provides a mock function for the type MockTicker
519+func (_mock *MockTicker) Stop() {
520+ _mock.Called()
521+ return
522+}
523+
524+// MockTicker_Stop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stop'
525+type MockTicker_Stop_Call struct {
526+ *mock.Call
527+}
528+
529+// Stop is a helper method to define mock.On call
530+func (_e *MockTicker_Expecter) Stop() *MockTicker_Stop_Call {
531+ return &MockTicker_Stop_Call{Call: _e.mock.On("Stop")}
532+}
533+
534+func (_c *MockTicker_Stop_Call) Run(run func()) *MockTicker_Stop_Call {
535+ _c.Call.Run(func(args mock.Arguments) {
536+ run()
537+ })
538+ return _c
539+}
540+
541+func (_c *MockTicker_Stop_Call) Return() *MockTicker_Stop_Call {
542+ _c.Call.Return()
543+ return _c
544+}
545+
546+func (_c *MockTicker_Stop_Call) RunAndReturn(run func()) *MockTicker_Stop_Call {
547+ _c.Run(run)
548+ return _c
549+}
@@ -0,0 +1,549 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "time"
9+
10+ "github.com/bnema/neferwl/internal/ports"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockClock creates a new instance of MockClock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockClock(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockClock {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &MockClock{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// MockClock is an autogenerated mock type for the Clock type
38+type MockClock struct {
39+ mock.Mock
40+}
41+
42+type MockClock_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *MockClock) EXPECT() *MockClock_Expecter {
47+ return &MockClock_Expecter{mock: &_m.Mock}
48+}
49+
50+// AfterFunc provides a mock function for the type MockClock
51+func (_mock *MockClock) AfterFunc(d time.Duration, f func()) ports.Timer {
52+ ret := _mock.Called(d, f)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for AfterFunc")
56+ }
57+
58+ var r0 ports.Timer
59+ if returnFunc, ok := ret.Get(0).(func(time.Duration, func()) ports.Timer); ok {
60+ r0 = returnFunc(d, f)
61+ } else {
62+ if ret.Get(0) != nil {
63+ r0 = ret.Get(0).(ports.Timer)
64+ }
65+ }
66+ return r0
67+}
68+
69+// MockClock_AfterFunc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AfterFunc'
70+type MockClock_AfterFunc_Call struct {
71+ *mock.Call
72+}
73+
74+// AfterFunc is a helper method to define mock.On call
75+// - d time.Duration
76+// - f func()
77+func (_e *MockClock_Expecter) AfterFunc(d any, f any) *MockClock_AfterFunc_Call {
78+ return &MockClock_AfterFunc_Call{Call: _e.mock.On("AfterFunc", d, f)}
79+}
80+
81+func (_c *MockClock_AfterFunc_Call) Run(run func(d time.Duration, f func())) *MockClock_AfterFunc_Call {
82+ _c.Call.Run(func(args mock.Arguments) {
83+ var arg0 time.Duration
84+ if args[0] != nil {
85+ arg0 = args[0].(time.Duration)
86+ }
87+ var arg1 func()
88+ if args[1] != nil {
89+ arg1 = args[1].(func())
90+ }
91+ run(
92+ arg0,
93+ arg1,
94+ )
95+ })
96+ return _c
97+}
98+
99+func (_c *MockClock_AfterFunc_Call) Return(timer ports.Timer) *MockClock_AfterFunc_Call {
100+ _c.Call.Return(timer)
101+ return _c
102+}
103+
104+func (_c *MockClock_AfterFunc_Call) RunAndReturn(run func(d time.Duration, f func()) ports.Timer) *MockClock_AfterFunc_Call {
105+ _c.Call.Return(run)
106+ return _c
107+}
108+
109+// NewTicker provides a mock function for the type MockClock
110+func (_mock *MockClock) NewTicker(d time.Duration) ports.Ticker {
111+ ret := _mock.Called(d)
112+
113+ if len(ret) == 0 {
114+ panic("no return value specified for NewTicker")
115+ }
116+
117+ var r0 ports.Ticker
118+ if returnFunc, ok := ret.Get(0).(func(time.Duration) ports.Ticker); ok {
119+ r0 = returnFunc(d)
120+ } else {
121+ if ret.Get(0) != nil {
122+ r0 = ret.Get(0).(ports.Ticker)
123+ }
124+ }
125+ return r0
126+}
127+
128+// MockClock_NewTicker_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewTicker'
129+type MockClock_NewTicker_Call struct {
130+ *mock.Call
131+}
132+
133+// NewTicker is a helper method to define mock.On call
134+// - d time.Duration
135+func (_e *MockClock_Expecter) NewTicker(d any) *MockClock_NewTicker_Call {
136+ return &MockClock_NewTicker_Call{Call: _e.mock.On("NewTicker", d)}
137+}
138+
139+func (_c *MockClock_NewTicker_Call) Run(run func(d time.Duration)) *MockClock_NewTicker_Call {
140+ _c.Call.Run(func(args mock.Arguments) {
141+ var arg0 time.Duration
142+ if args[0] != nil {
143+ arg0 = args[0].(time.Duration)
144+ }
145+ run(
146+ arg0,
147+ )
148+ })
149+ return _c
150+}
151+
152+func (_c *MockClock_NewTicker_Call) Return(ticker ports.Ticker) *MockClock_NewTicker_Call {
153+ _c.Call.Return(ticker)
154+ return _c
155+}
156+
157+func (_c *MockClock_NewTicker_Call) RunAndReturn(run func(d time.Duration) ports.Ticker) *MockClock_NewTicker_Call {
158+ _c.Call.Return(run)
159+ return _c
160+}
161+
162+// NewTimer provides a mock function for the type MockClock
163+func (_mock *MockClock) NewTimer(d time.Duration) ports.Timer {
164+ ret := _mock.Called(d)
165+
166+ if len(ret) == 0 {
167+ panic("no return value specified for NewTimer")
168+ }
169+
170+ var r0 ports.Timer
171+ if returnFunc, ok := ret.Get(0).(func(time.Duration) ports.Timer); ok {
172+ r0 = returnFunc(d)
173+ } else {
174+ if ret.Get(0) != nil {
175+ r0 = ret.Get(0).(ports.Timer)
176+ }
177+ }
178+ return r0
179+}
180+
181+// MockClock_NewTimer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewTimer'
182+type MockClock_NewTimer_Call struct {
183+ *mock.Call
184+}
185+
186+// NewTimer is a helper method to define mock.On call
187+// - d time.Duration
188+func (_e *MockClock_Expecter) NewTimer(d any) *MockClock_NewTimer_Call {
189+ return &MockClock_NewTimer_Call{Call: _e.mock.On("NewTimer", d)}
190+}
191+
192+func (_c *MockClock_NewTimer_Call) Run(run func(d time.Duration)) *MockClock_NewTimer_Call {
193+ _c.Call.Run(func(args mock.Arguments) {
194+ var arg0 time.Duration
195+ if args[0] != nil {
196+ arg0 = args[0].(time.Duration)
197+ }
198+ run(
199+ arg0,
200+ )
201+ })
202+ return _c
203+}
204+
205+func (_c *MockClock_NewTimer_Call) Return(timer ports.Timer) *MockClock_NewTimer_Call {
206+ _c.Call.Return(timer)
207+ return _c
208+}
209+
210+func (_c *MockClock_NewTimer_Call) RunAndReturn(run func(d time.Duration) ports.Timer) *MockClock_NewTimer_Call {
211+ _c.Call.Return(run)
212+ return _c
213+}
214+
215+// Now provides a mock function for the type MockClock
216+func (_mock *MockClock) Now() time.Time {
217+ ret := _mock.Called()
218+
219+ if len(ret) == 0 {
220+ panic("no return value specified for Now")
221+ }
222+
223+ var r0 time.Time
224+ if returnFunc, ok := ret.Get(0).(func() time.Time); ok {
225+ r0 = returnFunc()
226+ } else {
227+ r0 = ret.Get(0).(time.Time)
228+ }
229+ return r0
230+}
231+
232+// MockClock_Now_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Now'
233+type MockClock_Now_Call struct {
234+ *mock.Call
235+}
236+
237+// Now is a helper method to define mock.On call
238+func (_e *MockClock_Expecter) Now() *MockClock_Now_Call {
239+ return &MockClock_Now_Call{Call: _e.mock.On("Now")}
240+}
241+
242+func (_c *MockClock_Now_Call) Run(run func()) *MockClock_Now_Call {
243+ _c.Call.Run(func(args mock.Arguments) {
244+ run()
245+ })
246+ return _c
247+}
248+
249+func (_c *MockClock_Now_Call) Return(time1 time.Time) *MockClock_Now_Call {
250+ _c.Call.Return(time1)
251+ return _c
252+}
253+
254+func (_c *MockClock_Now_Call) RunAndReturn(run func() time.Time) *MockClock_Now_Call {
255+ _c.Call.Return(run)
256+ return _c
257+}
258+
259+// NewMockTimer creates a new instance of MockTimer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
260+// The first argument is typically a *testing.T value.
261+func NewMockTimer(t interface {
262+ mock.TestingT
263+ Cleanup(func())
264+}) *MockTimer {
265+ if helper, ok := t.(interface{ Helper() }); ok {
266+ helper.Helper()
267+ }
268+
269+ mock := &MockTimer{}
270+ mock.Mock.Test(t)
271+
272+ t.Cleanup(func() {
273+ if helper, ok := t.(interface{ Helper() }); ok {
274+ helper.Helper()
275+ }
276+ mock.AssertExpectations(t)
277+ })
278+
279+ return mock
280+}
281+
282+// MockTimer is an autogenerated mock type for the Timer type
283+type MockTimer struct {
284+ mock.Mock
285+}
286+
287+type MockTimer_Expecter struct {
288+ mock *mock.Mock
289+}
290+
291+func (_m *MockTimer) EXPECT() *MockTimer_Expecter {
292+ return &MockTimer_Expecter{mock: &_m.Mock}
293+}
294+
295+// C provides a mock function for the type MockTimer
296+func (_mock *MockTimer) C() <-chan time.Time {
297+ ret := _mock.Called()
298+
299+ if len(ret) == 0 {
300+ panic("no return value specified for C")
301+ }
302+
303+ var r0 <-chan time.Time
304+ if returnFunc, ok := ret.Get(0).(func() <-chan time.Time); ok {
305+ r0 = returnFunc()
306+ } else {
307+ if ret.Get(0) != nil {
308+ r0 = ret.Get(0).(<-chan time.Time)
309+ }
310+ }
311+ return r0
312+}
313+
314+// MockTimer_C_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'C'
315+type MockTimer_C_Call struct {
316+ *mock.Call
317+}
318+
319+// C is a helper method to define mock.On call
320+func (_e *MockTimer_Expecter) C() *MockTimer_C_Call {
321+ return &MockTimer_C_Call{Call: _e.mock.On("C")}
322+}
323+
324+func (_c *MockTimer_C_Call) Run(run func()) *MockTimer_C_Call {
325+ _c.Call.Run(func(args mock.Arguments) {
326+ run()
327+ })
328+ return _c
329+}
330+
331+func (_c *MockTimer_C_Call) Return(timeCh <-chan time.Time) *MockTimer_C_Call {
332+ _c.Call.Return(timeCh)
333+ return _c
334+}
335+
336+func (_c *MockTimer_C_Call) RunAndReturn(run func() <-chan time.Time) *MockTimer_C_Call {
337+ _c.Call.Return(run)
338+ return _c
339+}
340+
341+// Reset provides a mock function for the type MockTimer
342+func (_mock *MockTimer) Reset(d time.Duration) bool {
343+ ret := _mock.Called(d)
344+
345+ if len(ret) == 0 {
346+ panic("no return value specified for Reset")
347+ }
348+
349+ var r0 bool
350+ if returnFunc, ok := ret.Get(0).(func(time.Duration) bool); ok {
351+ r0 = returnFunc(d)
352+ } else {
353+ r0 = ret.Get(0).(bool)
354+ }
355+ return r0
356+}
357+
358+// MockTimer_Reset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reset'
359+type MockTimer_Reset_Call struct {
360+ *mock.Call
361+}
362+
363+// Reset is a helper method to define mock.On call
364+// - d time.Duration
365+func (_e *MockTimer_Expecter) Reset(d any) *MockTimer_Reset_Call {
366+ return &MockTimer_Reset_Call{Call: _e.mock.On("Reset", d)}
367+}
368+
369+func (_c *MockTimer_Reset_Call) Run(run func(d time.Duration)) *MockTimer_Reset_Call {
370+ _c.Call.Run(func(args mock.Arguments) {
371+ var arg0 time.Duration
372+ if args[0] != nil {
373+ arg0 = args[0].(time.Duration)
374+ }
375+ run(
376+ arg0,
377+ )
378+ })
379+ return _c
380+}
381+
382+func (_c *MockTimer_Reset_Call) Return(b bool) *MockTimer_Reset_Call {
383+ _c.Call.Return(b)
384+ return _c
385+}
386+
387+func (_c *MockTimer_Reset_Call) RunAndReturn(run func(d time.Duration) bool) *MockTimer_Reset_Call {
388+ _c.Call.Return(run)
389+ return _c
390+}
391+
392+// Stop provides a mock function for the type MockTimer
393+func (_mock *MockTimer) Stop() bool {
394+ ret := _mock.Called()
395+
396+ if len(ret) == 0 {
397+ panic("no return value specified for Stop")
398+ }
399+
400+ var r0 bool
401+ if returnFunc, ok := ret.Get(0).(func() bool); ok {
402+ r0 = returnFunc()
403+ } else {
404+ r0 = ret.Get(0).(bool)
405+ }
406+ return r0
407+}
408+
409+// MockTimer_Stop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stop'
410+type MockTimer_Stop_Call struct {
411+ *mock.Call
412+}
413+
414+// Stop is a helper method to define mock.On call
415+func (_e *MockTimer_Expecter) Stop() *MockTimer_Stop_Call {
416+ return &MockTimer_Stop_Call{Call: _e.mock.On("Stop")}
417+}
418+
419+func (_c *MockTimer_Stop_Call) Run(run func()) *MockTimer_Stop_Call {
420+ _c.Call.Run(func(args mock.Arguments) {
421+ run()
422+ })
423+ return _c
424+}
425+
426+func (_c *MockTimer_Stop_Call) Return(b bool) *MockTimer_Stop_Call {
427+ _c.Call.Return(b)
428+ return _c
429+}
430+
431+func (_c *MockTimer_Stop_Call) RunAndReturn(run func() bool) *MockTimer_Stop_Call {
432+ _c.Call.Return(run)
433+ return _c
434+}
435+
436+// NewMockTicker creates a new instance of MockTicker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
437+// The first argument is typically a *testing.T value.
438+func NewMockTicker(t interface {
439+ mock.TestingT
440+ Cleanup(func())
441+}) *MockTicker {
442+ if helper, ok := t.(interface{ Helper() }); ok {
443+ helper.Helper()
444+ }
445+
446+ mock := &MockTicker{}
447+ mock.Mock.Test(t)
448+
449+ t.Cleanup(func() {
450+ if helper, ok := t.(interface{ Helper() }); ok {
451+ helper.Helper()
452+ }
453+ mock.AssertExpectations(t)
454+ })
455+
456+ return mock
457+}
458+
459+// MockTicker is an autogenerated mock type for the Ticker type
460+type MockTicker struct {
461+ mock.Mock
462+}
463+
464+type MockTicker_Expecter struct {
465+ mock *mock.Mock
466+}
467+
468+func (_m *MockTicker) EXPECT() *MockTicker_Expecter {
469+ return &MockTicker_Expecter{mock: &_m.Mock}
470+}
471+
472+// C provides a mock function for the type MockTicker
473+func (_mock *MockTicker) C() <-chan time.Time {
474+ ret := _mock.Called()
475+
476+ if len(ret) == 0 {
477+ panic("no return value specified for C")
478+ }
479+
480+ var r0 <-chan time.Time
481+ if returnFunc, ok := ret.Get(0).(func() <-chan time.Time); ok {
482+ r0 = returnFunc()
483+ } else {
484+ if ret.Get(0) != nil {
485+ r0 = ret.Get(0).(<-chan time.Time)
486+ }
487+ }
488+ return r0
489+}
490+
491+// MockTicker_C_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'C'
492+type MockTicker_C_Call struct {
493+ *mock.Call
494+}
495+
496+// C is a helper method to define mock.On call
497+func (_e *MockTicker_Expecter) C() *MockTicker_C_Call {
498+ return &MockTicker_C_Call{Call: _e.mock.On("C")}
499+}
500+
501+func (_c *MockTicker_C_Call) Run(run func()) *MockTicker_C_Call {
502+ _c.Call.Run(func(args mock.Arguments) {
503+ run()
504+ })
505+ return _c
506+}
507+
508+func (_c *MockTicker_C_Call) Return(timeCh <-chan time.Time) *MockTicker_C_Call {
509+ _c.Call.Return(timeCh)
510+ return _c
511+}
512+
513+func (_c *MockTicker_C_Call) RunAndReturn(run func() <-chan time.Time) *MockTicker_C_Call {
514+ _c.Call.Return(run)
515+ return _c
516+}
517+
518+// Stop provides a mock function for the type MockTicker
519+func (_mock *MockTicker) Stop() {
520+ _mock.Called()
521+ return
522+}
523+
524+// MockTicker_Stop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stop'
525+type MockTicker_Stop_Call struct {
526+ *mock.Call
527+}
528+
529+// Stop is a helper method to define mock.On call
530+func (_e *MockTicker_Expecter) Stop() *MockTicker_Stop_Call {
531+ return &MockTicker_Stop_Call{Call: _e.mock.On("Stop")}
532+}
533+
534+func (_c *MockTicker_Stop_Call) Run(run func()) *MockTicker_Stop_Call {
535+ _c.Call.Run(func(args mock.Arguments) {
536+ run()
537+ })
538+ return _c
539+}
540+
541+func (_c *MockTicker_Stop_Call) Return() *MockTicker_Stop_Call {
542+ _c.Call.Return()
543+ return _c
544+}
545+
546+func (_c *MockTicker_Stop_Call) RunAndReturn(run func()) *MockTicker_Stop_Call {
547+ _c.Run(run)
548+ return _c
549+}
A internal/ports/clock.go +27 -0
@@ -0,0 +1,27 @@
1+package ports
2+
3+import "time"
4+
5+// Clock tells the time and makes timers. Owner goroutines select on timer
6+// channels in their own loop, so tests control time by firing them.
7+type Clock interface {
8+ Now() time.Time
9+ NewTimer(d time.Duration) Timer
10+ NewTicker(d time.Duration) Ticker
11+ // AfterFunc calls f in its own goroutine after d, unless stopped.
12+ AfterFunc(d time.Duration, f func()) Timer
13+}
14+
15+// Timer is a time.Timer. A stopped or reset timer never delivers a stale
16+// value (Go 1.23+ timer semantics).
17+type Timer interface {
18+ C() <-chan time.Time
19+ Stop() bool
20+ Reset(d time.Duration) bool
21+}
22+
23+// Ticker is a time.Ticker.
24+type Ticker interface {
25+ C() <-chan time.Time
26+ Stop()
27+}
@@ -0,0 +1,27 @@
1+package ports
2+
3+import "time"
4+
5+// Clock tells the time and makes timers. Owner goroutines select on timer
6+// channels in their own loop, so tests control time by firing them.
7+type Clock interface {
8+ Now() time.Time
9+ NewTimer(d time.Duration) Timer
10+ NewTicker(d time.Duration) Ticker
11+ // AfterFunc calls f in its own goroutine after d, unless stopped.
12+ AfterFunc(d time.Duration, f func()) Timer
13+}
14+
15+// Timer is a time.Timer. A stopped or reset timer never delivers a stale
16+// value (Go 1.23+ timer semantics).
17+type Timer interface {
18+ C() <-chan time.Time
19+ Stop() bool
20+ Reset(d time.Duration) bool
21+}
22+
23+// Ticker is a time.Ticker.
24+type Ticker interface {
25+ C() <-chan time.Time
26+ Stop()
27+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close