1package daemon
2
3import (
4 "sync"
5 "testing"
6
7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"
9 "github.com/stretchr/testify/require"
10)
11
12func registryTestSession() *session {
13 sz := domain.Size{Cols: 80, Rows: 23}
14 first := newTabWithStableID("tab-a", "pane-a", nil, sz)
15 second := newTabWithStableID("tab-b", "pane-b", nil, sz)
16 return &session{
17 sessionCore: sessionCore{id: "registry-test"},
18 tabs: []*tab{first, second},
19 }
20}
21
22func registryTestAttachment(id byte) *attachedClient {
23 ac := &attachedClient{}
24 ac.clientID[0] = id
25 return ac
26}
27
28func TestAttachmentRegistryPreservesOtherAttachmentsOnDetach(t *testing.T) {
29 sess := registryTestSession()
30 first, second, third := registryTestAttachment(1), registryTestAttachment(2), registryTestAttachment(3)
31
32 for _, ac := range []*attachedClient{first, second, third} {
33 if !sess.registerAttachment(ac) {
34 t.Fatalf("register %v failed", ac.clientID[0])
35 }
36 }
37 if got := sess.snapshotAttachments(); len(got) != 3 {
38 t.Fatalf("attachment count = %d, want 3", len(got))
39 }
40 if !sess.unregisterAttachment(second) {
41 t.Fatal("unregister second failed")
42 }
43 got := sess.snapshotAttachments()
44 if len(got) != 2 || got[0] != first || got[1] != third {
45 t.Fatalf("remaining attachments = %v, want first and third", got)
46 }
47}
48
49func TestAttachmentRegistryRepairsRemovedStableTarget(t *testing.T) {
50 sess := registryTestSession()
51 ac := registryTestAttachment(1)
52 if !sess.registerAttachment(ac) || !sess.selectAttachmentTab(ac, "tab-b") {
53 t.Fatal("failed to select second tab")
54 }
55 before := ac.viewSnapshot()
56 if before.tabID != "tab-b" || before.paneID != "pane-b" {
57 t.Fatalf("selected view = %#v", before)
58 }
59
60 if err := sess.runMutation(func() error {
61 sess.mu.Lock()
62 defer sess.mu.Unlock()
63 sess.tabs = sess.tabs[:1]
64 sess.repairAttachmentViewsLocked()
65 return nil
66 }); err != nil {
67 t.Fatal(err)
68 }
69 after := ac.viewSnapshot()
70 if after.tabID != "tab-a" || after.paneID != "pane-a" {
71 t.Fatalf("repaired view = %#v, want tab-a/pane-a", after)
72 }
73 if after.revision <= before.revision || !after.liveBottom {
74 t.Fatalf("repaired view revision/liveBottom = %d/%v, want revision increase and live bottom", after.revision, after.liveBottom)
75 }
76}
77
78func TestTabForAttachmentRepairRebasesOutputBeforePublishingRevision(t *testing.T) {
79 sess := registryTestSession()
80 ac := registryTestAttachment(1)
81 ac.size = domain.Size{Cols: 80, Rows: 23}
82 ac.output = newOutputStateStream()
83 ac.output.attachment = ac
84 require.True(t, sess.registerAttachment(ac))
85 require.True(t, sess.selectAttachmentTab(ac, "tab-b"))
86
87 before := ac.viewSnapshot()
88 beforeEpoch := ac.output.currentEpoch()
89 require.NoError(t, sess.runMutation(func() error {
90 sess.mu.Lock()
91 sess.tabs = sess.tabs[:1]
92 sess.mu.Unlock()
93 return nil
94 }))
95
96 require.Same(t, sess.tabs[0], sess.tabForAttachment(ac))
97 after := ac.viewSnapshot()
98 require.Greater(t, after.revision, before.revision)
99 require.Greater(t, ac.output.currentEpoch(), beforeEpoch)
100 frame, err := ac.output.sideEffect([]byte("x"), 0)
101 require.NoError(t, err)
102 output, err := ports.UnmarshalOutput(frame.Payload)
103 require.NoError(t, err)
104 require.Equal(t, after.revision, output.ViewRevision)
105 require.Equal(t, ac.output.currentEpoch(), output.Epoch)
106}
107
108func TestPrepareRemovedTabViewRebasesOutputBeforeSideEffect(t *testing.T) {
109 sess := registryTestSession()
110 ac := registryTestAttachment(1)
111 ac.size = domain.Size{Cols: 80, Rows: 23}
112 ac.output = newOutputStateStream()
113 ac.output.attachment = ac
114 require.True(t, sess.registerAttachment(ac))
115 require.True(t, sess.selectAttachmentTab(ac, "tab-b"))
116
117 before := ac.viewSnapshot()
118 beforeEpoch := ac.output.currentEpoch()
119 sess.mu.Lock()
120 sess.prepareAttachmentViewsForRemovedTabLocked(sess.tabs[1], 1)
121 sess.tabs = sess.tabs[:1]
122 sess.mu.Unlock()
123
124 after := ac.viewSnapshot()
125 require.Equal(t, domain.TabStableID("tab-a"), after.tabID)
126 require.Equal(t, domain.PaneStableID(""), after.paneID)
127 require.Equal(t, before.revision+1, after.revision)
128 require.Greater(t, ac.output.currentEpoch(), beforeEpoch)
129
130 frame, err := ac.output.sideEffect([]byte("replacement"), 0)
131 require.NoError(t, err)
132 output, err := ports.UnmarshalOutput(frame.Payload)
133 require.NoError(t, err)
134 require.Equal(t, after.revision, output.ViewRevision)
135 require.NotEqual(t, beforeEpoch, output.Epoch)
136 require.Equal(t, ac.output.currentEpoch(), output.Epoch)
137}
138
139func TestAttachmentRegistryViewMutationIsSerialized(t *testing.T) {
140 sess := registryTestSession()
141 ac := registryTestAttachment(1)
142 if !sess.registerAttachment(ac) {
143 t.Fatal("register failed")
144 }
145 before := ac.viewSnapshot()
146 if !sess.updateAttachmentView(ac, func(view *attachmentView) {
147 view.windowTop = 4
148 view.windowRows = 12
149 view.bookmark = 9
150 view.liveBottom = false
151 }) {
152 t.Fatal("view update was not published")
153 }
154 after := ac.viewSnapshot()
155 if after.windowTop != 4 || after.windowRows != 12 || after.bookmark != 9 || after.liveBottom || after.revision != before.revision+1 {
156 t.Fatalf("view after mutation = %#v, want updated fields and one revision", after)
157 }
158}
159
160func TestAttachmentRegistryConcurrentRegistrationSnapshotIsStable(t *testing.T) {
161 sess := registryTestSession()
162 attachments := []*attachedClient{
163 registryTestAttachment(3),
164 registryTestAttachment(1),
165 registryTestAttachment(2),
166 }
167 start := make(chan struct{})
168 var wg sync.WaitGroup
169 for _, ac := range attachments {
170 wg.Add(1)
171 go func(ac *attachedClient) {
172 defer wg.Done()
173 <-start
174 sess.registerAttachment(ac)
175 }(ac)
176 }
177 close(start)
178 wg.Wait()
179
180 got := sess.snapshotAttachments()
181 if len(got) != len(attachments) {
182 t.Fatalf("attachment count = %d, want %d", len(got), len(attachments))
183 }
184 for i, want := range []byte{1, 2, 3} {
185 if got[i].clientID[0] != want {
186 t.Fatalf("snapshot[%d] id = %d, want %d", i, got[i].clientID[0], want)
187 }
188 }
189 ordered := sess.repairAttachmentViewsForTest()
190 for i, want := range []byte{1, 2, 3} {
191 if ordered[i].clientID[0] != want {
192 t.Fatalf("ordered attachment[%d] id = %d, want %d", i, ordered[i].clientID[0], want)
193 }
194 }
195}
196
197func (s *session) repairAttachmentViewsForTest() []*attachedClient {
198 s.mu.Lock()
199 defer s.mu.Unlock()
200 s.repairAttachmentViewsLocked()
201 return s.snapshotAttachmentsLocked()
202}