server_test.go RawHistoryFolder

1package uidriver
2
3import (
4	"bytes"
5	"context"
6	"encoding/json"
7	"io"
8	"net"
9	"strings"
10	"testing"
11	"time"
12
13	"github.com/bnema/vev/internal/ports"
14	portsmocks "github.com/bnema/vev/internal/ports/mocks"
15	"github.com/stretchr/testify/mock"
16	"github.com/stretchr/testify/require"
17)
18
19func testSnapshot() ports.UISnapshot {
20	return ports.UISnapshot{Revision: 1, Columns: 1, Rows: 1, Context: ports.UIContext{AttachmentHandle: "a", Generation: 1, Status: ports.UIStatusAttached}, Cells: []ports.UICell{{Text: "x", Width: 1}}}
21}
22
23func testReady(control bool) Ready {
24	return Ready{Attachment: "a", Generation: 1, Control: control, Status: ports.UIStatusAttached}
25}
26
27func readEnvelope(t *testing.T, decoder *json.Decoder) envelope {
28	t.Helper()
29	var response envelope
30	require.NoError(t, decoder.Decode(&response))
31	return response
32}
33
34func TestServeCaptureProceedsWhileWaitIsPending(t *testing.T) {
35	service := portsmocks.NewMockUIService(t)
36	started := make(chan struct{})
37	release := make(chan struct{})
38	service.EXPECT().Wait(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, _ ports.UIWaitRequest) (ports.UIWaitResult, error) {
39		close(started)
40		select {
41		case <-release:
42			return ports.UIWaitResult{Revision: 2}, nil
43		case <-ctx.Done():
44			return ports.UIWaitResult{}, ctx.Err()
45		}
46	}).Once()
47	service.EXPECT().Capture("a").Return(testSnapshot(), nil).Once()
48	server := New(service, nil)
49	host, peer := net.Pipe()
50	defer func() { _ = peer.Close() }()
51	require.NoError(t, peer.SetDeadline(time.Now().Add(2*time.Second)))
52	done := make(chan error, 1)
53	go func() { done <- server.Serve(context.Background(), host, testReady(true)) }()
54	decoder := json.NewDecoder(peer)
55	ready := readEnvelope(t, decoder)
56	require.Zero(t, ready.ID)
57	_, err := io.WriteString(peer, "{\"version\":1,\"id\":1,\"op\":\"wait\",\"attachment\":\"a\",\"expect\":{\"status\":\"attached\"}}\n")
58	require.NoError(t, err)
59	select {
60	case <-started:
61	case <-time.After(time.Second):
62		t.Fatal("wait not started")
63	}
64	for range 2 {
65		_, err = io.WriteString(peer, "{\"version\":1,\"id\":1,\"op\":\"capture\",\"attachment\":\"a\"}\n")
66		require.NoError(t, err)
67		duplicate := readEnvelope(t, decoder)
68		require.Equal(t, ports.UIErrInvalidRequest, duplicate.Error.Code)
69	}
70	_, err = io.WriteString(peer, "{\"version\":1,\"id\":2,\"op\":\"capture\",\"attachment\":\"a\"}\n")
71	require.NoError(t, err)
72	capture := readEnvelope(t, decoder)
73	require.Equal(t, uint64(2), capture.ID)
74	require.Nil(t, capture.Error)
75	close(release)
76	waited := readEnvelope(t, decoder)
77	require.Equal(t, uint64(1), waited.ID)
78	require.Nil(t, waited.Error)
79	require.NoError(t, peer.Close())
80	select {
81	case err := <-done:
82		require.NoError(t, err)
83	case <-time.After(time.Second):
84		t.Fatal("connection did not stop")
85	}
86	require.Zero(t, server.serialized)
87}
88
89func TestServeReadOnlyRejectsBeforeServiceAction(t *testing.T) {
90	service := portsmocks.NewMockUIService(t)
91	host, peer := net.Pipe()
92	defer func() { _ = peer.Close() }()
93	require.NoError(t, peer.SetDeadline(time.Now().Add(2*time.Second)))
94	done := make(chan error, 1)
95	go func() { done <- New(service, nil).Serve(context.Background(), host, testReady(false)) }()
96	decoder := json.NewDecoder(peer)
97	readEnvelope(t, decoder)
98	_, err := io.WriteString(peer, "{\"version\":1,\"id\":1,\"op\":\"text\",\"attachment\":\"a\",\"generation\":1,\"text\":\"x\"}\n")
99	require.NoError(t, err)
100	denied := readEnvelope(t, decoder)
101	require.Equal(t, ports.UIErrPermissionDenied, denied.Error.Code)
102	require.False(t, denied.Error.Accepted)
103	require.NoError(t, peer.Close())
104	select {
105	case <-done:
106	case <-time.After(time.Second):
107		t.Fatal("connection did not stop")
108	}
109	service.AssertNotCalled(t, "Action", mock.Anything, mock.Anything)
110}
111
112type finiteStream struct {
113	input *strings.Reader
114	bytes.Buffer
115}
116
117func (s *finiteStream) Read(data []byte) (int, error) { return s.input.Read(data) }
118func (*finiteStream) Close() error                    { return nil }
119
120func TestServeAcceptsFinalObjectWithoutNewline(t *testing.T) {
121	service := portsmocks.NewMockUIService(t)
122	service.EXPECT().Capture("a").Return(testSnapshot(), nil).Once()
123	stream := &finiteStream{input: strings.NewReader(`{"version":1,"id":1,"op":"capture","attachment":"a"}`)}
124	server := New(service, nil)
125	require.NoError(t, server.Serve(context.Background(), stream, testReady(true)))
126	decoder := json.NewDecoder(bytes.NewReader(stream.Bytes()))
127	require.Zero(t, readEnvelope(t, decoder).ID)
128	require.Equal(t, uint64(1), readEnvelope(t, decoder).ID)
129	require.Zero(t, server.serialized)
130}
131
132func TestServeWriteDeadlineClosesNonreadingStream(t *testing.T) {
133	clock := portsmocks.NewMockClock(t)
134	timer := portsmocks.NewMockTimer(t)
135	expired := make(chan time.Time, 1)
136	created := make(chan struct{})
137	clock.EXPECT().NewTimer(writeTimeout).RunAndReturn(func(time.Duration) ports.Timer { close(created); return timer }).Once()
138	timer.EXPECT().C().Return(expired).Once()
139	timer.EXPECT().Stop().Return(true).Once()
140	server := New(nil, clock)
141	host, peer := net.Pipe()
142	defer func() { _ = peer.Close() }()
143	done := make(chan error, 1)
144	go func() { done <- server.Serve(context.Background(), host, testReady(false)) }()
145	select {
146	case <-created:
147	case <-time.After(time.Second):
148		t.Fatal("writer did not arm deadline")
149	}
150	expired <- time.Time{}
151	select {
152	case err := <-done:
153		require.Error(t, err)
154	case <-time.After(time.Second):
155		t.Fatal("nonreading observer retained workers")
156	}
157	require.Zero(t, server.serialized)
158	require.Zero(t, server.connections)
159}
160
161func TestSerializedBudgetKeepsSpaceForBusyErrors(t *testing.T) {
162	server := New(nil, nil)
163	for range 2 {
164		require.True(t, server.reserve(maxResponseBytes))
165	}
166	require.False(t, server.reserve(maxResponseBytes))
167	require.True(t, server.reserve(errorResponseReserve))
168	require.LessOrEqual(t, server.serialized, maxSerializedBytes)
169}

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