1package daemon
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/vev/internal/ports"
8 portsmocks "github.com/bnema/vev/internal/ports/mocks"
9 "github.com/stretchr/testify/require"
10)
11
12func TestUIFenceRegistrationAndCapturedThreshold(t *testing.T) {
13 for _, scenario := range []string{"processed", "rebind", "detach", "teardown"} {
14 t.Run(scenario, func(t *testing.T) {
15 rc := newRenderCoordinator(renderCoordinatorOptions{})
16 ac := &attachedClient{}
17 lease := rc.attachWithReadiness(ac, true)
18 t.Cleanup(func() { rc.beginSessionTeardown().finish(); rc.waitForTimerWorkers() })
19 before := rc.captureUIFence(lease)
20 require.False(t, rc.registerUIFence(lease, 0, nil))
21 require.True(t, rc.registerUIFence(lease, 7, nil))
22 require.False(t, rc.registerUIFence(lease, 7, nil))
23 require.False(t, rc.registerUIFence(lease, 8, nil))
24 require.Nil(t, rc.retireUIFence(lease, before), "a capture already underway cannot confirm new input")
25 captured := rc.captureUIFence(lease)
26 require.Greater(t, captured, before)
27 switch scenario {
28 case "rebind":
29 rc.mu.Lock()
30 replacement := rc.rebindAttachmentWithReadinessLocked(ac, true)
31 rc.mu.Unlock()
32 require.Zero(t, rc.captureUIFence(replacement))
33 case "detach":
34 rc.beginDetach(ac).finish()
35 case "teardown":
36 rc.beginSessionTeardown().finish()
37 }
38 pending := rc.retireUIFence(lease, captured)
39 if scenario != "processed" {
40 require.Nil(t, pending)
41 return
42 }
43 require.NotNil(t, pending)
44 require.Equal(t, uint64(7), pending.actionID)
45 require.Nil(t, rc.retireUIFence(lease, captured), "only one sender owns retirement")
46 require.False(t, rc.registerUIFence(lease, 8, nil), "a selected receipt still owns the slot until accepted send")
47 rc.finishUIFence(lease, pending)
48 require.True(t, rc.registerUIFence(lease, 8, nil))
49 require.Nil(t, rc.retireUIFence(lease, captured), "an older confirmed capture cannot complete the next action")
50 })
51 }
52}
53
54func TestUIFenceRegistrationDoesNotRunInvalidationCallbackInline(t *testing.T) {
55 entered, release := make(chan struct{}), make(chan struct{})
56 rc := newRenderCoordinator(renderCoordinatorOptions{onInvalidate: func(renderInvalidation) {
57 close(entered)
58 <-release
59 }})
60 lease := rc.attachWithReadiness(&attachedClient{}, true)
61 t.Cleanup(func() { close(release); rc.beginSessionTeardown().finish(); rc.waitForTimerWorkers() })
62 returned := make(chan bool, 1)
63 go func() { returned <- rc.registerUIFence(lease, 1, nil) }()
64 require.True(t, awaitTestValue(t, returned, "fence dispatch waited for render work"))
65 awaitTestValue(t, entered, "invalidation work was not started")
66}
67
68func TestUIFenceExpiryAndCancellation(t *testing.T) {
69 for _, scenario := range []string{"expire", "detach", "retire"} {
70 t.Run(scenario, func(t *testing.T) {
71 clock := portsmocks.NewMockClock(t)
72 timer := portsmocks.NewMockTimer(t)
73 deadline := make(chan time.Time, 1)
74 armed, stopped := make(chan struct{}), make(chan struct{})
75 clock.EXPECT().NewTimer(30 * time.Second).RunAndReturn(func(time.Duration) ports.Timer { return timer }).Once()
76 timer.EXPECT().C().RunAndReturn(func() <-chan time.Time { close(armed); return deadline }).Once()
77 timer.EXPECT().Stop().Run(func() { close(stopped) }).Return(true).Once()
78 // Leave an invalidation pending without a timer so registration does
79 // not need another clock expectation unrelated to fence expiry.
80 rc := newRenderCoordinator(renderCoordinatorOptions{clock: clock})
81 lease := rc.attachWithReadiness(&attachedClient{}, true)
82 rc.pending, rc.pendingUrgent, rc.armed = true, true, true
83 expired := make(chan uint64, 1)
84 t.Cleanup(func() { rc.beginSessionTeardown().finish(); rc.waitForTimerWorkers() })
85 require.True(t, rc.registerUIFence(lease, 42, func(id uint64) { expired <- id }))
86 awaitTestValue(t, armed, "expiry timer was not armed")
87 switch scenario {
88 case "expire":
89 deadline <- time.Time{}
90 require.Equal(t, uint64(42), awaitTestValue(t, expired, "fence did not expire"))
91 case "detach":
92 rc.beginDetach(lease.attachment).finish()
93 case "retire":
94 pending := rc.retireUIFence(lease, rc.captureUIFence(lease))
95 require.NotNil(t, pending)
96 rc.finishUIFence(lease, pending)
97 }
98 awaitTestValue(t, stopped, "fence timer was not stopped")
99 rc.waitForTimerWorkers()
100 require.Empty(t, expired)
101 require.Nil(t, rc.retireUIFence(lease, ^uint64(0)))
102 })
103 }
104}
105
106func TestUIFenceThresholdExhaustion(t *testing.T) {
107 rc := newRenderCoordinator(renderCoordinatorOptions{})
108 lease := rc.attachWithReadiness(&attachedClient{}, true)
109 lease.requestedUIFence = ^uint64(0)
110 require.False(t, rc.registerUIFence(lease, 1, nil))
111 require.False(t, rc.pending, "refusal must not reserve invalidation")
112}