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