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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +737 -47
M .mockery.yml +3 -0
| @@ -13,10 +13,13 @@ packages: | |||
| 13 | 13 | pkgname: portsmocks | |
| 14 | 14 | filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go' | |
| 15 | 15 | interfaces: | |
| 16 | + | Clock: | |
| 16 | 17 | Notifier: | |
| 17 | 18 | OutputScaleStore: | |
| 18 | 19 | Renderer: | |
| 19 | 20 | Seat: | |
| 21 | + | Ticker: | |
| 22 | + | Timer: | |
| 20 | 23 | # Unexported adapter seams: mocks live next to the code, in test files. | |
| 21 | 24 | github.com/bnema/neferwl/internal/adapters/drm: | |
| 22 | 25 | interfaces: | |
| @@ -13,10 +13,13 @@ packages: | |||||
| 13 | pkgname: portsmocks | 13 | 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 { | |||
| 22 | 22 | timeout time.Duration | |
| 23 | 23 | input bool // get_input_idle_notification: ignores idle inhibitors | |
| 24 | 24 | idle bool // idled sent, resumed not yet | |
| 25 | - | timer *time.Timer | |
| 25 | + | timer ports.Timer | |
| 26 | 26 | 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 | 93 | gen := n.gen | |
| 94 | 94 | // Core reports activity at most once per ActivityInterval: a shorter | |
| 95 | 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 | 97 | s.display.Do(func() { | |
| 98 | 98 | // A timer stopped or replaced while it waited for Do is stale. | |
| 99 | 99 | if n.gen != gen || !n.res.Resource.Alive() || n.idle || s.idleHeld(n) { | |
| @@ -22,7 +22,7 @@ type idleNotification struct { | |||||
| 22 | timeout time.Duration | 22 | timeout time.Duration | ||
| 23 | input bool // get_input_idle_notification: ignores idle inhibitors | 23 | input bool // get_input_idle_notification: ignores idle inhibitors | ||
| 24 | idle bool // idled sent, resumed not yet | 24 | idle bool // idled sent, resumed not yet | ||
| 25 | - | timer *time.Timer | 25 | + | timer ports.Timer |
| 26 | gen uint64 // bumped on stop: a firing timer of an older gen is stale | 26 | 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.gen | 93 | gen := n.gen | ||
| 94 | // Core reports activity at most once per ActivityInterval: a shorter | 94 | // 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 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "testing" | |
| 5 | 6 | "time" | |
| 6 | 7 | ||
| 8 | + | "github.com/bnema/neferwl/internal/logging" | |
| 9 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 7 | 10 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 11 | "github.com/bnema/purego-libwayland/protocol/extidlenotify" | |
| 9 | 12 | "github.com/bnema/purego-libwayland/protocol/idleinhibit" | |
| 10 | 13 | "github.com/bnema/purego-libwayland/protocol/wlroutputpower" | |
| 11 | 14 | "github.com/bnema/wlturbo" | |
| 15 | + | "github.com/stretchr/testify/mock" | |
| 12 | 16 | ) | |
| 13 | 17 | ||
| 14 | 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 | 218 | // 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 | 221 | func TestIdleShortTimeout(t *testing.T) { | |
| 175 | - | s, _, commands, _, dir := contentServer(t) | |
| 222 | + | s, commands, dir, timers := idleClockServer(t) | |
| 176 | 223 | c := protocolClient(t, s, dir) | |
| 177 | 224 | seat := bindProtocol(t, c, "wl_seat") | |
| 178 | 225 | registerProtocol(t, c, seat) | |
| 179 | 226 | notifier := bindVersion(t, c, "ext_idle_notifier_v1", 2) | |
| 180 | 227 | id, n := newEventProxy(c) | |
| 181 | - | start := time.Now() | |
| 182 | 228 | 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) | |
| 186 | 236 | } | |
| 187 | 237 | 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) | |
| 192 | 249 | } | |
| 193 | - | n.none(t, c, ports.ActivityInterval/2) | |
| 194 | 250 | } | |
| 195 | 251 | ||
| 196 | 252 | // An unknown mode is a protocol error. | |
| @@ -1,14 +1,18 @@ | |||||
| 1 | package wayland | 1 | 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, so | 218 | // 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) // resumed | 238 | + | 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 ( | |||
| 9 | 9 | "sync" | |
| 10 | 10 | "time" | |
| 11 | 11 | ||
| 12 | + | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 12 | 13 | "github.com/bnema/neferwl/internal/ports" | |
| 13 | 14 | "github.com/bnema/purego-libwayland/protocol/fractionalscale" | |
| 14 | 15 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| @@ -30,6 +31,8 @@ type Options struct { | |||
| 30 | 31 | SyncobjNode string | |
| 31 | 32 | Keymap string | |
| 32 | 33 | RepeatRate, RepeatDelay int | |
| 34 | + | // Clock runs idle notification timers; nil is the system clock. | |
| 35 | + | Clock ports.Clock | |
| 33 | 36 | // syncDev replaces the render node's syncobj interface (tests). | |
| 34 | 37 | syncDev syncobjDevice | |
| 35 | 38 | } | |
| @@ -161,6 +164,7 @@ type Server struct { | |||
| 161 | 164 | // idleNotes and powers are the idle notifications and output power | |
| 162 | 165 | // objects (idle.go); outputsOff the outputs core turned off. | |
| 163 | 166 | idleNotes []*idleNotification | |
| 167 | + | clock ports.Clock | |
| 164 | 168 | powers []*outputPower | |
| 165 | 169 | outputsOff map[string]bool | |
| 166 | 170 | contentNotify chan struct{} | |
| @@ -233,6 +237,10 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 233 | 237 | return nil, err | |
| 234 | 238 | } | |
| 235 | 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 | 244 | s.captureReplies = map[uint64]func(ports.CaptureDone){} | |
| 237 | 245 | s.outputReplies = map[uint64]*outputConfiguration{} | |
| 238 | 246 | 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 string | 31 | SyncobjNode string | ||
| 31 | Keymap string | 32 | Keymap string | ||
| 32 | RepeatRate, RepeatDelay int | 33 | 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 syncobjDevice | 37 | syncDev syncobjDevice | ||
| 35 | } | 38 | } | ||
| @@ -161,6 +164,7 @@ type Server struct { | |||||
| 161 | // idleNotes and powers are the idle notifications and output power | 164 | // 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 []*idleNotification | 166 | idleNotes []*idleNotification | ||
| 167 | + | clock ports.Clock | |||
| 164 | powers []*outputPower | 168 | powers []*outputPower | ||
| 165 | outputsOff map[string]bool | 169 | 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, err | 237 | 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 = 1 | 246 | s.managementSerial = 1 | ||
M internal/app/app.go +2 -1
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | "sync" | |
| 12 | 12 | "time" | |
| 13 | 13 | ||
| 14 | + | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 14 | 15 | "github.com/bnema/neferwl/internal/adapters/config" | |
| 15 | 16 | "github.com/bnema/neferwl/internal/adapters/drm" | |
| 16 | 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 | 162 | workers.Add(1) | |
| 162 | 163 | go func() { | |
| 163 | 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 | 168 | 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 ( | |||
| 10 | 10 | "sync" | |
| 11 | 11 | "time" | |
| 12 | 12 | ||
| 13 | + | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 13 | 14 | "github.com/bnema/neferwl/internal/adapters/config" | |
| 14 | 15 | "github.com/bnema/neferwl/internal/adapters/drm" | |
| 15 | 16 | "github.com/bnema/neferwl/internal/adapters/seat" | |
| @@ -20,10 +21,14 @@ import ( | |||
| 20 | 21 | ||
| 21 | 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 | 27 | // drmBackend is the seat and the cards with at least one usable output. | |
| 24 | 28 | type drmBackend struct { | |
| 25 | 29 | seat *seat.Seat | |
| 26 | 30 | cards []*drmCard | |
| 31 | + | clock ports.Clock | |
| 27 | 32 | } | |
| 28 | 33 | ||
| 29 | 34 | type drmCard struct { | |
| @@ -73,7 +78,7 @@ func openDRM(ctx context.Context, cfg ports.Config) (*drmBackend, error) { | |||
| 73 | 78 | want := wantFromConfig(cfg) | |
| 74 | 79 | paths, _ := filepath.Glob("/dev/dri/card[0-9]*") | |
| 75 | 80 | sort.Strings(paths) | |
| 76 | - | b := &drmBackend{seat: s} | |
| 81 | + | b := &drmBackend{seat: s, clock: clock.System{}} | |
| 77 | 82 | var errs []error | |
| 78 | 83 | for _, path := range paths { | |
| 79 | 84 | fd, err := s.OpenDevice(path) | |
| @@ -180,17 +185,18 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||
| 180 | 185 | err error | |
| 181 | 186 | } | |
| 182 | 187 | 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 | |
| 185 | 193 | var op uint64 | |
| 186 | 194 | complete := func(d applyDecision) { | |
| 187 | 195 | if !d.reply { | |
| 188 | 196 | return | |
| 189 | 197 | } | |
| 190 | - | if timer != nil { | |
| 191 | - | timer.Stop() | |
| 192 | - | timer = nil | |
| 193 | - | } | |
| 198 | + | timer.Stop() | |
| 199 | + | deadline = nil | |
| 194 | 200 | apply.finished(d.err) | |
| 195 | 201 | } | |
| 196 | 202 | publishInventory := func() { | |
| @@ -291,10 +297,8 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||
| 291 | 297 | configure := func(cfg ports.Config) { | |
| 292 | 298 | currentConfig = cfg | |
| 293 | 299 | op++ | |
| 294 | - | if timer != nil { | |
| 295 | - | timer.Stop() | |
| 296 | - | timer = nil | |
| 297 | - | } | |
| 300 | + | timer.Stop() | |
| 301 | + | deadline = nil | |
| 298 | 302 | required := map[string]bool{} | |
| 299 | 303 | // Desired enabled heads, not the inventory's current mode, require a first modeset. | |
| 300 | 304 | w := want(currentConfig) | |
| @@ -316,13 +320,8 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||
| 316 | 320 | } | |
| 317 | 321 | d := progress.scanned(running) | |
| 318 | 322 | 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() | |
| 326 | 325 | } | |
| 327 | 326 | complete(d) | |
| 328 | 327 | } | |
| @@ -362,8 +361,8 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||
| 362 | 361 | apply.headsSent() | |
| 363 | 362 | case result := <-ready: | |
| 364 | 363 | 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)) | |
| 367 | 366 | case s := <-ch.scenes: | |
| 368 | 367 | set.scenes(s) | |
| 369 | 368 | 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.Seat | 29 | seat *seat.Seat | ||
| 26 | cards []*drmCard | 30 | 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 []error | 82 | 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 error | 185 | 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.Timer | 189 | + | timer := b.clock.NewTimer(applyTimeout) |
| 190 | + | timer.Stop() | |||
| 191 | + | defer timer.Stop() | |||
| 192 | + | var deadline <-chan time.Time | |||
| 185 | var op uint64 | 193 | var op uint64 | ||
| 186 | complete := func(d applyDecision) { | 194 | complete := func(d applyDecision) { | ||
| 187 | if !d.reply { | 195 | if !d.reply { | ||
| 188 | return | 196 | 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 = cfg | 298 | 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 := op | 323 | + | 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 ( | |||
| 14 | 14 | // debounce, so a burst of Cmd+= presses writes the config once, and tells | |
| 15 | 15 | // the user through notes. It runs until ctx ends; pending scales are then | |
| 16 | 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 | 18 | pending := map[string]float64{} | |
| 19 | - | timer := time.NewTimer(debounce) | |
| 19 | + | timer := clock.NewTimer(debounce) | |
| 20 | 20 | timer.Stop() | |
| 21 | 21 | for { | |
| 22 | 22 | select { | |
| @@ -26,7 +26,7 @@ func PersistScales(ctx context.Context, changes <-chan ports.ScaleChanged, store | |||
| 26 | 26 | case ev := <-changes: | |
| 27 | 27 | pending[ev.Output] = ev.Scale | |
| 28 | 28 | timer.Reset(debounce) | |
| 29 | - | case <-timer.C: | |
| 29 | + | case <-timer.C(): | |
| 30 | 30 | // Sorted: several outputs save and notify in a stable order. | |
| 31 | 31 | for _, name := range slices.Sorted(maps.Keys(pending)) { | |
| 32 | 32 | scale := pending[name] | |
| @@ -14,9 +14,9 @@ import ( | |||||
| 14 | // debounce, so a burst of Cmd+= presses writes the config once, and tells | 14 | // 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 then | 15 | // 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.Scale | 27 | 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 ( | |||
| 12 | 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 | 29 | // A burst of scale changes saves only the last scale of each output, once. | |
| 16 | 30 | func TestPersistScalesDebounces(t *testing.T) { | |
| 17 | 31 | store := portsmocks.NewMockOutputScaleStore(t) | |
| 18 | 32 | notes := portsmocks.NewMockNotifier(t) | |
| 19 | - | done := make(chan struct{}, 2) | |
| 20 | 33 | store.EXPECT().SaveOutputScale("DP-1", 1.25).Return(nil).Once() | |
| 21 | 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() | |
| 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) | |
| 24 | 38 | changes := make(chan ports.ScaleChanged) | |
| 25 | 39 | ctx, cancel := context.WithCancel(context.Background()) | |
| 26 | 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 | 42 | for _, ev := range []ports.ScaleChanged{{Output: "DP-2", Scale: 1.5}, {Output: "DP-1", Scale: 1.25}, {Output: "DP-2", Scale: 2}} { | |
| 29 | 43 | changes <- ev | |
| 30 | 44 | } | |
| 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} | |
| 33 | 48 | cancel() | |
| 34 | 49 | <-stopped | |
| 35 | 50 | } | |
| @@ -38,15 +53,15 @@ func TestPersistScalesDebounces(t *testing.T) { | |||
| 38 | 53 | func TestPersistScalesReportsFailure(t *testing.T) { | |
| 39 | 54 | store := portsmocks.NewMockOutputScaleStore(t) | |
| 40 | 55 | notes := portsmocks.NewMockNotifier(t) | |
| 41 | - | done := make(chan struct{}, 1) | |
| 42 | 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 | 59 | changes := make(chan ports.ScaleChanged) | |
| 45 | 60 | ctx, cancel := context.WithCancel(context.Background()) | |
| 46 | 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 | 63 | changes <- ports.ScaleChanged{Output: "DP-1", Scale: 1.5} | |
| 49 | - | receive(t, done) | |
| 64 | + | fire <- time.Time{} | |
| 50 | 65 | cancel() | |
| 51 | 66 | <-stopped | |
| 52 | 67 | } | |
| @@ -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 <- ev | 43 | 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 | <-stopped | 49 | <-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 | <-stopped | 66 | <-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 | + | } | |||