detached 4d15883

test(daemon): use generated clock mocks for coordinator

commit
4d15883de381328bfc39d8cd5ad24c97b18bf469
author
brice · · 2026-07-11 17:47 UTC
parent
3398032
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +79 -44
M internal/usecase/daemon/render_coordinator_test.go +76 -41
@@ -9,20 +9,72 @@ import (
99 "testing"
1010 "time"
1111
12+ "github.com/stretchr/testify/mock"
1213 "github.com/stretchr/testify/require"
1314
1415 "github.com/bnema/vev/internal/domain"
1516 "github.com/bnema/vev/internal/ports"
17+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
1618 "github.com/bnema/vev/internal/usecase/keys"
1719 "github.com/bnema/vev/internal/usecase/layout"
1820 )
1921
2022 // --- coordinator harness ------------------------------------------------------
2123
22-// coordinatorHarness wires one coordinator to a fake clock and recording
23-// hooks. Every assertion is channel- or counter-based; nothing sleeps.
24+// coordinatorMockClock uses generated mocks while retaining deterministic timer
25+// channels and recorded deadlines for coordinator scheduling assertions.
26+type coordinatorMockClock struct {
27+ clock *portsmocks.MockClock
28+ timers chan *coordinatorMockTimer
29+ inert bool
30+}
31+
32+type coordinatorMockTimer struct {
33+ mock *portsmocks.MockTimer
34+ ch chan time.Time
35+ duration time.Duration
36+}
37+
38+func newCoordinatorMockClock(t *testing.T, capacity int) *coordinatorMockClock {
39+ t.Helper()
40+ clk := &coordinatorMockClock{
41+ clock: portsmocks.NewMockClock(t),
42+ timers: make(chan *coordinatorMockTimer, capacity),
43+ }
44+ clk.clock.EXPECT().Now().Return(time.Time{}).Maybe()
45+ clk.clock.EXPECT().NewTimer(mock.MatchedBy(func(d time.Duration) bool {
46+ return d == urgentRenderDeadline ||
47+ (d >= minOutputRenderDeadline && d <= maxOutputRenderDeadline) ||
48+ d == maxSyncUpdateDuration
49+ })).RunAndReturn(func(d time.Duration) ports.Timer {
50+ timer := &coordinatorMockTimer{
51+ mock: portsmocks.NewMockTimer(t),
52+ duration: d,
53+ }
54+ if !clk.inert {
55+ timer.ch = make(chan time.Time, 1)
56+ }
57+ // A normal deadline reads C both in its worker and in the inert-clock
58+ // guard; a watchdog reads it in its worker. The generated expectation
59+ // makes every channel access observable without a hand-written Timer.
60+ timer.mock.EXPECT().C().Maybe().Return((<-chan time.Time)(timer.ch))
61+ timer.mock.EXPECT().Stop().Maybe().Return(true)
62+ clk.timers <- timer
63+ return timer.mock
64+ }).Maybe()
65+ return clk
66+}
67+
68+func newInertCoordinatorMockClock(t *testing.T, capacity int) *coordinatorMockClock {
69+ clk := newCoordinatorMockClock(t, capacity)
70+ clk.inert = true
71+ return clk
72+}
73+
74+// coordinatorHarness wires one coordinator to generated clock/timer mocks and
75+// recording hooks. Every assertion is channel- or counter-based; nothing sleeps.
2476 type coordinatorHarness struct {
25- clk *signalClock
77+ clk *coordinatorMockClock
2678 wakes chan renderWake
2779 previews chan renderWake
2880 ackReady atomic.Bool
@@ -33,13 +85,13 @@ type coordinatorHarness struct {
3385 func newCoordinatorHarness(t *testing.T) *coordinatorHarness {
3486 t.Helper()
3587 h := &coordinatorHarness{
36- clk: &signalClock{timers: make(chan *signalTimer, 16)},
88+ clk: newCoordinatorMockClock(t, 16),
3789 wakes: make(chan renderWake, 16),
3890 previews: make(chan renderWake, 16),
3991 }
4092 h.ackReady.Store(true)
4193 h.rc = newRenderCoordinator(renderCoordinatorOptions{
42- clock: h.clk,
94+ clock: h.clk.clock,
4395 wake: func(w renderWake) { h.wakes <- w },
4496 ackReady: func() bool { return h.ackReady.Load() },
4597 syncActive: func() bool { return h.syncActive.Load() },
@@ -50,9 +102,9 @@ func newCoordinatorHarness(t *testing.T) *coordinatorHarness {
50102 // armedTimers drains every deadline armed so far. Arming happens
51103 // synchronously inside the coordinator call, so a non-blocking drain is
52104 // deterministic.
53-func (h *coordinatorHarness) armedTimers(t *testing.T) []*signalTimer {
105+func (h *coordinatorHarness) armedTimers(t *testing.T) []*coordinatorMockTimer {
54106 t.Helper()
55- var timers []*signalTimer
107+ var timers []*coordinatorMockTimer
56108 for {
57109 select {
58110 case tm := <-h.clk.timers:
@@ -112,7 +164,7 @@ func requireNoInvalidation(t *testing.T, ch chan renderInvalidation) {
112164 }
113165 }
114166
115-func awaitScheduledTimer(t *testing.T, clk *signalClock) *signalTimer {
167+func awaitCoordinatorScheduledTimer(t *testing.T, clk *coordinatorMockClock) *coordinatorMockTimer {
116168 t.Helper()
117169 select {
118170 case tm := <-clk.timers:
@@ -639,10 +691,10 @@ func TestProducerInvalidations(t *testing.T) {
639691 file: "render.go",
640692 name: "retained resize dispatch",
641693 run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
642- clk := &signalClock{timers: make(chan *signalTimer, 2)}
643- d.clock = clk
694+ clk := newCoordinatorMockClock(t, 2)
695+ d.clock = clk.clock
644696 d.resize(sess, ac, domain.Size{Cols: 100, Rows: 26})
645- awaitScheduledTimer(t, clk).ch <- time.Time{}
697+ awaitCoordinatorScheduledTimer(t, clk).ch <- time.Time{}
646698 },
647699 },
648700 {
@@ -747,23 +799,6 @@ func TestStartPaneGoroutinesAccountsForOneReader(t *testing.T) {
747799 }
748800 }
749801
750-// inertTimer deliberately has a nil C channel. It verifies coordinator
751-// lifecycle paths do not strand workers behind fake clocks that never fire.
752-type inertTimer struct{ stopped atomic.Bool }
753-
754-func (t *inertTimer) C() <-chan time.Time { return nil }
755-func (t *inertTimer) Reset(time.Duration) bool { return false }
756-func (t *inertTimer) Stop() bool { t.stopped.Store(true); return true }
757-
758-type inertClock struct{ timers chan *inertTimer }
759-
760-func (c *inertClock) Now() time.Time { return time.Time{} }
761-func (c *inertClock) NewTimer(time.Duration) ports.Timer {
762- t := &inertTimer{}
763- c.timers <- t
764- return t
765-}
766-
767802 func requireWorkerExit(t *testing.T, done <-chan struct{}) {
768803 t.Helper()
769804 for range 4096 {
@@ -788,8 +823,8 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) {
788823 }
789824 for _, tc := range lifecycle {
790825 t.Run(tc.name, func(t *testing.T) {
791- clk := &inertClock{timers: make(chan *inertTimer, 2)}
792- rc := newRenderCoordinator(renderCoordinatorOptions{clock: clk})
826+ clk := newInertCoordinatorMockClock(t, 2)
827+ rc := newRenderCoordinator(renderCoordinatorOptions{clock: clk.clock})
793828 ac := &attachedClient{}
794829 rc.attach(ac)
795830 rc.invalidate(renderInvalidation{class: invalidateOutput})
@@ -798,12 +833,12 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) {
798833 normalDone := rc.normalWorkerDone
799834 rc.mu.Unlock()
800835 tc.end(rc, ac)
801- require.True(t, normal.stopped.Load(), "normal timer must be stopped")
836+ normal.mock.AssertNumberOfCalls(t, "Stop", 1)
802837 requireWorkerExit(t, normalDone)
803838
804839 // Use a fresh coordinator because detach/teardown intentionally makes
805840 // an attachment ineligible for a new synchronized batch.
806- rc = newRenderCoordinator(renderCoordinatorOptions{clock: clk})
841+ rc = newRenderCoordinator(renderCoordinatorOptions{clock: clk.clock})
807842 rc.attach(ac)
808843 rc.noteSyncBegin(1)
809844 syncTimer := <-clk.timers
@@ -811,23 +846,23 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) {
811846 syncDone := rc.syncWorkerDone
812847 rc.mu.Unlock()
813848 tc.end(rc, ac)
814- require.True(t, syncTimer.stopped.Load(), "sync watchdog must be stopped")
849+ syncTimer.mock.AssertNumberOfCalls(t, "Stop", 1)
815850 requireWorkerExit(t, syncDone)
816851 })
817852 }
818853 }
819854
820855 func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) {
821- newResizeFixture := func(t *testing.T) (*Daemon, *session, *attachedClient, chan ports.Frame, *signalClock, chan renderInvalidation) {
856+ newResizeFixture := func(t *testing.T) (*Daemon, *session, *attachedClient, chan ports.Frame, *coordinatorMockClock, chan renderInvalidation) {
822857 t.Helper()
823858 p, releasePTY := newBlockingPTY(t)
824859 t.Cleanup(releasePTY)
825860 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
826- clk := &signalClock{timers: make(chan *signalTimer, 4)}
827- d.clock = clk
861+ clk := newCoordinatorMockClock(t, 4)
862+ d.clock = clk.clock
828863 invs := make(chan renderInvalidation, 4)
829864 sess.installRenderCoordinator(newRenderCoordinator(renderCoordinatorOptions{
830- clock: clk,
865+ clock: clk.clock,
831866 wake: func(renderWake) {},
832867 onInvalidate: func(inv renderInvalidation) { invs <- inv },
833868 }))
@@ -838,7 +873,7 @@ func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) {
838873 d, sess, ac, sends, clk, invs := newResizeFixture(t)
839874
840875 d.resize(sess, ac, domain.Size{Cols: 120, Rows: 24})
841- timer := awaitScheduledTimer(t, clk)
876+ timer := awaitCoordinatorScheduledTimer(t, clk)
842877 require.GreaterOrEqual(t, timer.duration, minOutputRenderDeadline)
843878 require.LessOrEqual(t, timer.duration, maxOutputRenderDeadline)
844879 ac.sendMu.Lock()
@@ -865,9 +900,9 @@ func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) {
865900 d, sess, ac, sends, clk, invs := newResizeFixture(t)
866901
867902 d.resize(sess, ac, domain.Size{Cols: 100, Rows: 24})
868- first := awaitScheduledTimer(t, clk)
903+ first := awaitCoordinatorScheduledTimer(t, clk)
869904 d.resize(sess, ac, domain.Size{Cols: 120, Rows: 24})
870- latest := awaitScheduledTimer(t, clk)
905+ latest := awaitCoordinatorScheduledTimer(t, clk)
871906 require.Equal(t, uint64(2), sess.renderCoordinator().resizeSnapshot().epoch,
872907 "every resize request must advance the coordinator epoch")
873908
@@ -887,7 +922,7 @@ func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) {
887922 d, sess, ac, sends, clk, invs := newResizeFixture(t)
888923
889924 d.resize(sess, ac, domain.Size{Cols: 90, Rows: 30})
890- timer := awaitScheduledTimer(t, clk)
925+ timer := awaitCoordinatorScheduledTimer(t, clk)
891926 ac.cancelResizePaint()
892927
893928 timer.ch <- time.Time{}
@@ -9,20 +9,72 @@ import (
9 "testing"9 "testing"
10 "time"10 "time"
11 11
12+ "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"13 "github.com/stretchr/testify/require"
13 14
14 "github.com/bnema/vev/internal/domain"15 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/ports"16 "github.com/bnema/vev/internal/ports"
17+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
16 "github.com/bnema/vev/internal/usecase/keys"18 "github.com/bnema/vev/internal/usecase/keys"
17 "github.com/bnema/vev/internal/usecase/layout"19 "github.com/bnema/vev/internal/usecase/layout"
18 )20 )
19 21
20 // --- coordinator harness ------------------------------------------------------22 // --- coordinator harness ------------------------------------------------------
21 23
22-// coordinatorHarness wires one coordinator to a fake clock and recording24+// coordinatorMockClock uses generated mocks while retaining deterministic timer
23-// hooks. Every assertion is channel- or counter-based; nothing sleeps.25+// channels and recorded deadlines for coordinator scheduling assertions.
26+type coordinatorMockClock struct {
27+ clock *portsmocks.MockClock
28+ timers chan *coordinatorMockTimer
29+ inert bool
30+}
31+
32+type coordinatorMockTimer struct {
33+ mock *portsmocks.MockTimer
34+ ch chan time.Time
35+ duration time.Duration
36+}
37+
38+func newCoordinatorMockClock(t *testing.T, capacity int) *coordinatorMockClock {
39+ t.Helper()
40+ clk := &coordinatorMockClock{
41+ clock: portsmocks.NewMockClock(t),
42+ timers: make(chan *coordinatorMockTimer, capacity),
43+ }
44+ clk.clock.EXPECT().Now().Return(time.Time{}).Maybe()
45+ clk.clock.EXPECT().NewTimer(mock.MatchedBy(func(d time.Duration) bool {
46+ return d == urgentRenderDeadline ||
47+ (d >= minOutputRenderDeadline && d <= maxOutputRenderDeadline) ||
48+ d == maxSyncUpdateDuration
49+ })).RunAndReturn(func(d time.Duration) ports.Timer {
50+ timer := &coordinatorMockTimer{
51+ mock: portsmocks.NewMockTimer(t),
52+ duration: d,
53+ }
54+ if !clk.inert {
55+ timer.ch = make(chan time.Time, 1)
56+ }
57+ // A normal deadline reads C both in its worker and in the inert-clock
58+ // guard; a watchdog reads it in its worker. The generated expectation
59+ // makes every channel access observable without a hand-written Timer.
60+ timer.mock.EXPECT().C().Maybe().Return((<-chan time.Time)(timer.ch))
61+ timer.mock.EXPECT().Stop().Maybe().Return(true)
62+ clk.timers <- timer
63+ return timer.mock
64+ }).Maybe()
65+ return clk
66+}
67+
68+func newInertCoordinatorMockClock(t *testing.T, capacity int) *coordinatorMockClock {
69+ clk := newCoordinatorMockClock(t, capacity)
70+ clk.inert = true
71+ return clk
72+}
73+
74+// coordinatorHarness wires one coordinator to generated clock/timer mocks and
75+// recording hooks. Every assertion is channel- or counter-based; nothing sleeps.
24 type coordinatorHarness struct {76 type coordinatorHarness struct {
25- clk *signalClock77+ clk *coordinatorMockClock
26 wakes chan renderWake78 wakes chan renderWake
27 previews chan renderWake79 previews chan renderWake
28 ackReady atomic.Bool80 ackReady atomic.Bool
@@ -33,13 +85,13 @@ type coordinatorHarness struct {
33 func newCoordinatorHarness(t *testing.T) *coordinatorHarness {85 func newCoordinatorHarness(t *testing.T) *coordinatorHarness {
34 t.Helper()86 t.Helper()
35 h := &coordinatorHarness{87 h := &coordinatorHarness{
36- clk: &signalClock{timers: make(chan *signalTimer, 16)},88+ clk: newCoordinatorMockClock(t, 16),
37 wakes: make(chan renderWake, 16),89 wakes: make(chan renderWake, 16),
38 previews: make(chan renderWake, 16),90 previews: make(chan renderWake, 16),
39 }91 }
40 h.ackReady.Store(true)92 h.ackReady.Store(true)
41 h.rc = newRenderCoordinator(renderCoordinatorOptions{93 h.rc = newRenderCoordinator(renderCoordinatorOptions{
42- clock: h.clk,94+ clock: h.clk.clock,
43 wake: func(w renderWake) { h.wakes <- w },95 wake: func(w renderWake) { h.wakes <- w },
44 ackReady: func() bool { return h.ackReady.Load() },96 ackReady: func() bool { return h.ackReady.Load() },
45 syncActive: func() bool { return h.syncActive.Load() },97 syncActive: func() bool { return h.syncActive.Load() },
@@ -50,9 +102,9 @@ func newCoordinatorHarness(t *testing.T) *coordinatorHarness {
50 // armedTimers drains every deadline armed so far. Arming happens102 // armedTimers drains every deadline armed so far. Arming happens
51 // synchronously inside the coordinator call, so a non-blocking drain is103 // synchronously inside the coordinator call, so a non-blocking drain is
52 // deterministic.104 // deterministic.
53-func (h *coordinatorHarness) armedTimers(t *testing.T) []*signalTimer {105+func (h *coordinatorHarness) armedTimers(t *testing.T) []*coordinatorMockTimer {
54 t.Helper()106 t.Helper()
55- var timers []*signalTimer107+ var timers []*coordinatorMockTimer
56 for {108 for {
57 select {109 select {
58 case tm := <-h.clk.timers:110 case tm := <-h.clk.timers:
@@ -112,7 +164,7 @@ func requireNoInvalidation(t *testing.T, ch chan renderInvalidation) {
112 }164 }
113 }165 }
114 166
115-func awaitScheduledTimer(t *testing.T, clk *signalClock) *signalTimer {167+func awaitCoordinatorScheduledTimer(t *testing.T, clk *coordinatorMockClock) *coordinatorMockTimer {
116 t.Helper()168 t.Helper()
117 select {169 select {
118 case tm := <-clk.timers:170 case tm := <-clk.timers:
@@ -639,10 +691,10 @@ func TestProducerInvalidations(t *testing.T) {
639 file: "render.go",691 file: "render.go",
640 name: "retained resize dispatch",692 name: "retained resize dispatch",
641 run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {693 run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
642- clk := &signalClock{timers: make(chan *signalTimer, 2)}694+ clk := newCoordinatorMockClock(t, 2)
643- d.clock = clk695+ d.clock = clk.clock
644 d.resize(sess, ac, domain.Size{Cols: 100, Rows: 26})696 d.resize(sess, ac, domain.Size{Cols: 100, Rows: 26})
645- awaitScheduledTimer(t, clk).ch <- time.Time{}697+ awaitCoordinatorScheduledTimer(t, clk).ch <- time.Time{}
646 },698 },
647 },699 },
648 {700 {
@@ -747,23 +799,6 @@ func TestStartPaneGoroutinesAccountsForOneReader(t *testing.T) {
747 }799 }
748 }800 }
749 801
750-// inertTimer deliberately has a nil C channel. It verifies coordinator
751-// lifecycle paths do not strand workers behind fake clocks that never fire.
752-type inertTimer struct{ stopped atomic.Bool }
753-
754-func (t *inertTimer) C() <-chan time.Time { return nil }
755-func (t *inertTimer) Reset(time.Duration) bool { return false }
756-func (t *inertTimer) Stop() bool { t.stopped.Store(true); return true }
757-
758-type inertClock struct{ timers chan *inertTimer }
759-
760-func (c *inertClock) Now() time.Time { return time.Time{} }
761-func (c *inertClock) NewTimer(time.Duration) ports.Timer {
762- t := &inertTimer{}
763- c.timers <- t
764- return t
765-}
766-
767 func requireWorkerExit(t *testing.T, done <-chan struct{}) {802 func requireWorkerExit(t *testing.T, done <-chan struct{}) {
768 t.Helper()803 t.Helper()
769 for range 4096 {804 for range 4096 {
@@ -788,8 +823,8 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) {
788 }823 }
789 for _, tc := range lifecycle {824 for _, tc := range lifecycle {
790 t.Run(tc.name, func(t *testing.T) {825 t.Run(tc.name, func(t *testing.T) {
791- clk := &inertClock{timers: make(chan *inertTimer, 2)}826+ clk := newInertCoordinatorMockClock(t, 2)
792- rc := newRenderCoordinator(renderCoordinatorOptions{clock: clk})827+ rc := newRenderCoordinator(renderCoordinatorOptions{clock: clk.clock})
793 ac := &attachedClient{}828 ac := &attachedClient{}
794 rc.attach(ac)829 rc.attach(ac)
795 rc.invalidate(renderInvalidation{class: invalidateOutput})830 rc.invalidate(renderInvalidation{class: invalidateOutput})
@@ -798,12 +833,12 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) {
798 normalDone := rc.normalWorkerDone833 normalDone := rc.normalWorkerDone
799 rc.mu.Unlock()834 rc.mu.Unlock()
800 tc.end(rc, ac)835 tc.end(rc, ac)
801- require.True(t, normal.stopped.Load(), "normal timer must be stopped")836+ normal.mock.AssertNumberOfCalls(t, "Stop", 1)
802 requireWorkerExit(t, normalDone)837 requireWorkerExit(t, normalDone)
803 838
804 // Use a fresh coordinator because detach/teardown intentionally makes839 // Use a fresh coordinator because detach/teardown intentionally makes
805 // an attachment ineligible for a new synchronized batch.840 // an attachment ineligible for a new synchronized batch.
806- rc = newRenderCoordinator(renderCoordinatorOptions{clock: clk})841+ rc = newRenderCoordinator(renderCoordinatorOptions{clock: clk.clock})
807 rc.attach(ac)842 rc.attach(ac)
808 rc.noteSyncBegin(1)843 rc.noteSyncBegin(1)
809 syncTimer := <-clk.timers844 syncTimer := <-clk.timers
@@ -811,23 +846,23 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) {
811 syncDone := rc.syncWorkerDone846 syncDone := rc.syncWorkerDone
812 rc.mu.Unlock()847 rc.mu.Unlock()
813 tc.end(rc, ac)848 tc.end(rc, ac)
814- require.True(t, syncTimer.stopped.Load(), "sync watchdog must be stopped")849+ syncTimer.mock.AssertNumberOfCalls(t, "Stop", 1)
815 requireWorkerExit(t, syncDone)850 requireWorkerExit(t, syncDone)
816 })851 })
817 }852 }
818 }853 }
819 854
820 func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) {855 func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) {
821- newResizeFixture := func(t *testing.T) (*Daemon, *session, *attachedClient, chan ports.Frame, *signalClock, chan renderInvalidation) {856+ newResizeFixture := func(t *testing.T) (*Daemon, *session, *attachedClient, chan ports.Frame, *coordinatorMockClock, chan renderInvalidation) {
822 t.Helper()857 t.Helper()
823 p, releasePTY := newBlockingPTY(t)858 p, releasePTY := newBlockingPTY(t)
824 t.Cleanup(releasePTY)859 t.Cleanup(releasePTY)
825 d, sess, ac, sends := newManualSessionWithPTYs(t, p)860 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
826- clk := &signalClock{timers: make(chan *signalTimer, 4)}861+ clk := newCoordinatorMockClock(t, 4)
827- d.clock = clk862+ d.clock = clk.clock
828 invs := make(chan renderInvalidation, 4)863 invs := make(chan renderInvalidation, 4)
829 sess.installRenderCoordinator(newRenderCoordinator(renderCoordinatorOptions{864 sess.installRenderCoordinator(newRenderCoordinator(renderCoordinatorOptions{
830- clock: clk,865+ clock: clk.clock,
831 wake: func(renderWake) {},866 wake: func(renderWake) {},
832 onInvalidate: func(inv renderInvalidation) { invs <- inv },867 onInvalidate: func(inv renderInvalidation) { invs <- inv },
833 }))868 }))
@@ -838,7 +873,7 @@ func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) {
838 d, sess, ac, sends, clk, invs := newResizeFixture(t)873 d, sess, ac, sends, clk, invs := newResizeFixture(t)
839 874
840 d.resize(sess, ac, domain.Size{Cols: 120, Rows: 24})875 d.resize(sess, ac, domain.Size{Cols: 120, Rows: 24})
841- timer := awaitScheduledTimer(t, clk)876+ timer := awaitCoordinatorScheduledTimer(t, clk)
842 require.GreaterOrEqual(t, timer.duration, minOutputRenderDeadline)877 require.GreaterOrEqual(t, timer.duration, minOutputRenderDeadline)
843 require.LessOrEqual(t, timer.duration, maxOutputRenderDeadline)878 require.LessOrEqual(t, timer.duration, maxOutputRenderDeadline)
844 ac.sendMu.Lock()879 ac.sendMu.Lock()
@@ -865,9 +900,9 @@ func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) {
865 d, sess, ac, sends, clk, invs := newResizeFixture(t)900 d, sess, ac, sends, clk, invs := newResizeFixture(t)
866 901
867 d.resize(sess, ac, domain.Size{Cols: 100, Rows: 24})902 d.resize(sess, ac, domain.Size{Cols: 100, Rows: 24})
868- first := awaitScheduledTimer(t, clk)903+ first := awaitCoordinatorScheduledTimer(t, clk)
869 d.resize(sess, ac, domain.Size{Cols: 120, Rows: 24})904 d.resize(sess, ac, domain.Size{Cols: 120, Rows: 24})
870- latest := awaitScheduledTimer(t, clk)905+ latest := awaitCoordinatorScheduledTimer(t, clk)
871 require.Equal(t, uint64(2), sess.renderCoordinator().resizeSnapshot().epoch,906 require.Equal(t, uint64(2), sess.renderCoordinator().resizeSnapshot().epoch,
872 "every resize request must advance the coordinator epoch")907 "every resize request must advance the coordinator epoch")
873 908
@@ -887,7 +922,7 @@ func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) {
887 d, sess, ac, sends, clk, invs := newResizeFixture(t)922 d, sess, ac, sends, clk, invs := newResizeFixture(t)
888 923
889 d.resize(sess, ac, domain.Size{Cols: 90, Rows: 30})924 d.resize(sess, ac, domain.Size{Cols: 90, Rows: 30})
890- timer := awaitScheduledTimer(t, clk)925+ timer := awaitCoordinatorScheduledTimer(t, clk)
891 ac.cancelResizePaint()926 ac.cancelResizePaint()
892 927
893 timer.ch <- time.Time{}928 timer.ch <- time.Time{}
M internal/usecase/daemon/snapshot_test.go +3 -3
@@ -221,9 +221,9 @@ func TestPTYReaderSameReadSynchronizedOutputUsesUrgentCoordinatorDeadline(t *tes
221221 tb := newTestTabWithContext(p, sctx, cancel)
222222 sess := &session{id: "sync-urgent", name: "sync-urgent", tabs: []*tab{tb}, ctx: sctx, cancel: cancel}
223223 invs := make(chan renderInvalidation, 1)
224- clock := &signalClock{timers: make(chan *signalTimer, 2)}
224+ clock := newCoordinatorMockClock(t, 2)
225225 sess.installRenderCoordinator(newRenderCoordinator(renderCoordinatorOptions{
226- clock: clock,
226+ clock: clock.clock,
227227 onInvalidate: func(inv renderInvalidation) { invs <- inv },
228228 }))
229229 d.sessions[sess.id] = sess
@@ -232,7 +232,7 @@ func TestPTYReaderSameReadSynchronizedOutputUsesUrgentCoordinatorDeadline(t *tes
232232
233233 inv := awaitInvalidation(t, invs)
234234 require.Equal(t, invalidateUrgent, inv.class, "a complete same-read synchronized batch must flush urgently")
235- timer := awaitScheduledTimer(t, clock)
235+ timer := awaitCoordinatorScheduledTimer(t, clock)
236236 require.Equal(t, urgentRenderDeadline, timer.duration)
237237 }
238238
@@ -221,9 +221,9 @@ func TestPTYReaderSameReadSynchronizedOutputUsesUrgentCoordinatorDeadline(t *tes
221 tb := newTestTabWithContext(p, sctx, cancel)221 tb := newTestTabWithContext(p, sctx, cancel)
222 sess := &session{id: "sync-urgent", name: "sync-urgent", tabs: []*tab{tb}, ctx: sctx, cancel: cancel}222 sess := &session{id: "sync-urgent", name: "sync-urgent", tabs: []*tab{tb}, ctx: sctx, cancel: cancel}
223 invs := make(chan renderInvalidation, 1)223 invs := make(chan renderInvalidation, 1)
224- clock := &signalClock{timers: make(chan *signalTimer, 2)}224+ clock := newCoordinatorMockClock(t, 2)
225 sess.installRenderCoordinator(newRenderCoordinator(renderCoordinatorOptions{225 sess.installRenderCoordinator(newRenderCoordinator(renderCoordinatorOptions{
226- clock: clock,226+ clock: clock.clock,
227 onInvalidate: func(inv renderInvalidation) { invs <- inv },227 onInvalidate: func(inv renderInvalidation) { invs <- inv },
228 }))228 }))
229 d.sessions[sess.id] = sess229 d.sessions[sess.id] = sess
@@ -232,7 +232,7 @@ func TestPTYReaderSameReadSynchronizedOutputUsesUrgentCoordinatorDeadline(t *tes
232 232
233 inv := awaitInvalidation(t, invs)233 inv := awaitInvalidation(t, invs)
234 require.Equal(t, invalidateUrgent, inv.class, "a complete same-read synchronized batch must flush urgently")234 require.Equal(t, invalidateUrgent, inv.class, "a complete same-read synchronized batch must flush urgently")
235- timer := awaitScheduledTimer(t, clock)235+ timer := awaitCoordinatorScheduledTimer(t, clock)
236 require.Equal(t, urgentRenderDeadline, timer.duration)236 require.Equal(t, urgentRenderDeadline, timer.duration)
237 }237 }
238 238

Keyboard shortcuts

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

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

Branches 12

Tags 11

↑↓ moveenter openesc close