v0.3.0 99dc266

attachment_registry_test.go RawHistoryFolder

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}

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