test(daemon): use generated clock mocks for coordinator
- commit
- 4d15883de381328bfc39d8cd5ad24c97b18bf469
- author
- brice · · 2026-07-11 17:47 UTC
- parent
- 3398032
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 ( | |||
| 9 | 9 | "testing" | |
| 10 | 10 | "time" | |
| 11 | 11 | ||
| 12 | + | "github.com/stretchr/testify/mock" | |
| 12 | 13 | "github.com/stretchr/testify/require" | |
| 13 | 14 | ||
| 14 | 15 | "github.com/bnema/vev/internal/domain" | |
| 15 | 16 | "github.com/bnema/vev/internal/ports" | |
| 17 | + | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 16 | 18 | "github.com/bnema/vev/internal/usecase/keys" | |
| 17 | 19 | "github.com/bnema/vev/internal/usecase/layout" | |
| 18 | 20 | ) | |
| 19 | 21 | ||
| 20 | 22 | // --- coordinator harness ------------------------------------------------------ | |
| 21 | 23 | ||
| 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. | |
| 24 | 76 | type coordinatorHarness struct { | |
| 25 | - | clk *signalClock | |
| 77 | + | clk *coordinatorMockClock | |
| 26 | 78 | wakes chan renderWake | |
| 27 | 79 | previews chan renderWake | |
| 28 | 80 | ackReady atomic.Bool | |
| @@ -33,13 +85,13 @@ type coordinatorHarness struct { | |||
| 33 | 85 | func newCoordinatorHarness(t *testing.T) *coordinatorHarness { | |
| 34 | 86 | t.Helper() | |
| 35 | 87 | h := &coordinatorHarness{ | |
| 36 | - | clk: &signalClock{timers: make(chan *signalTimer, 16)}, | |
| 88 | + | clk: newCoordinatorMockClock(t, 16), | |
| 37 | 89 | wakes: make(chan renderWake, 16), | |
| 38 | 90 | previews: make(chan renderWake, 16), | |
| 39 | 91 | } | |
| 40 | 92 | h.ackReady.Store(true) | |
| 41 | 93 | h.rc = newRenderCoordinator(renderCoordinatorOptions{ | |
| 42 | - | clock: h.clk, | |
| 94 | + | clock: h.clk.clock, | |
| 43 | 95 | wake: func(w renderWake) { h.wakes <- w }, | |
| 44 | 96 | ackReady: func() bool { return h.ackReady.Load() }, | |
| 45 | 97 | syncActive: func() bool { return h.syncActive.Load() }, | |
| @@ -50,9 +102,9 @@ func newCoordinatorHarness(t *testing.T) *coordinatorHarness { | |||
| 50 | 102 | // armedTimers drains every deadline armed so far. Arming happens | |
| 51 | 103 | // synchronously inside the coordinator call, so a non-blocking drain is | |
| 52 | 104 | // deterministic. | |
| 53 | - | func (h *coordinatorHarness) armedTimers(t *testing.T) []*signalTimer { | |
| 105 | + | func (h *coordinatorHarness) armedTimers(t *testing.T) []*coordinatorMockTimer { | |
| 54 | 106 | t.Helper() | |
| 55 | - | var timers []*signalTimer | |
| 107 | + | var timers []*coordinatorMockTimer | |
| 56 | 108 | for { | |
| 57 | 109 | select { | |
| 58 | 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 | 168 | t.Helper() | |
| 117 | 169 | select { | |
| 118 | 170 | case tm := <-clk.timers: | |
| @@ -639,10 +691,10 @@ func TestProducerInvalidations(t *testing.T) { | |||
| 639 | 691 | file: "render.go", | |
| 640 | 692 | name: "retained resize dispatch", | |
| 641 | 693 | 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 | |
| 644 | 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 | 802 | func requireWorkerExit(t *testing.T, done <-chan struct{}) { | |
| 768 | 803 | t.Helper() | |
| 769 | 804 | for range 4096 { | |
| @@ -788,8 +823,8 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) { | |||
| 788 | 823 | } | |
| 789 | 824 | for _, tc := range lifecycle { | |
| 790 | 825 | 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}) | |
| 793 | 828 | ac := &attachedClient{} | |
| 794 | 829 | rc.attach(ac) | |
| 795 | 830 | rc.invalidate(renderInvalidation{class: invalidateOutput}) | |
| @@ -798,12 +833,12 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) { | |||
| 798 | 833 | normalDone := rc.normalWorkerDone | |
| 799 | 834 | rc.mu.Unlock() | |
| 800 | 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 | 837 | requireWorkerExit(t, normalDone) | |
| 803 | 838 | ||
| 804 | 839 | // Use a fresh coordinator because detach/teardown intentionally makes | |
| 805 | 840 | // an attachment ineligible for a new synchronized batch. | |
| 806 | - | rc = newRenderCoordinator(renderCoordinatorOptions{clock: clk}) | |
| 841 | + | rc = newRenderCoordinator(renderCoordinatorOptions{clock: clk.clock}) | |
| 807 | 842 | rc.attach(ac) | |
| 808 | 843 | rc.noteSyncBegin(1) | |
| 809 | 844 | syncTimer := <-clk.timers | |
| @@ -811,23 +846,23 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) { | |||
| 811 | 846 | syncDone := rc.syncWorkerDone | |
| 812 | 847 | rc.mu.Unlock() | |
| 813 | 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 | 850 | requireWorkerExit(t, syncDone) | |
| 816 | 851 | }) | |
| 817 | 852 | } | |
| 818 | 853 | } | |
| 819 | 854 | ||
| 820 | 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 | 857 | t.Helper() | |
| 823 | 858 | p, releasePTY := newBlockingPTY(t) | |
| 824 | 859 | t.Cleanup(releasePTY) | |
| 825 | 860 | 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 | |
| 828 | 863 | invs := make(chan renderInvalidation, 4) | |
| 829 | 864 | sess.installRenderCoordinator(newRenderCoordinator(renderCoordinatorOptions{ | |
| 830 | - | clock: clk, | |
| 865 | + | clock: clk.clock, | |
| 831 | 866 | wake: func(renderWake) {}, | |
| 832 | 867 | onInvalidate: func(inv renderInvalidation) { invs <- inv }, | |
| 833 | 868 | })) | |
| @@ -838,7 +873,7 @@ func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) { | |||
| 838 | 873 | d, sess, ac, sends, clk, invs := newResizeFixture(t) | |
| 839 | 874 | ||
| 840 | 875 | d.resize(sess, ac, domain.Size{Cols: 120, Rows: 24}) | |
| 841 | - | timer := awaitScheduledTimer(t, clk) | |
| 876 | + | timer := awaitCoordinatorScheduledTimer(t, clk) | |
| 842 | 877 | require.GreaterOrEqual(t, timer.duration, minOutputRenderDeadline) | |
| 843 | 878 | require.LessOrEqual(t, timer.duration, maxOutputRenderDeadline) | |
| 844 | 879 | ac.sendMu.Lock() | |
| @@ -865,9 +900,9 @@ func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) { | |||
| 865 | 900 | d, sess, ac, sends, clk, invs := newResizeFixture(t) | |
| 866 | 901 | ||
| 867 | 902 | d.resize(sess, ac, domain.Size{Cols: 100, Rows: 24}) | |
| 868 | - | first := awaitScheduledTimer(t, clk) | |
| 903 | + | first := awaitCoordinatorScheduledTimer(t, clk) | |
| 869 | 904 | d.resize(sess, ac, domain.Size{Cols: 120, Rows: 24}) | |
| 870 | - | latest := awaitScheduledTimer(t, clk) | |
| 905 | + | latest := awaitCoordinatorScheduledTimer(t, clk) | |
| 871 | 906 | require.Equal(t, uint64(2), sess.renderCoordinator().resizeSnapshot().epoch, | |
| 872 | 907 | "every resize request must advance the coordinator epoch") | |
| 873 | 908 | ||
| @@ -887,7 +922,7 @@ func TestRenderCoordinatorRetainsPR71ResizeDispatch(t *testing.T) { | |||
| 887 | 922 | d, sess, ac, sends, clk, invs := newResizeFixture(t) | |
| 888 | 923 | ||
| 889 | 924 | d.resize(sess, ac, domain.Size{Cols: 90, Rows: 30}) | |
| 890 | - | timer := awaitScheduledTimer(t, clk) | |
| 925 | + | timer := awaitCoordinatorScheduledTimer(t, clk) | |
| 891 | 926 | ac.cancelResizePaint() | |
| 892 | 927 | ||
| 893 | 928 | 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 recording | 24 | + | // 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 *signalClock | 77 | + | clk *coordinatorMockClock |
| 26 | wakes chan renderWake | 78 | wakes chan renderWake | ||
| 27 | previews chan renderWake | 79 | previews chan renderWake | ||
| 28 | ackReady atomic.Bool | 80 | 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 happens | 102 | // armedTimers drains every deadline armed so far. Arming happens | ||
| 51 | // synchronously inside the coordinator call, so a non-blocking drain is | 103 | // 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 []*signalTimer | 107 | + | 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 = clk | 695 | + | 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.normalWorkerDone | 833 | 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 makes | 839 | // 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.timers | 844 | syncTimer := <-clk.timers | ||
| @@ -811,23 +846,23 @@ func TestRenderCoordinatorStopsInertTimerWorkers(t *testing.T) { | |||||
| 811 | syncDone := rc.syncWorkerDone | 846 | 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 = clk | 862 | + | 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 | |||
| 221 | 221 | tb := newTestTabWithContext(p, sctx, cancel) | |
| 222 | 222 | sess := &session{id: "sync-urgent", name: "sync-urgent", tabs: []*tab{tb}, ctx: sctx, cancel: cancel} | |
| 223 | 223 | invs := make(chan renderInvalidation, 1) | |
| 224 | - | clock := &signalClock{timers: make(chan *signalTimer, 2)} | |
| 224 | + | clock := newCoordinatorMockClock(t, 2) | |
| 225 | 225 | sess.installRenderCoordinator(newRenderCoordinator(renderCoordinatorOptions{ | |
| 226 | - | clock: clock, | |
| 226 | + | clock: clock.clock, | |
| 227 | 227 | onInvalidate: func(inv renderInvalidation) { invs <- inv }, | |
| 228 | 228 | })) | |
| 229 | 229 | d.sessions[sess.id] = sess | |
| @@ -232,7 +232,7 @@ func TestPTYReaderSameReadSynchronizedOutputUsesUrgentCoordinatorDeadline(t *tes | |||
| 232 | 232 | ||
| 233 | 233 | inv := awaitInvalidation(t, invs) | |
| 234 | 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 | 236 | require.Equal(t, urgentRenderDeadline, timer.duration) | |
| 237 | 237 | } | |
| 238 | 238 | ||
| @@ -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] = sess | 229 | 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 | ||||