attachment_registry_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close