1package daemon
2
3import (
4 "testing"
5 "time"
6
7 "github.com/stretchr/testify/require"
8
9 "github.com/bnema/vev/internal/domain"
10 portsmocks "github.com/bnema/vev/internal/ports/mocks"
11)
12
13func TestTimerLaneDetachRemovesCompleteToken(t *testing.T) {
14 lane := timerLane{generation: 1}
15 timer := portsmocks.NewMockTimer(t)
16 token := lane.publishLocked(1, timer)
17 require.NotNil(t, token)
18 detached := lane.detachLocked()
19 require.Same(t, token, detached)
20 require.Nil(t, lane.token)
21 select {
22 case <-token.cancel:
23 default:
24 t.Fatal("detach did not cancel token")
25 }
26 require.Nil(t, lane.publishLocked(0, timer), "stale generation reacquired detached lane")
27}
28
29func TestRenderDeadlineCallbackCanInvalidateAgainWithoutJoiningItself(t *testing.T) {
30 clock := newCoordinatorMockClock(t, 2)
31 ac := &attachedClient{}
32 wakeReturned := make(chan struct{})
33 var rc *renderCoordinator
34 rc = newRenderCoordinator(renderCoordinatorOptions{
35 clock: clock.clock,
36 wake: func(renderWake) {
37 require.True(t, rc.invalidateForAttachment(ac, renderInvalidation{class: invalidateUrgent, producer: "reentrant-wake-test"}))
38 close(wakeReturned)
39 },
40 })
41 rc.attach(ac)
42 t.Cleanup(func() {
43 rc.beginSessionTeardown().finish()
44 rc.waitForTimerWorkers()
45 })
46 require.True(t, rc.invalidateForAttachment(ac, renderInvalidation{class: invalidateUrgent, producer: "initial-wake-test"}))
47 awaitCoordinatorScheduledTimer(t, clock).ch <- time.Time{}
48 awaitTestCompletion(t, wakeReturned, "timer callback deadlocked while publishing its next invalidation")
49}
50
51func TestRenderTimerCallbacksCanReplaceTheirOwnLane(t *testing.T) {
52 cases := []struct {
53 name string
54 run func(*testing.T, *renderCoordinator, *coordinatorMockClock, *attachedClient)
55 }{
56 {
57 name: "normal wake invalidates", run: func(t *testing.T, rc *renderCoordinator, clock *coordinatorMockClock, ac *attachedClient) {
58 woke := make(chan struct{})
59 rc.opts.wake = func(renderWake) {
60 rc.invalidateForAttachment(ac, renderInvalidation{class: invalidateUrgent, producer: "table-normal"})
61 close(woke)
62 }
63 rc.invalidateForAttachment(ac, renderInvalidation{class: invalidateUrgent, producer: "table-normal"})
64 awaitCoordinatorScheduledTimer(t, clock).ch <- time.Time{}
65 awaitTestCompletion(t, woke, "normal callback did not reenter invalidation")
66 },
67 },
68 {
69 name: "resize callback schedules resize", run: func(t *testing.T, rc *renderCoordinator, clock *coordinatorMockClock, ac *attachedClient) {
70 lease := rc.attachmentLease(ac)
71 rc.scheduleResizeForLease(domain.Size{Cols: 80, Rows: 24}, ac, lease, func(uint64) {
72 rc.scheduleResizeForLease(domain.Size{Cols: 81, Rows: 24}, ac, lease, func(uint64) {})
73 })
74 timer := awaitCoordinatorScheduledTimer(t, clock)
75 done := captureResizeCallbackDone(t, rc)
76 timer.ch <- time.Time{}
77 _ = awaitTestValue(t, clock.timers, "resize callback did not schedule replacement")
78 awaitTestCompletion(t, done, "resize callback did not complete")
79 },
80 },
81 {
82 name: "retry callback schedules retry", run: func(t *testing.T, rc *renderCoordinator, clock *coordinatorMockClock, ac *attachedClient) {
83 lease := rc.attachmentLease(ac)
84 epoch := rc.recordResizeRequestForLease(domain.Size{Cols: 80, Rows: 24}, ac, lease)
85 require.True(t, rc.resizeCurrentForLease(epoch, ac, lease, true))
86 rc.scheduleResizeRetryForLease(epoch, ac, lease, func() { rc.scheduleResizeRetryForLease(epoch, ac, lease, func() {}) })
87 awaitCoordinatorScheduledTimer(t, clock).ch <- time.Time{}
88 _ = awaitTestValue(t, clock.timers, "retry callback did not schedule replacement")
89 },
90 },
91 {
92 name: "sync force transitions sync generation", run: func(t *testing.T, rc *renderCoordinator, clock *coordinatorMockClock, _ *attachedClient) {
93 p := &pane{}
94 rc.noteSyncBegin(p, 1, func() { rc.noteSyncBegin(p, 2) })
95 awaitCoordinatorScheduledTimer(t, clock).ch <- time.Time{}
96 _ = awaitTestValue(t, clock.timers, "sync force did not schedule replacement")
97 },
98 },
99 }
100 for _, tc := range cases {
101 t.Run(tc.name, func(t *testing.T) {
102 clock := newCoordinatorMockClock(t, 4)
103 rc, ac := newRenderCoordinator(renderCoordinatorOptions{clock: clock.clock}), &attachedClient{}
104 rc.attach(ac)
105 tc.run(t, rc, clock, ac)
106 rc.beginSessionTeardown().finish()
107 rc.waitForTimerWorkers()
108 })
109 }
110}
111
112func TestRenderTimerScheduleRacesTerminalTeardown(t *testing.T) {
113 for range 32 {
114 clock := newCoordinatorMockClock(t, 2)
115 rc, ac := newRenderCoordinator(renderCoordinatorOptions{clock: clock.clock}), &attachedClient{}
116 rc.attach(ac)
117 start := make(chan struct{})
118 scheduled := make(chan struct{})
119 go func() {
120 <-start
121 rc.invalidateForAttachment(ac, renderInvalidation{class: invalidateUrgent, producer: "race"})
122 close(scheduled)
123 }()
124 close(start)
125 cleanup := rc.beginSessionTeardown()
126 awaitTestCompletion(t, scheduled, "schedule raced terminal teardown")
127 cleanup.finish()
128 rc.waitForTimerWorkers()
129 rc.mu.Lock()
130 require.True(t, rc.torndown)
131 require.Nil(t, rc.normalLane.token)
132 rc.mu.Unlock()
133 }
134}
135
136func TestTimerSupervisorCancelsNeverFiringWorker(t *testing.T) {
137 var lane timerLane
138 generation, _ := lane.replaceLocked()
139 token := lane.publishLocked(generation, portsmocks.NewMockTimer(t))
140 var supervisor timerSupervisor
141 supervisor.startLocked(token, nil, func() { t.Fatal("cancelled worker fired") })
142 lane.detachLocked()
143 supervisor.wait()
144}